ソースを参照

security: move all secrets to .env, remove hardcoded credentials

docker-compose.yml:
- All hardcoded credentials removed (postgres password, DB URL, IPs)
- Now uses ${VAR:?Required} for required secrets so deploy fails fast if .env missing
- Removed stale local IPs from ALLOWED_ORIGINS
- init/admin: now reads DB credentials from .env via env vars

.env / .env.example:
- All vars now consolidated: DB (POSTGRES_*), auth (JWT_*), server, CORS, email, admin
- NEXT_PUBLIC_API_URL added (was missing from .env, causing blank var)
- .env.example updated with placeholder values and clear comments

Security audit:
- POSTGRES_PASSWORD: exposed in compose but from .env (already in .gitignore)
- JWT_SECRET: placeholder (user must replace)
- RESEND_API_KEY: empty by default (user fills in if they want email)
- No Resend key leaked
- No JWT secret leaked
- No real IPs leaked

README.md: Updated deployment section and env var table to reflect .env-based config
Claude Dev 1 ヶ月 前
コミット
20ba93b6b0
3 ファイル変更95 行追加65 行削除
  1. 30 12
      .env.example
  2. 38 28
      README.md
  3. 27 25
      docker-compose.yml

+ 30 - 12
.env.example

@@ -1,17 +1,35 @@
-# Database
-DATABASE_URL="postgresql://vidreview:vidreview123@localhost:5432/vidreview"
+# Copy this file to .env and fill in the values.
+# NEVER commit .env to version control.
 
-# JWT
-JWT_SECRET="your-super-secret-jwt-key-change-this-in-production"
-JWT_EXPIRES_IN="7d"
+# ── Database ──────────────────────────────────────────────────────────────────
+POSTGRES_USER=vidreview
+POSTGRES_PASSWORD=<change-me>
+POSTGRES_DB=vidreview
+DATABASE_URL=postgresql://vidreview:<change-me>@postgres:5432/vidreview
 
-# Server
-API_PORT=3001
-NODE_ENV=development
+# ── Auth ─────────────────────────────────────────────────────────────────────
+# Generate a strong secret: openssl rand -hex 64
+JWT_SECRET=<change-me>
+JWT_EXPIRES_IN=7d
 
-# File Storage (relative to api container)
-UPLOAD_DIR="./uploads"
+# ── Server ─────────────────────────────────────────────────────────────────────
+API_PORT=3001
+NODE_ENV=production
+UPLOAD_DIR=/app/uploads
 MAX_FILE_SIZE_MB=500
 
-# Frontend
-NEXT_PUBLIC_API_URL="http://localhost:3001"
+# ── CORS / Domain ──────────────────────────────────────────────────────────────
+# Comma-separated list of allowed origins (no spaces)
+ALLOWED_ORIGINS=https://your-domain.com,http://localhost
+FRONTEND_URL=https://your-domain.com
+
+# ── Email (Resend) ─────────────────────────────────────────────────────────────
+# Optional. Leave blank to disable invite emails.
+RESEND_API_KEY=
+
+# ── Admin (used by init container on fresh deploy only) ────────────────────────
+ADMIN_EMAIL=admin@vidreview.local
+ADMIN_NAME=Admin
+
+# ── Worker ─────────────────────────────────────────────────────────────────────
+POLL_INTERVAL_MS=2000

+ 38 - 28
README.md

@@ -280,8 +280,7 @@ cd vidreview
 
 # 2. Configure environment
 cp .env.example .env
-# Edit .env — at minimum set a strong JWT_SECRET:
-# JWT_SECRET=$(openssl rand -hex 32)
+# Edit .env with your production values (see Environment Variables below)
 
 # 3. Start all services
 sudo docker compose up -d
@@ -289,33 +288,33 @@ sudo docker compose up -d
 # 4. Verify health
 sudo docker compose ps
 # All containers should show "healthy" or "Up"
-
-# 5. First-time DB schema push
-sudo docker exec vidreview-api npx prisma db push
 ```
 
-**Default credentials** (dev/demo):
-```
-Email:    admin@vidreview.local
-Password: admin123
+On a **fresh deploy**, the `init` container runs automatically and creates an admin account.
+Credentials are saved to `/seed-output/admin-credentials.txt` inside the `seed_output` Docker volume.
+Read them with:
+```bash
+sudo docker compose run --rm --entrypoint "cat /seed-output/admin-credentials.txt" init
 ```
 
+On an **update deploy**, the init container skips admin creation silently.
+
 ### Production Deployment Checklist
 
