Appearance
Docker & Coolify
Build the Docker image
Build context is Directwerk/ (the directory containing Dockerfile).
sh
cd Directwerk
docker build -t directwerk:local .The Dockerfile:
- Build stage —
./gradlew :directwerk-app:bootJar -x teston JDK 21 Alpine - 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 --buildStop (keeps Postgres data):
sh
docker compose --profile stack down| Service | Host access |
|---|---|
| directwerk | http://localhost:8080 |
| postgres | localhost:5433 |
| mailpit UI | http://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)
| Variable | Required | Notes |
|---|---|---|
SPRING_PROFILES_ACTIVE | yes | prod or stage |
SPRING_DATASOURCE_URL | yes | Managed Postgres JDBC URL |
SPRING_DATASOURCE_USERNAME / PASSWORD | yes | Database credentials |
DIRECTWERK_ISSUER | yes (prod) | Public HTTPS issuer URL |
DIRECTWERK_PLATFORM_CLIENT_SECRET | yes | OAuth2 platform client secret |
DIRECTWERK_TENANT_CLIENT_SECRET | yes | OAuth2 tenant client secret |
DIRECTWERK_JWT_PRIVATE_KEY / PUBLIC_KEY | yes (prod) | RSA PEM keys |
DIRECTWERK_STORAGE_ENABLED | yes (prod) | Must be true |
DIRECTWERK_STORAGE_BUCKET | yes (prod) | Object-storage bucket |
DIRECTWERK_STORAGE_PUBLIC_CDN_BASE_URL | yes (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
- Create a Dockerfile application in Coolify.
- Set build context to
Directwerk. - Set Dockerfile to
Dockerfile. - Configure environment variables as Coolify secrets.
- Attach managed PostgreSQL 18+.
- Expose port 8080; terminate TLS at the reverse proxy.
- Set
DIRECTWERK_FORWARD_HEADERS_STRATEGY=frameworkwhen the proxy overwritesHost. - Health check:
/actuator/health(allow ~90s on cold start with migrations). - Scale horizontally as needed — set distinct
DIRECTWERK_QUEUE_WORKER_IDper 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.
| App | Dockerfile path | Port | Health check |
|---|---|---|---|
directwerk-admin | directwerk-admin/Dockerfile | 3001 | /api/health |
directwerk-studio | directwerk-studio/Dockerfile | 3003 | /api/health |
directwerk-web | directwerk-web/Dockerfile | 3004 | /api/health |
homepage | homepage/Dockerfile | 3005 | /api/health |
directwerk-docs | directwerk-docs/Dockerfile | 3006 | / |
Coolify settings for each app:
| Setting | Value |
|---|---|
| Build pack | Dockerfile (not Nixpacks) |
| Base Directory | Repository root (. — leave empty) |
| Dockerfile path | See table above |
| Port | Must match the table — docs is 3006, not 4173 or 3001 |
| Health check path | See 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.
- General → Ports Exposes:
3006 - Environment: delete a stray
PORT=4173if present; leave unset so the Dockerfile default applies - Health check:
/ - 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 → Configuration → General → Build Pack, select Dockerfile (not Nixpacks, not Railpack). Match the working directwerk-admin app:
| Setting | directwerk-studio example |
|---|---|
| Build Pack | Dockerfile |
| Base Directory | . (repository root — leave empty) |
| Dockerfile | directwerk-studio/Dockerfile |
| Port | 3003 |
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.
Related
- Local development
- Multi-tenancy — Host header and proxy setup