📷 Single-user personal photo gallery and manager.
- Masonry layout that respects original aspect ratios
- Responsive mobile-first UI
- Configurable UI via
src/config/site.ts - Filters & sorting by category, orientation, date, popularity
- Lightbox viewer with keyboard controls
- Admin panel at
/admin - Workers-ready OpenNext deployment
- Node.js 18+
- npm or pnpm
-
Clone the repository:
git clone <repository-url> cd photos
-
Install dependencies:
npm install
-
Configure Environment: Copy
.env.exampleto.env.localand edit values (seeCONFIGURATION.mdfor full list). -
Run Development Server:
npm run dev
See:
CONFIGURATION.md(EN)CONFIGURATION_ZH.md(䏿–‡)
The project includes a built-in admin panel at /admin. You need to configure the following environment variables:
| Variable | Required | Description |
|---|---|---|
ADMIN_USER |
Yes | Admin username |
ADMIN_PASS_HASH |
Recommended | PBKDF2-SHA256 password hash |
ADMIN_PASS |
Fallback | Plaintext password (not recommended) |
SESSION_SECRET |
Yes | JWT signing secret (at least 32 characters) |
-
Generate a password hash:
node scripts/hash-password.mjs your-password # Output: pbkdf2:100000:<salt>:<hash> -
Set the environment variable:
Cloudflare Workers (recommended):
wrangler secret put ADMIN_USER wrangler secret put ADMIN_PASS_HASH wrangler secret put SESSION_SECRET
Local development (
.env.local):ADMIN_USER=admin ADMIN_PASS_HASH=pbkdf2:100000:xxxx:xxxx SESSION_SECRET=your-random-secret-at-least-32-chars
Note: If
ADMIN_PASS_HASHis set, plaintextADMIN_PASSwill be ignored. Login is rate-limited to 5 attempts per 15 minutes per IP (requires KV binding).
For a single-container deployment with SQLite and local media storage:
If you want to deploy using a published Docker image, see DEPLOY_DOCKER.md.
- Set these environment variables:
SQLITE_PATH=/data/pluto.db
MEDIA_DEFAULT_PROVIDER=local
MEDIA_LOCAL_DIR=/data/uploads
MEDIA_LOCAL_PUBLIC_URL=https://your-domain/uploadsYou can also set MEDIA_LOCAL_PUBLIC_URL=/uploads.
2. Initialize the database once:
sqlite3 /data/pluto.db < sql/init_d1.sql- Run:
npm run build
npm run startMake sure /data is a persistent volume in Docker.
docker build -t pluto:local .
docker run --rm -p 3000:3000 \
-e NODE_ENV=production \
-e SQLITE_PATH=/data/pluto.db \
-e MEDIA_DEFAULT_PROVIDER=local \
-e MEDIA_LOCAL_DIR=/data/uploads \
-e MEDIA_LOCAL_PUBLIC_URL=/uploads \
-e NEXT_PUBLIC_BASE_URL=http://localhost:3000 \
-e ADMIN_USER=admin \
-e ADMIN_PASS_HASH=pbkdf2:100000:<salt_hex>:<hash_hex> \
-e SESSION_SECRET=change-me-to-a-long-random-string \
-v $(pwd)/data:/data \
pluto:localOr use docker-compose:
docker compose up --buildThis starts an Nginx sidecar on http://localhost:8080.
If you want Nginx to serve local media directly:
location /uploads/ {
alias /data/uploads/;
access_log off;
add_header Cache-Control "public, max-age=31536000, immutable";
}Set MEDIA_LOCAL_PUBLIC_URL=https://your-domain/uploads and ensure /data/uploads is mounted.
If you prefer to serve files directly from Next.js, you can mount the volume to public/uploads and set MEDIA_LOCAL_DIR=public/uploads with MEDIA_LOCAL_PUBLIC_URL=/uploads.
See nginx/nginx.conf for a full working example.
This project targets Cloudflare Workers via OpenNext.
- Manual deploy:
npm run deploy - Git auto-builds: Workers Builds (Git-connected)
Details:
DEPLOY_WORKERS.mdDEPLOY_WORKERS_ZH.md- Docker image deployment:
DEPLOY_DOCKER.md
See:
API_DOC.md(EN)API_DOC_ZH.md(䏿–‡)
- Framework: Next.js 15 (App Router)
- Language: TypeScript
- Styling: Tailwind CSS & SCSS Modules
- Icons: Lucide React
- Deployment: Cloudflare Workers (OpenNext)