-- [ ] Generate a strong `JWT_SECRET` (minimum 32 random bytes)
-- [ ] Set `NODE_ENV=production` on all services
-- [ ] Set `ALLOWED_ORIGINS` to your domain (e.g., `https://vidreview.example.com`)
-- [ ] Configure TLS/SSL (reverse proxy in front of `:3000`)
-- [ ] Increase `POLL_INTERVAL_MS` if worker CPU is high (default: 2000ms)
-- [ ] Set `MAX_FILE_SIZE_MB` based on storage capacity
-- [ ] Mount a persistent volume for `/app/uploads` (video files can be large)
+- [ ] Generate a strong `JWT_SECRET` (`openssl rand -hex 64`)
+- [ ] Set `ALLOWED_ORIGINS` to your domain
+- [ ] Set `FRONTEND_URL` to your public URL
+- [ ] Set a strong `POSTGRES_PASSWORD`
+- [ ] Optionally set `RESEND_API_KEY` to enable invite emails
+- [ ] Mount a persistent named volume for `uploads:/app/uploads`
 - [ ] Set up automated backups for the PostgreSQL volume
 - [ ] Prune old completed assets to free disk space
 
 ### Updating
 
 ```bash
-# Rebuild and restart
+# Pull latest code and rebuild
+git pull
 sudo docker compose build
 sudo docker compose up -d
 ```
@@ -325,6 +324,7 @@ sudo docker compose up -d
 ```bash
 # All services
 sudo docker compose logs -f
+sudo docker compose logs -f
 
 # API only
 sudo docker logs vidreview-api -f
@@ -415,15 +415,25 @@ npm run dev:frontend
 
 ## Environment Variables
 
-| Variable | Default | Description |
+> All secrets are read from `.env` — **never hardcoded in `docker-compose.yml`**.
+> Copy `.env.example` → `.env` and fill in your values before deploying.
+
+| Variable | Required | Description |
 |---|---|---|
-| `DATABASE_URL` | `postgresql://vidreview:vidreview123@localhost:5432/vidreview` | PostgreSQL connection string |
-| `JWT_SECRET` | `change-me` | JWT signing secret (**change in production**) |
-| `JWT_EXPIRES_IN` | `7d` | JWT token expiry |
-| `API_PORT` | `3001` | API server port |
-| `NODE_ENV` | `development` | `development` or `production` |
-| `UPLOAD_DIR` | `./uploads` | Local upload directory (API container) |
-| `MAX_FILE_SIZE_MB` | `500` | Max upload file size in MB |
-| `ALLOWED_ORIGINS` | `*` | CORS allowed origins (comma-separated) |
-| `NEXT_PUBLIC_API_URL` | `http://localhost:3001` | API base URL for frontend |
-| `POLL_INTERVAL_MS` | `2000` | Worker poll interval in milliseconds |
+| `POSTGRES_USER` | Yes | PostgreSQL username |
+| `POSTGRES_PASSWORD` | Yes | PostgreSQL password |
+| `POSTGRES_DB` | Yes | PostgreSQL database name |
+| `DATABASE_URL` | Yes | Full PostgreSQL connection string |
+| `JWT_SECRET` | Yes | JWT signing secret — **generate with `openssl rand -hex 64`** |
+| `JWT_EXPIRES_IN` | No | JWT expiry (default: `7d`) |
+| `API_PORT` | No | API server port (default: `3001`) |
+| `NODE_ENV` | No | `production` or `development` (default: `production`) |
+| `UPLOAD_DIR` | No | Upload directory in API container (default: `/app/uploads`) |
+| `MAX_FILE_SIZE_MB` | No | Max upload file size in MB (default: `500`) |
+| `ALLOWED_ORIGINS` | No | Comma-separated CORS origins (default: empty = block all) |
+| `FRONTEND_URL` | No | Public URL of the frontend |
+| `NEXT_PUBLIC_API_URL` | No | API base URL for frontend (default: `https://vid.k9tech.space/api`) |
+| `RESEND_API_KEY` | No | Resend API key for invite emails (optional, leave blank to disable) |
+| `ADMIN_EMAIL` | No | Admin account email on fresh deploy (default: `admin@vidreview.local`) |
+| `ADMIN_NAME` | No | Admin account display name (default: `Admin`) |
+| `POLL_INTERVAL_MS` | No | Worker poll interval in ms (default: `2000`) |

+ 27 - 25
docker-compose.yml

@@ -3,9 +3,9 @@ services:
     image: postgres:16-alpine
     container_name: vidreview-db
     environment:
-      POSTGRES_USER: vidreview
-      POSTGRES_PASSWORD: vidreview123
-      POSTGRES_DB: vidreview
+      POSTGRES_USER: ${POSTGRES_USER:-vidreview}
+      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Required}
+      POSTGRES_DB: ${POSTGRES_DB:-vidreview}
     ports:
       - '5432:5432'
     volumes:
