Developer · Docker deployment
User Guide
Developer
The whole stack, one compose file
docker-compose.yml builds and runs six services — web, api, db, redis, minio, nginx — on a private bridge network with healthchecks and named volumes. nginx is the only public door.
Boot it
cp .env.example .env # every variable also has a safe dev default
docker compose up --buildThen open http://localhost. First boot note: the api container does not run migrations automatically. Apply them once from your host against the published Postgres port:
DATABASE_URL=postgresql://pacfully:pacfully@localhost:5432/pacfully npm run db:migrateHow nginx routes
| Path | Upstream | Notes |
|---|---|---|
| / | web (Next.js) | The product — pages, editor, docs. |
| /api/ | api (Fastify) | The /api prefix is stripped, so /api/v1/templates arrives as /v1/templates. |
| /files/ | minio (S3) | Uploaded files, rewritten to path-style /<S3_BUCKET>/<name>. If the object is not in MinIO (STORAGE_DRIVER=local), nginx falls back to the api's on-disk file server. The MinIO console is published separately on http://localhost:9001 (default minioadmin/minioadmin). |
The browser reaches the api through the same nginx origin: the web client calls the relative path /api whenever NEXT_PUBLIC_API_URL is unset, and the compose stack leaves it unset — no build-time wiring to get wrong.
The six services
- web — Next.js production build (apps/web/Dockerfile), port 3000 internal.
- api — Fastify production build (apps/api/Dockerfile), port 4000 internal; waits for healthy db and redis.
- db — postgres:16-alpine with a pg_isready healthcheck and the
db_datavolume. - redis — redis:7-alpine, healthchecked,
redis_datavolume; reserved for queues/caching. - minio — S3-compatible object storage,
minio_datavolume; only the console port is published. - nginx — nginx:1.27-alpine, the single published HTTP port (default 80).
Environment variable reference
Everything below lives in .env.example with a safe dev default — copy it to .env and adjust. Compose reads .env automatically for interpolation.
| Variable | Default | Purpose |
|---|---|---|
| NEXT_PUBLIC_API_URL | unset → same-origin /api | URL the browser uses for the api. Leave unset: the web client defaults to the relative /api path, which nginx proxies to the api (and the dev server rewrites to localhost:4000). Set only for an api on another origin. |
| PORT / HOST | 4000 / 0.0.0.0 | Where the Fastify api listens. |
| LOG_LEVEL | info | Fastify logger verbosity. |
| JWT_SECRET | change-me-to-a-long-random-string | Signs the 7-day auth tokens. Set a real secret anywhere outside local dev. |
| CORS_ORIGIN | http://localhost:3000 | Allowed browser origins (comma-separated). Behind nginx, same-origin requests make this moot. |
| POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB | pacfully / pacfully / pacfully | Database credentials; also interpolated into the api DATABASE_URL in compose. |
| DB_PORT | 5432 | Host port publishing Postgres in local-dev mode. |
| DATABASE_URL | postgresql://pacfully:pacfully@localhost:5432/pacfully | Prisma connection string. Compose overrides it to the internal db hostname. |
| REDIS_URL / REDIS_PORT | redis://localhost:6379 / 6379 | Reserved for queues and caching. |
| MINIO_ROOT_USER / MINIO_ROOT_PASSWORD | minioadmin / minioadmin | Object-storage credentials; reused as the api S3 access key pair in compose. |
| MINIO_CONSOLE_PORT | 9001 | Publishes the MinIO web console. |
| STORAGE_DRIVER | minio (compose) · local (bare-metal) | Upload storage backend: 'minio'/'s3' writes to the S3 bucket, 'local' writes to UPLOAD_DIR on the api filesystem. |
| S3_ENDPOINT / S3_REGION / S3_BUCKET | http://minio:9000 / us-east-1 / pacfully-files | S3-compatible storage coordinates for the api storage driver. The bucket is auto-created with anonymous read so nginx can serve /files/<name>. |
| S3_ACCESS_KEY / S3_SECRET_KEY | minioadmin / minioadmin | Credentials the api uses against the S3 endpoint. |
| HTTP_PORT | 80 | The public port nginx binds in the full deployment. |
Change the dev defaults before real traffic
dev-only-secret-change-me for JWT signing and minioadmin/minioadmin for object storage if you set nothing. Those defaults exist so a fresh clone boots — they are not for anything you care about.