Docker and proxy deployment
The standard deployment runs these services:
| Service | Purpose |
|---|---|
db | Postgres with pgvector. |
backend | FastAPI API, migrations, uploads, AI, and background work. |
email-worker | Database-backed email outbox worker. |
frontend | Authenticated Vite application. |
docs | Customer-facing Docusaurus site, enabled with the docs profile. |
authentik-server, authentik-worker, authentik-db, authentik-redis | Optional authentik identity provider stack, enabled with the authentik profile. |
authentik-bootstrap | Optional one-shot service that creates or updates the authentik OAuth2/OIDC provider and TOW application. |
Start the app with:
docker compose up --build
Run the docs site with:
docker compose --profile docs up --build docs
Default ports
Docker Compose exposes these defaults:
| Variable | Default |
|---|---|
BACKEND_PORT | 8000 |
FRONTEND_PORT | 3000 |
DOCS_PORT | 3001 |
AUTHENTIK_PORT | 9000 |
The frontend opens at http://localhost:3000. The backend health endpoint is available at /api/health. API docs are available at /api/docs on the backend.
Same-origin reverse proxy
For production, a common pattern is one public app origin:
https://tow.example.com
Route:
/api/to the backend./to the frontend.
When the browser and backend share one origin, leave VITE_API_BASE_URL blank. The frontend will call /api on the same origin.
If /api reaches the frontend server first, it can proxy requests to API_PROXY_TARGET. In Docker Compose, the default target is:
http://backend:8000
Example nginx routing
location /api/ {
proxy_pass http://127.0.0.1:8000/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Realtime app updates use Socket.IO at /api/socket.io. Keep that path routed to
the backend with WebSocket upgrade headers; Socket.IO can use HTTP fallback if a
proxy cannot upgrade.
If your deployment uses container names instead of host ports, point proxy_pass at the internal service address used by your platform.
Built-in nginx proxy overlay
TOW includes an opt-in Compose overlay for nginx. It is intended for single-domain deployments such as:
https://app.theonlyworkspace.com
The production overlay reads secrets from .env.launch, reads runtime app settings
from config/tow.launch.yaml, keeps the frontend, backend, and authentik host
ports private, and exposes only nginx on ports 80 and 443.
Start the production stack:
TOW_IMAGE_TAG=v0.1.0 docker compose --env-file .env.launch \
-f docker-compose.yml \
-f docker-compose.production.yml \
-f docker-compose.proxy.yml \
--profile authentik pull
TOW_IMAGE_TAG=v0.1.0 docker compose --env-file .env.launch \
-f docker-compose.yml \
-f docker-compose.production.yml \
-f docker-compose.proxy.yml \
--profile authentik up -d
The production overlay pulls prebuilt launch images from Docker Hub and keeps the frontend, backend, and authentik host ports private.
Docker Hub launch images
For launch, the custom TOW images are published as public Docker Hub repositories
under anistow:
| Image | Used by |
|---|---|
anistow/tow-backend | backend, search-worker, email-worker, migration-worker, and authentik-bootstrap. |
anistow/tow-frontend | frontend. |
anistow/tow-docs | docs. |
Third-party images such as Postgres, pgvector, Meilisearch, nginx, Redis, and authentik are pulled from their upstream registries.
Before publishing, create the three public Docker Hub repositories and sign in:
docker login --username anistow
Preview the publish commands:
scripts/publish-dockerhub.sh --dry-run
Build and push the pinned launch release:
VERSION=v0.1.0 scripts/publish-dockerhub.sh
The script publishes v0.1.0 plus an immutable sha-<git-sha> tag. It does not
publish latest unless PUSH_LATEST=1 is set.
Deploy from Docker Hub with the production overlay:
TOW_IMAGE_TAG=v0.1.0 docker compose --env-file .env.launch \
-f docker-compose.yml \
-f docker-compose.production.yml \
-f docker-compose.proxy.yml \
--profile authentik pull
TOW_IMAGE_TAG=v0.1.0 docker compose --env-file .env.launch \
-f docker-compose.yml \
-f docker-compose.production.yml \
-f docker-compose.proxy.yml \
--profile authentik up -d
Use TOW_BACKEND_IMAGE, TOW_FRONTEND_IMAGE, or TOW_DOCS_IMAGE only when a
deployment needs to override the full image reference for one image.
The generated deployment files are ignored by git:
| File | Purpose |
|---|---|
.env.launch | Secrets, proxy mode, public host, and authentik bootstrap values. |
config/tow.launch.yaml | Non-secret runtime settings for the production deployment. |
TLS modes
Set TLS_MODE in .env.launch:
| Mode | Behavior |
|---|---|
off | nginx serves HTTP only. Use this when an enterprise load balancer or ingress terminates TLS upstream. |
provided | nginx serves HTTPS with enterprise-managed certificate files mounted under TLS_CERT_DIR. |
To enable TLS, place the certificate and key in TLS_CERT_DIR, then set
TLS_MODE=provided:
| Variable | Default container path |
|---|---|
TLS_CERT_PATH | /etc/nginx/certs/fullchain.pem |
TLS_KEY_PATH | /etc/nginx/certs/privkey.pem |
If either file is missing or unreadable, nginx logs the problem and starts in HTTP-only mode.
You can validate rendered nginx config without starting the whole stack:
TLS_MODE=off docker compose --env-file .env.launch \
-f docker-compose.yml \
-f docker-compose.production.yml \
-f docker-compose.proxy.yml \
run --rm --no-deps proxy nginx -t
After placing certificate files under TLS_CERT_DIR, validate the HTTPS config:
TLS_MODE=provided docker compose --env-file .env.launch \
-f docker-compose.yml \
-f docker-compose.production.yml \
-f docker-compose.proxy.yml \
run --rm --no-deps proxy nginx -t
OIDC with authentik
For enterprise installs, TOW can use authentik as the authentication broker. TOW remains an OIDC client only: authentik owns login UI, MFA, LDAP, Active Directory, SAML, and upstream identity federation, while TOW owns organisations, memberships, roles, and permissions.
Before first boot, set TOW to OIDC mode in tow.yaml:
runtime:
public_app_url: https://tow.example.com
auth:
mode: oidc
oidc:
issuer: https://auth.example.com/application/o/tow/
client_id: tow
scopes:
- openid
- profile
- email
Then set secrets and bootstrap values in .env or your secret manager:
PUBLIC_APP_URL=https://tow.example.com
OIDC_CLIENT_SECRET=replace-with-a-long-random-secret
AUTHENTIK_PUBLIC_URL=https://auth.example.com
AUTHENTIK_BOOTSTRAP_EMAIL=admin@example.com
AUTHENTIK_BOOTSTRAP_PASSWORD=replace-with-a-long-random-password
AUTHENTIK_BOOTSTRAP_TOKEN=replace-with-a-long-random-token
AUTHENTIK_SECRET_KEY=replace-with-a-long-random-secret
AUTHENTIK_POSTGRES_PASSWORD=replace-with-a-long-random-password
Start the bundled authentik stack with:
docker compose --profile authentik up --build
The bootstrap service is idempotent. It uses AUTHENTIK_BOOTSTRAP_TOKEN to create or update:
- The authentik OAuth2/OIDC provider for TOW.
- A strict redirect URI of
PUBLIC_APP_URL + /api/auth/oidc/callback. - A strict post-logout redirect URI of
PUBLIC_APP_URL + /login?logged_out=1. - The authentik application that points users back to TOW.
The app-side OIDC issuer should be:
AUTHENTIK_PUBLIC_URL/application/o/tow/
For example, if AUTHENTIK_PUBLIC_URL=https://auth.example.com, set:
auth:
oidc:
issuer: https://auth.example.com/application/o/tow/
If AUTHENTIK_PUBLIC_URL=https://example.com/authentik, set:
auth:
oidc:
issuer: https://example.com/authentik/application/o/tow/
Choose auth.mode before first boot. In OIDC mode, the first successful OIDC login bootstraps the server admin and organisation owner. Later OIDC users need an invite or an existing linked identity.
Authentik on a subdomain
Use this pattern when users should visit authentik at https://auth.example.com.
server {
server_name auth.example.com;
location / {
proxy_pass http://127.0.0.1:9000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Set:
AUTHENTIK_PUBLIC_URL=https://auth.example.com
AUTHENTIK_WEB__PATH=/
Authentik under /authentik
Use this pattern when users should visit authentik at https://example.com/authentik.
location /authentik/ {
proxy_pass http://127.0.0.1:9000/authentik/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Set:
AUTHENTIK_PUBLIC_URL=https://example.com/authentik
AUTHENTIK_WEB__PATH=/authentik/
Keep the TOW app routes separate from the authentik subpath. For example, route /api/ to TOW backend, /authentik/ to authentik, and / to the TOW frontend.
Docs URL
The Docusaurus docs site is separate from the authenticated in-product wiki.
Configure these values in Compose, your build pipeline, or your deployment platform:
| Variable | Purpose |
|---|---|
DOCS_SITE_URL | Public site URL used by the docs build. |
DOCS_BASE_URL | Base path for the docs site. Use / for a docs subdomain. |
VITE_DOCS_URL | URL shown in the app account menu as Documentation. |
Set VITE_DOCS_URL only when the docs site is deployed and reachable by users.
Email delivery
The backend writes email jobs to the database and the email-worker service delivers them.
For local SMTP testing, enable Mailpit and set the email block in tow.yaml to SMTP:
email:
transport: smtp
smtp:
host: mailpit
port: 1025
tls_mode: none
Then run:
docker compose --profile mailpit up --build
Open Mailpit at http://localhost:8025.
For self-hosted production, set email.transport: smtp and point email.smtp.host, email.smtp.port, and email.smtp.tls_mode at your SMTP relay in tow.yaml. Keep only SMTP_USERNAME and SMTP_PASSWORD in .env or your secrets manager. For air-gapped deployments, use email.transport: disabled or email.transport: file.
Persistent data
Docker Compose defines:
tow_postgresfor database data.tow_backend_datafor backend data, including/app/data/tow.yaml, uploads, and onboarding backup output.
Back up both the database volume and backend data volume. Database-only backups do not include uploaded file content stored on disk.
Deployment checklist
Before opening the deployment to users:
- Run migrations through the backend startup command.
- Confirm
/api/healthis healthy through the public proxy. - Confirm cookies work over HTTPS.
- Confirm upload and export actions work.
- Send a test email from Admin, Server Settings.
- Confirm docs URL behavior in the app account menu.
- Confirm the first account was intentionally created by the correct owner.