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.
151 lines
4.4 KiB
YAML
151 lines
4.4 KiB
YAML
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
|
|
|
|
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"]
|
|
|
|
volumes:
|
|
postgres_data:
|
|
minio_data:
|
|
caddy_data:
|
|
caddy_config:
|