Skip to content

Docker & Coolify

Build the Docker image

Build context is Directwerk/ (the directory containing Dockerfile).

sh
cd Directwerk
docker build -t directwerk:local .

The Dockerfile:

  1. Build stage./gradlew :directwerk-app:bootJar -x test on JDK 21 Alpine
  2. Runtime stage — JRE 21 Alpine, non-root user, port 8080, health check /actuator/health (90s start period)

Full stack in Docker Compose

Run Postgres + Mailpit + Directwerk with the stack profile:

sh
cd Directwerk
cp .env.example .env
docker compose --profile stack up --build

Stop (keeps Postgres data):

sh
docker compose --profile stack down
ServiceHost access
directwerkhttp://localhost:8080
postgreslocalhost:5433
mailpit UIhttp://127.0.0.1:8025

Minimum .env for the stack profile:

ini
SPRING_DATASOURCE_PASSWORD=your-local-db-password
DIRECTWERK_PLATFORM_CLIENT_SECRET=your-platform-client-secret
DIRECTWERK_TENANT_CLIENT_SECRET=your-tenant-client-secret
DIRECTWERK_EMAIL_FROM=noreply@directwerk.local
DIRECTWERK_EMAIL_STUDIO_BASE_URL=http://localhost:3004
DIRECTWERK_EMAIL_ADMIN_BASE_URL=http://localhost:3001
DIRECTWERK_DEV_SEED_PASSWORD=ChangeMe-Dev-Seed!
DIRECTWERK_DEV_PLATFORM_ADMIN_PASSWORD=ChangeMe-Dev-Seed!

Production deployment

Production uses the same Docker image with SPRING_PROFILES_ACTIVE=prod and external managed PostgreSQL. Do not run Compose Postgres or Mailpit in production.

Required environment variables (prod)

VariableRequiredNotes
SPRING_PROFILES_ACTIVEyesprod or stage
SPRING_DATASOURCE_URLyesManaged Postgres JDBC URL
SPRING_DATASOURCE_USERNAME / PASSWORDyesDatabase credentials
DIRECTWERK_ISSUERyes (prod)Public HTTPS issuer URL
DIRECTWERK_PLATFORM_CLIENT_SECRETyesOAuth2 platform client secret
DIRECTWERK_TENANT_CLIENT_SECRETyesOAuth2 tenant client secret
DIRECTWERK_JWT_PRIVATE_KEY / PUBLIC_KEYyes (prod)RSA PEM keys
DIRECTWERK_STORAGE_ENABLEDyes (prod)Must be true
DIRECTWERK_STORAGE_BUCKETyes (prod)Object-storage bucket
DIRECTWERK_STORAGE_PUBLIC_CDN_BASE_URLyes (prod)HTTPS public CDN URL

Email and SMTP variables are required when email is enabled. Full list: Environment variables.

Must stay false in prod: DIRECTWERK_ACCOUNT_EXPOSE_DEV_TOKENS.

Deploying with Coolify / Hetzner

  1. Create a Dockerfile application in Coolify.
  2. Set build context to Directwerk.
  3. Set Dockerfile to Dockerfile.
  4. Configure environment variables as Coolify secrets.
  5. Attach managed PostgreSQL 18+.
  6. Expose port 8080; terminate TLS at the reverse proxy.
  7. Set DIRECTWERK_FORWARD_HEADERS_STRATEGY=framework when the proxy overwrites Host.
  8. Health check: /actuator/health (allow ~90s on cold start with migrations).
  9. Scale horizontally as needed — set distinct DIRECTWERK_QUEUE_WORKER_ID per instance.

Flyway runs automatically on startup.

Deploying Next.js apps and docs (Coolify)

Each frontend has its own Dockerfile. Build context must always be the monorepo root (.) — apps depend on workspace packages (@directwerk/ui, @directwerk/api) and the root lockfile. Do not use Nixpacks with a subdirectory base directory.

