Prepare production and Coolify deployment with original exports and datetime pickers

This commit is contained in:
2026-09-09 17:59:20 -04:00
parent 574f29a68e
commit 64d7acc9c0
38 changed files with 1296 additions and 8 deletions
+105
View File
@@ -0,0 +1,105 @@
# Production deployment
## Coolify on one VM
Use the repository Docker Compose build pack and `/compose.coolify.yml`.
This adds isolated Postgres and Garage volumes, migrations, and a one-shot
storage initializer (exact-origin CORS, one-day export expiry and abandoned
multipart cleanup). It does not import local demo data or create accounts.
Coolify generates `SERVICE_URL_WEB` and `SERVICE_URL_GARAGE`; these are passed
to the app/auth and browser storage endpoint respectively. Route web to port
3000 and Garage to 3900. Both generated domains must use HTTPS. Only these
services are public; do not expose Postgres or Garage's admin/RPC ports.
Set `EMAIL_FROM`, `RESEND_API_KEY`, and `RESEND_WEBHOOK_SECRET` in Coolify before
starting. Generated credentials belong to this stack, not other applications.
Keep worker concurrency at one initially on a shared VM. Back up all three
data volumes off-host and verify recovery before collecting real wedding photos.
## Standalone Compose
Requires Docker Compose, an HTTPS reverse proxy, and a private S3-compatible
bucket. Compose includes persistent Postgres, a one-shot migration service,
the standalone web app, and a persistent Bun image/email/export worker.
Mailpit and demo credentials are not included. Production never auto-creates
the bucket or changes its CORS configuration.
1. Copy `.env.production.example` to `.env.production` and fill every required
secret. Use random values, not development credentials. Keep auth and public
URLs identical to the final HTTPS origin. The public URL is also a build arg;
rebuild the web image when changing it.
2. Provision the private bucket and configure CORS: allow your exact public app
origin, GET/PUT/HEAD, Content-Type and necessary S3 checksum headers; expose
ETag and Content-Length. Guests must reach `S3_PUBLIC_ENDPOINT` over HTTPS.
The worker needs list/get/put/delete and multipart-upload permissions.
Set a one-day lifecycle expiry for the `exports/` prefix and abort incomplete
multipart uploads after one day. Do not expire originals. The worker also
removes expired ZIPs, but lifecycle handles archives belonging to deleted events.
3. Verify the Resend sender domain. Configure `/api/webhooks/resend` as described
in `email-delivery.md`. Set Authentik's callback if using it (see README).
4. Run:
```sh
docker compose --env-file .env.production -f compose.production.yml build
docker compose --env-file .env.production -f compose.production.yml up -d
docker compose --env-file .env.production -f compose.production.yml ps
```
The web service binds only `127.0.0.1:3000`. Configure your host reverse proxy to
forward HTTPS to that address, preserve Host and X-Forwarded-Proto, and support
normal streaming responses. Postgres is not published to the host. If your proxy
runs in Docker, attach it to the Compose network and use `web:3000` instead.
## First administrator
Register your real account in the app and verify its email (or sign in through
Authentik). Obtain its ID from your database administrator, then run:
```sh
docker compose --env-file .env.production -f compose.production.yml exec worker bun packages/database/src/bootstrap-admin.ts USER_ID
```
This promotes only an existing verified account and refuses once any platform
administrator exists. No passwords are created or logged. Never run demo seeds.
## Updates and operations
Back up Postgres and the original-image bucket before updates. Build updated
images, then run `docker compose ... run --rm migrate` before `up -d` (replace
`...` with the same env-file/file arguments above). Do not use `down -v` on a
live installation. Schema rollback requires a tested backup/restore strategy.
Monitor web health at `/api/health` (process liveness only), worker logs, failed
jobs, disk usage, database backups and storage lifecycle rules. Test restores.
Pin image digests in your deployment pipeline after verifying them.
## Originals exports
Event → Photos → Export originals. Owners/managers with settings and private-photo
access can choose approved or all processed photos. Exports are requester-only;
permissions are rechecked before issuing each five-minute signed download URL.
Already-issued URLs remain usable until expiry. ZIP files contain unchanged
original bytes (including EXIF/GPS), named by photo ID to prevent unsafe or
duplicate filenames. Standalone banners and unprocessed uploads are excluded.
The selection is taken when processing starts. No originals pass through Next.js.
ZIP64, streaming multipart uploads and one-at-a-time input reads bound memory.
One active export per requester/event is allowed; limit 10,000 photos / 20 GB.
Archives expire after 24 hours; interrupted jobs fail and may be requested again.
## Verification
Validated locally: web/worker/migration Docker image builds (Linux ARM64), web
container startup and health response, worker native Sharp and ZIP dependencies,
Compose configuration, clean database migrations, typecheck and application build.
The demo's eight exported originals were compared byte-for-byte successfully.
No production services were deployed and no production emails were sent.
```sh
EXPORT_INTEGRATION=1 bun --env-file=.env test apps/worker/src/exports.integration.test.ts
cd apps/web
EXPORT_PERMISSIONS_INTEGRATION=1 bun --env-file=../../.env test src/server/exports.integration.test.ts
```
The local-only integration tests cover archive contents, original bytes,
approved/private filtering, expiry deletion, duplicate jobs, event/requester
scoping, denied roles, revoked permissions, and expired download rejection.
They remove their own temporary fixtures. Validate your actual S3 provider and
target architecture in staging before launch.