Developer · Docker deployment
Developer Docs

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

shell
cp .env.example .env     # every variable also has a safe dev default
docker compose up --build

Then 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:

Apply migrations after first boot
DATABASE_URL=postgresql://pacfully:pacfully@localhost:5432/pacfully npm run db:migrate

How nginx routes

PathUpstreamNotes
/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_data volume.
  • redis — redis:7-alpine, healthchecked, redis_data volume; reserved for queues/caching.
  • minio — S3-compatible object storage, minio_data volume; 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.

VariableDefaultPurpose
NEXT_PUBLIC_API_URLunset → same-origin /apiURL 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 / HOST4000 / 0.0.0.0Where the Fastify api listens.
LOG_LEVELinfoFastify logger verbosity.
JWT_SECRETchange-me-to-a-long-random-stringSigns the 7-day auth tokens. Set a real secret anywhere outside local dev.
CORS_ORIGINhttp://localhost:3000Allowed browser origins (comma-separated). Behind nginx, same-origin requests make this moot.
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DBpacfully / pacfully / pacfullyDatabase credentials; also interpolated into the api DATABASE_URL in compose.
DB_PORT5432Host port publishing Postgres in local-dev mode.
DATABASE_URLpostgresql://pacfully:pacfully@localhost:5432/pacfullyPrisma connection string. Compose overrides it to the internal db hostname.
REDIS_URL / REDIS_PORTredis://localhost:6379 / 6379Reserved for queues and caching.
MINIO_ROOT_USER / MINIO_ROOT_PASSWORDminioadmin / minioadminObject-storage credentials; reused as the api S3 access key pair in compose.
MINIO_CONSOLE_PORT9001Publishes the MinIO web console.
STORAGE_DRIVERminio (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_BUCKEThttp://minio:9000 / us-east-1 / pacfully-filesS3-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_KEYminioadmin / minioadminCredentials the api uses against the S3 endpoint.
HTTP_PORT80The public port nginx binds in the full deployment.

Change the dev defaults before real traffic

The compose file falls back to 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.

Continue to project structure