AppDockerfile pathPortHealth check
directwerk-admindirectwerk-admin/Dockerfile3001/api/health
directwerk-studiodirectwerk-studio/Dockerfile3003/api/health
directwerk-webdirectwerk-web/Dockerfile3004/api/health
homepagehomepage/Dockerfile3005/api/health
directwerk-docsdirectwerk-docs/Dockerfile3006/

Coolify settings for each app:

SettingValue
Build packDockerfile (not Nixpacks)
Base DirectoryRepository root (. — leave empty)
Dockerfile pathSee table above
PortMust match the table — docs is 3006, not 4173 or 3001
Health check pathSee table above

The Dockerfiles accept either a full monorepo build context or Coolify's app-scoped context (app sources at the context root with workspace files injected). If the build fails with missing packages/ or pnpm-workspace.yaml, the Base Directory is set too deep — move it to the repository root.

Example local build:

sh
docker build -f directwerk-admin/Dockerfile -t directwerk-admin:local .

Required runtime env vars for directwerk-admin: DIRECTWERK_API_URL, OAUTH_CLIENT_ID (directwerk-platform-admin), OAUTH_CLIENT_SECRET, and optionally tenant OAuth vars for the dual-session tenant products UI. See docker-compose.full-stack.yaml for a full example.

directwerk-studio (and directwerk-web) also need tenant OAuth vars (OAUTH_CLIENT_ID / OAUTH_CLIENT_SECRET = directwerk-tenant-frontend credentials).

Shared directwerk-studio (studio.example.com): no tenant env vars. Users sign in with email/password; the app discovers workspaces via POST /api/v1/auth/studio/workspaces and stores the chosen tenant in a session cookie. Tenant domains are resolved at runtime (verified site-config Host lookup), not from deploy configuration.

For server-side API calls from containers on the same Docker network, prefer an internal HTTP URL for DIRECTWERK_API_URL (e.g. http://directwerk:8080). If the API is only reachable via HTTPS with a self-signed certificate, set DIRECTWERK_UPSTREAM_TLS_INSECURE=true on the frontend container (staging only — prefer valid TLS or internal HTTP in production).

Troubleshooting

directwerk-docs Bad Gateway (502)

The container is usually healthy (logs show GET / 200), but Coolify’s proxy is hitting the wrong port. Docs listens on PORT (default 3006), same as the other frontends.

  1. General → Ports Exposes: 3006
  2. Environment: delete a stray PORT=4173 if present; leave unset so the Dockerfile default applies
  3. Health check: /
  4. Redeploy after changes

Runtime logs should show VitePress preview serving / with ~10 KB responses, not nginx’s 615 byte default page.

App exits immediately in prod

Check logs for ProdSecurityPropertiesValidator, ProdEmailPropertiesValidator, or ProdStoragePropertiesValidator.

Flyway migration failure

Ensure Postgres is reachable and the DB user can create tables.

Health check failing

Cold databases can take up to ~90 seconds during first startup with migrations.

Build fails in Docker

Build context must be Directwerk (not the monorepo root for the API image).

pnpm install --frozen-lockfile --prefer-offline fails (Next.js apps)

Coolify is using Nixpacks or Railpack (auto-detect), not the app Dockerfile. The log shows pnpm install --frozen-lockfile --prefer-offline and/or corepack errors such as Cannot find module '/opt/corepack/v1/pnpm/12.0.0-rc.5/bin/pnpm.mjs'.

In the Coolify app → ConfigurationGeneralBuild Pack, select Dockerfile (not Nixpacks, not Railpack). Match the working directwerk-admin app:

Settingdirectwerk-studio example
Build PackDockerfile
Base Directory. (repository root — leave empty)
Dockerfiledirectwerk-studio/Dockerfile
Port3003

Redeploy after saving. If a railpack.toml was generated on the server, remove it when switching to Dockerfile.

pnpm install --frozen-lockfile --prefer-offline fails (wrong base directory)

Coolify is using Nixpacks with base directory set to an app subfolder (e.g. directwerk-admin/). Switch to Dockerfile build with monorepo root context and the app-specific Dockerfile — see Deploying Next.js apps and docs above.

Directwerk — whitelabel publication infrastructure