@@ -16,7 +16,9 @@ services:
       timeout: 5s
       retries: 5
 
-  # ── Init (runs once on fresh deploy) ───────────────────────────────────────
+  # ── Init: runs once on fresh deploy ─────────────────────────────────────
+  # Creates admin account, locks registration, saves credentials to seed_output.
+  # Skips silently if DB already has an admin (safe to re-run on updates).
   init:
     build:
       context: .
@@ -24,14 +26,14 @@ services:
     container_name: vidreview-init
     entrypoint: ['bash', '/scripts/init-admin.sh']
     environment:
-      DB_HOST: vidreview-db
-      DB_NAME: vidreview
-      DB_USER: vidreview
-      DB_PASS: vidreview123
+      DB_HOST: postgres
+      DB_NAME: ${POSTGRES_DB:-vidreview}
+      DB_USER: ${POSTGRES_USER:-vidreview}
+      DB_PASS: ${POSTGRES_PASSWORD:?Required}
       API_CONTAINER: vidreview-api
       OUTPUT_DIR: /seed-output
-      ADMIN_EMAIL: admin@vidreview.local
-      ADMIN_NAME: Admin
+      ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@vidreview.local}
+      ADMIN_NAME: ${ADMIN_NAME:-Admin}
     volumes:
       - /var/run/docker.sock:/var/run/docker.sock:rw
       - /usr/bin/docker:/usr/bin/docker:rw
@@ -44,21 +46,22 @@ services:
         condition: service_healthy
     restart: 'no'
 
+  # ── API ─────────────────────────────────────────────────────────────────
   api:
     build:
       context: .
       dockerfile: Dockerfile.api
     container_name: vidreview-api
     environment:
-      DATABASE_URL: postgresql://vidreview:vidreview123@postgres:5432/vidreview
-      JWT_SECRET: ${JWT_SECRET:-change-me-in-production}
+      DATABASE_URL: ${DATABASE_URL:?Required}
+      JWT_SECRET: ${JWT_SECRET:?Required}
       JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-7d}
       API_PORT: 3001
-      NODE_ENV: production
+      NODE_ENV: ${NODE_ENV:-production}
       UPLOAD_DIR: /app/uploads
-      MAX_FILE_SIZE_MB: 500
-      ALLOWED_ORIGINS: 'https://vid.k9tech.space,http://vid.k9tech.space,http://10.147.17.128,http://192.168.1.31'
-      FRONTEND_URL: https://vid.k9tech.space
+      MAX_FILE_SIZE_MB: ${MAX_FILE_SIZE_MB:-500}
+      ALLOWED_ORIGINS: ${ALLOWED_ORIGINS}
+      FRONTEND_URL: ${FRONTEND_URL}
       RESEND_API_KEY: ${RESEND_API_KEY:-}
     ports:
       - '3001:3001'
@@ -73,7 +76,7 @@ services:
       timeout: 5s
       retries: 5
 
-  # ── Transcode Worker ─────────────────────────────────────────────────────
+  # ── Transcode Worker ─────────────────────────────────────────────────────
   worker:
     build:
       context: .
@@ -81,10 +84,10 @@ services:
     container_name: vidreview-worker
     command: node src/worker/index.js
     environment:
-      DATABASE_URL: postgresql://vidreview:vidreview123@postgres:5432/vidreview
-      NODE_ENV: production
+      DATABASE_URL: ${DATABASE_URL:?Required}
+      NODE_ENV: ${NODE_ENV:-production}
       UPLOAD_DIR: /app/uploads
-      POLL_INTERVAL_MS: 2000
+      POLL_INTERVAL_MS: ${POLL_INTERVAL_MS:-2000}
     depends_on:
       postgres:
         condition: service_healthy
@@ -92,9 +95,7 @@ services:
       - uploads:/app/uploads
     restart: unless-stopped
 
-  # ── Caddy Reverse Proxy ───────────────────────────────────────────────────
-  # Receives HTTP traffic forwarded by Synology reverse proxy.
-  # Routes /api/* → api container, everything else → frontend.
+  # ── Caddy Reverse Proxy ──────────────────────────────────────────────────
   caddy:
     image: caddy:2-alpine
     container_name: vidreview-caddy
@@ -109,14 +110,15 @@ services:
       - frontend
       - api
 
+  # ── Frontend ────────────────────────────────────────────────────────────
   frontend:
     build:
       context: .
       dockerfile: Dockerfile.frontend
     container_name: vidreview-frontend
     environment:
-      NEXT_PUBLIC_API_URL: https://vid.k9tech.space/api
-      NODE_ENV: production
+      NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
+      NODE_ENV: ${NODE_ENV:-production}
     expose:
       - '3000'
     depends_on: