Infra-only local stack
docker/dev runs Postgres, Redis, and Mailpit. Apps (@frame/next, @frame/nest, @frame/astro) run on the host with pnpm -F @frame/<app> dev for fast reload and debugging.
The FRAME monorepo uses Docker Compose for shared infrastructure and multi-stage images for production-like app deploys. Local development runs apps on the host against infra containers under docker/dev.
Infra-only local stack
docker/dev runs Postgres, Redis, and Mailpit. Apps (@frame/next, @frame/nest, @frame/astro) run on the host with pnpm -F @frame/<app> dev for fast reload and debugging.
Production Security
Production app containers run as non-root (app or nginx-unprivileged). Postgres and
Redis ports are not published to the host by default. Prod Redis requires
REDIS_PASSWORD (--requirepass). Set strong secrets in docker/prod/.env before
up.
Efficiency and Caching
We use multi-stage builds and BuildKit cache mounts (--mount=type=cache) to reuse the
pnpm store across consecutive builds. Dockerfiles pin Node 24.18.0 and
pnpm@11.18.0 to match the root workspace policy. Per-app Dockerfiles live under
apps/*/docker/prod/ and packages/models/docker/prod/.
| Service | Environment | Local Port | Technology / Server |
|---|---|---|---|
| Next.js App | Host / Prod | 4100 | Node.js (host dev / Standalone in Prod) |
| NestJS API | Host / Prod | 4200 | Node.js (host maps to container 3000) |
| Astro App | Host / Prod | 4300 | Astro Dev (host) / nginx-unprivileged on 8080 (Prod) |
| Postgres Database | Dev / Prod | 5432 (dev) | Postgres Alpine (frame-postgres-dev) |
| Redis | Dev / Prod | 6379 (dev) | Redis Alpine (frame-redis-dev) |
| Mailpit Server | Dev | 8025 / 1025 | Mailpit UI / SMTP (frame-mailpit-dev) |
Prod Compose does not publish Postgres or Redis ports by default.
Start shared infra with Compose, then run applications on the host.
frame-postgres-dev: Postgres database server (Port 5432).frame-redis-dev: Redis for BullMQ and related queues (Port 6379, no password).frame-mailpit-dev: Local SMTP capture and web UI (Ports 1025 / 8025).Copy docker/dev/.env.example to docker/dev/.env only if you need to override Postgres credentials. Host apps use localhost in their own .env files (see apps/nest/.env.example and packages/models/.env.example).
Postgres 18+ images expect the data volume at /var/lib/postgresql (not /var/lib/postgresql/data). After upgrading Compose, if the database looks empty or fails to start, recreate the volume and re-migrate:
pnpm docker:dev downdocker volume rm frame-dev_postgres_datapnpm docker:dev up -dpnpm -F @frame/models db:migratepnpm docker:dev up -dRun an application on the host:
pnpm -F @frame/nest devpnpm -F @frame/next dev# or: pnpm -F @frame/astro devStop the development stack:
pnpm docker:dev downThe browser uses the Next.js origin for API requests. Set
PUBLIC_API_URL=http://localhost:4100 and
INTERNAL_API_URL=http://localhost:4200. The Next.js proxy.ts entry rewrites
/api and /uploads requests to Nest while preserving the path, query string, HTTP
method, headers, cookies, and browser-visible URL.
The production environment is optimized for small images, fast response times, and secure defaults. Apps are optional Compose profiles (next, nest, astro). Infrastructure is attached only to the profiles that need it, so the Astro profile does not start Postgres, Redis, or the migrator.
Copy docker/prod/.env.example to docker/prod/.env. This file is the complete runtime baseline for the Next, Nest, and migrator containers: set values directly when operators should control them, or leave provider-managed application values blank so the entrypoint can inject them. Infrastructure values such as POSTGRES_PASSWORD and REDIS_PASSWORD must still be available to Compose itself. REDIS_URL must include the same URL-encoded password as REDIS_PASSWORD.
docker/prod/docker-compose.yml builds images from the local workspace. docker/prod/docker-compose.deploy.yml uses the published frame-next, frame-nest, frame-astro, and frame-migrator GHCR images while preserving the same runtime environment, provider injection, database dependencies, and persistent volumes.
standalone mode. The server serializes only public config keys supported by the client schema into the document before hydration. These values come from the container environment rather than image build arguments; recreate the container after changing them, and never add secrets to the public schema. The locale root waits for a request so server-rendered output and browser configuration agree, which makes Next routes request-rendered instead of fully static. The public Turnstile site key defaults to Cloudflare’s always-pass test key when omitted.nginxinc/nginx-unprivileged on container port 8080.pnpm deploy of @frame/models; it runs migrations followed by atomic, convergent production catalog seeds through the runtime tsx dependency as a non-root user with CI=true.package.json manifests are copied before pnpm install --frozen-lockfile (apps/*, packages/*, configs/*). FRAME also copies create/ and docs/ for its own workspace; create-frame removes those COPY lines in scaffolded projects..env* files are excluded from Docker build contexts. The x-runtime-env service anchor makes Next, Nest, and migrator load the complete docker/prod/.env; their explicit environment blocks contain only container invariants such as HOST, PORT, and provider base paths. App entrypoints discard blank optional values so fetched secrets and schema defaults can load. Astro’s Sentry upload token is a BuildKit secret and is not stored in an image layer.127.0.0.1. Nest Docker HEALTHCHECK uses /health/live (process up). Use /health/ready for DB readiness and /health for full diagnostics.Dokploy’s existing Traefik instance is the production ingress. Do not add another reverse proxy to the Compose stack. Configure two domains for the same public host in the Docker Compose service’s Domains tab:
| Path | Service | Container port | Strip Path | Internal Path |
|---|---|---|---|---|
/api | nest | 4200 | Disabled | Empty |
/uploads | nest | 4200 | Disabled | Empty |
/ | next | 4100 | Disabled | Empty |
Keep Strip Path disabled because Nest expects the /api prefix. Set
PUBLIC_API_URL, NEST_APP_URL, NEST_FRONTEND_URL, and NEST_CORS_ORIGINS to the
same public HTTPS origin. Keep INTERNAL_API_URL=http://nest:4200 so Next.js server
requests remain inside the Compose network.
Traefik normally sends /api and /uploads directly to Nest. The same Next.js
proxy.ts rewrites remain enabled in production as routing fallbacks when either path
reaches Next. These fallbacks do not replace Traefik or provide ingress availability
if Traefik itself is down.
Both local and s3 are supported production storage providers. The local provider
stores data in the nest_uploads volume mounted at /app/storage; Nest publishes its
immutable assets at /uploads. The volume remains attached when the Nest container is
recreated.
Local storage supports a single Docker host. Multiple Nest replicas must share the same
read-write filesystem; use S3 when the deployment spans hosts or does not provide a
shared volume. Include the nest_uploads volume in the deployment’s regular backup and
restore procedure.
Dokploy generates the Traefik labels and ingress network during deployment. Domain changes for Docker Compose services require a redeployment. See the Dokploy Docker Compose domains documentation.
pnpm docker:prod:all buildStart the production stack in the background:
pnpm docker:prod:all up -dCheck stack status:
docker ps --filter "name=frame"