This repository has been archived on 2026-06-09. You can view files and clone it, but cannot push or open issues or pull requests.
wedding-app/docker-compose.yml

151 lines
4.4 KiB
YAML
Raw Normal View History

feat: full Docker stack on a VPS with improved admin editing Self-hosted rewrite of the wedding photo app: Node + Hono replaces Cloudflare Workers, Postgres 16 replaces D1, MinIO replaces R2, Caddy fronts the stack with automatic Let's Encrypt TLS. Same routes and feature set as before; storage abstraction is the same S3 client so MinIO drops in without code changes. Architecture: - docker-compose.yml: postgres, minio, minio-init (creates bucket + anonymous read + CORS), app, caddy (reverse proxy + media subdomain). - Dockerfile: multi-stage pnpm build, single runtime image serving the API and the SPA dist as static assets from one process. - Caddyfile: primary domain proxies to app; media subdomain proxies to MinIO so guests upload directly and signatures match Host. - app: tsx runtime, runs SQL migrations idempotently at startup via a schema_migrations(name, sha256, applied_at) table. Admin upgrades requested: - PATCH /api/admin/uploads/:id to edit author/message. - POST /api/admin/uploads/bulk for bulk approve/reject/delete. - POST /api/admin/uploads/:id/cover and DELETE /api/admin/event/cover to set/clear a featured image (rendered on Home when set). - GET /api/admin/uploads gains ?q= text search across author and message and ?kind=photo|video filter. - Dashboard: bulk select checkboxes with a toolbar, edit modal that rewrites author and message, search input, kind filter, set-cover button per item, cover preview + clear in the event card. Singletons replace the per-request bindings pattern: initDb() and initStorage() run once in server.ts; routes call getDb()/getStorage() directly rather than threading env.DB / env.MEDIA through. env.ts uses zod to parse process.env and fails fast if anything mandatory is missing. .env.example documents every variable and flags the hairpin tradeoff for MinIO access from the app container.
2026-06-07 19:11:10 -03:00
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-wedding}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-wedding}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-wedding} -d ${POSTGRES_DB:-wedding}"]
interval: 10s
timeout: 5s
retries: 10
minio:
image: minio/minio:latest
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
volumes:
- minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 15s
timeout: 10s
retries: 10
minio-init:
image: minio/mc:latest
restart: "no"
depends_on:
minio:
condition: service_healthy
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
S3_BUCKET: ${S3_BUCKET:-wedding-media}
entrypoint:
- /bin/sh
- -c
- |
set -e
mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD"
mc mb --ignore-existing local/"$$S3_BUCKET"
mc anonymous set download local/"$$S3_BUCKET"
mc cors set local/"$$S3_BUCKET" /tmp/cors.json || true
echo "minio init done"
volumes:
- ./infra/minio-cors.json:/tmp/cors.json:ro
app:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
minio:
condition: service_healthy
environment:
DATABASE_URL: postgres://${POSTGRES_USER:-wedding}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-wedding}
S3_ENDPOINT: ${S3_INTERNAL_ENDPOINT:-https://${MEDIA_DOMAIN}}
S3_BUCKET: ${S3_BUCKET:-wedding-media}
S3_REGION: ${S3_REGION:-us-east-1}
S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
S3_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
S3_PUBLIC_BASE_URL: https://${MEDIA_DOMAIN}/${S3_BUCKET:-wedding-media}
S3_FORCE_PATH_STYLE: "true"
COUPLE_NAMES: ${COUPLE_NAMES:-Stefanie & Leandro}
EVENT_DATE: ${EVENT_DATE:-}
PUBLIC_BASE_URL: https://${DOMAIN}
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
SESSION_SECRET: ${SESSION_SECRET}
ALLOWED_ADMIN_EMAILS: ${ALLOWED_ADMIN_EMAILS}
AUTO_MIGRATE: "true"
NODE_ENV: production
PORT: 3000
expose:
- "3000"
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
environment:
DOMAIN: ${DOMAIN}
MEDIA_DOMAIN: ${MEDIA_DOMAIN}
ACME_EMAIL: ${ACME_EMAIL}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- app
- minio
feat: HEIC->JPEG transcoding and Postgres + MinIO backup sidecars HEIC handling - pillow-heif joins the deps; app/lib/transcode.py registers the HEIF opener and exposes heic_to_jpeg(bytes, quality=88) with EXIF rotation applied so portrait iPhone photos do not show sideways. - Storage gains get_bytes / put_bytes so the server can read the uploaded HEIC out of MinIO, decode it, and put a JPEG back. - /api/uploads/{id}/confirm now runs the transcode after the HEAD check passes when mime_type is image/heic or image/heif: writes the JPEG under a sibling key, deletes the HEIC, and updates the upload row's storage_key / mime_type / size_bytes. Failures fall back to keeping the HEIC so an upload is never lost in transit; the admin can re-upload if a particular file is unrenderable. Backups (two sidecars in docker-compose.yml) - postgres-backup uses prodrigestivill/postgres-backup-local:16-alpine with BACKUP_SCHEDULE_DB (default @daily) and layered retention (days/weeks/months) writing to ./backups/postgres on the host. - media-backup is an alpine container that pulls mc on boot, sets up an alias for the in-cluster MinIO, optionally adds a remote alias (R2/B2/S3 via BACKUP_REMOTE_*), and runs mc mirror on a configurable cron schedule. Local mirror always; remote mirror only when BACKUP_REMOTE_* are set. - Both write to ./backups/ on the host (bind mount), so the operator can rsync the directory off-box without touching containers. - .env.example documents every new variable, including a R2 example for the remote target, and TZ for cron alignment. Local backups directory is .gitignore'd so accidental commits do not ship someone else's wedding photos to GitHub.
2026-06-07 19:50:29 -03:00
postgres-backup:
image: prodrigestivill/postgres-backup-local:16-alpine
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
POSTGRES_HOST: postgres
POSTGRES_DB: ${POSTGRES_DB:-wedding}
POSTGRES_USER: ${POSTGRES_USER:-wedding}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_EXTRA_OPTS: "--clean --if-exists"
SCHEDULE: ${BACKUP_SCHEDULE_DB:-@daily}
BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-14}
BACKUP_KEEP_WEEKS: ${BACKUP_KEEP_WEEKS:-4}
BACKUP_KEEP_MONTHS: ${BACKUP_KEEP_MONTHS:-6}
TZ: ${TZ:-America/Sao_Paulo}
volumes:
- ./backups/postgres:/backups
media-backup:
image: alpine:3.20
restart: unless-stopped
depends_on:
minio:
condition: service_healthy
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
S3_BUCKET: ${S3_BUCKET:-wedding-media}
BACKUP_SCHEDULE_MEDIA: ${BACKUP_SCHEDULE_MEDIA:-0 3 * * *}
BACKUP_REMOTE_ENDPOINT: ${BACKUP_REMOTE_ENDPOINT:-}
BACKUP_REMOTE_ACCESS_KEY: ${BACKUP_REMOTE_ACCESS_KEY:-}
BACKUP_REMOTE_SECRET_KEY: ${BACKUP_REMOTE_SECRET_KEY:-}
BACKUP_REMOTE_BUCKET: ${BACKUP_REMOTE_BUCKET:-}
TZ: ${TZ:-America/Sao_Paulo}
volumes:
- ./backups/media:/backups
- ./infra/backup:/scripts:ro
entrypoint: ["/bin/sh", "/scripts/media-backup-entrypoint.sh"]
feat: full Docker stack on a VPS with improved admin editing Self-hosted rewrite of the wedding photo app: Node + Hono replaces Cloudflare Workers, Postgres 16 replaces D1, MinIO replaces R2, Caddy fronts the stack with automatic Let's Encrypt TLS. Same routes and feature set as before; storage abstraction is the same S3 client so MinIO drops in without code changes. Architecture: - docker-compose.yml: postgres, minio, minio-init (creates bucket + anonymous read + CORS), app, caddy (reverse proxy + media subdomain). - Dockerfile: multi-stage pnpm build, single runtime image serving the API and the SPA dist as static assets from one process. - Caddyfile: primary domain proxies to app; media subdomain proxies to MinIO so guests upload directly and signatures match Host. - app: tsx runtime, runs SQL migrations idempotently at startup via a schema_migrations(name, sha256, applied_at) table. Admin upgrades requested: - PATCH /api/admin/uploads/:id to edit author/message. - POST /api/admin/uploads/bulk for bulk approve/reject/delete. - POST /api/admin/uploads/:id/cover and DELETE /api/admin/event/cover to set/clear a featured image (rendered on Home when set). - GET /api/admin/uploads gains ?q= text search across author and message and ?kind=photo|video filter. - Dashboard: bulk select checkboxes with a toolbar, edit modal that rewrites author and message, search input, kind filter, set-cover button per item, cover preview + clear in the event card. Singletons replace the per-request bindings pattern: initDb() and initStorage() run once in server.ts; routes call getDb()/getStorage() directly rather than threading env.DB / env.MEDIA through. env.ts uses zod to parse process.env and fails fast if anything mandatory is missing. .env.example documents every variable and flags the hairpin tradeoff for MinIO access from the app container.
2026-06-07 19:11:10 -03:00
volumes:
postgres_data:
minio_data:
caddy_data:
caddy_config: