Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

Docker Architecture

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/.


ServiceEnvironmentLocal PortTechnology / Server
Next.js AppHost / Prod4100Node.js (host dev / Standalone in Prod)
NestJS APIHost / Prod4200Node.js (host maps to container 3000)
Astro AppHost / Prod4300Astro Dev (host) / nginx-unprivileged on 8080 (Prod)
Postgres DatabaseDev / Prod5432 (dev)Postgres Alpine (frame-postgres-dev)
RedisDev / Prod6379 (dev)Redis Alpine (frame-redis-dev)
Mailpit ServerDev8025 / 1025Mailpit 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:

Terminal window
pnpm docker:dev down
docker volume rm frame-dev_postgres_data
pnpm docker:dev up -d
pnpm -F @frame/models db:migrate
    1. Start development infra:
    Terminal window
    pnpm docker:dev up -d
    1. Run an application on the host:

      Terminal window
      pnpm -F @frame/nest dev
      pnpm -F @frame/next dev
      # or: pnpm -F @frame/astro dev
    2. Stop the development stack:

      Terminal window
      pnpm docker:dev down

The 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.

  • Next.js Standalone: Uses Next.js 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.
  • Astro Nginx: Static site is served by nginxinc/nginx-unprivileged on container port 8080.
  • Migrator: Production-only image from 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.
  • Scoped installs: Each image installs only its app and transitive workspace dependencies.
  • Unified Cache: Workspace 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.
  • Runtime environment control: Nested .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.
  • Container Health: IPv6-safe probes against 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:

PathServiceContainer portStrip PathInternal Path
/apinest4200DisabledEmpty
/uploadsnest4200DisabledEmpty
/next4100DisabledEmpty

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.

    1. Compile and build production images:
    Terminal window
    pnpm docker:prod:all build
    1. Start the production stack in the background:

      Terminal window
      pnpm docker:prod:all up -d
    2. Check stack status:

      Terminal window
      docker ps --filter "name=frame"