Skip to main content

Docker and proxy deployment

The standard deployment runs these services:

ServicePurpose
dbPostgres with pgvector.
backendFastAPI API, migrations, uploads, AI, and background work.
email-workerDatabase-backed email outbox worker.
frontendAuthenticated Vite application.
docsCustomer-facing Docusaurus site, enabled with the docs profile.
authentik-server, authentik-worker, authentik-db, authentik-redisOptional authentik identity provider stack, enabled with the authentik profile.
authentik-bootstrapOptional 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:

VariableDefault
BACKEND_PORT8000
FRONTEND_PORT3000
DOCS_PORT3001
AUTHENTIK_PORT9000

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:

ImageUsed by
anistow/tow-backendbackend, search-worker, email-worker, migration-worker, and authentik-bootstrap.
anistow/tow-frontendfrontend.
anistow/tow-docsdocs.

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:

FilePurpose
.env.launchSecrets, proxy mode, public host, and authentik bootstrap values.
config/tow.launch.yamlNon-secret runtime settings for the production deployment.

TLS modes

Set TLS_MODE in .env.launch:

ModeBehavior
offnginx serves HTTP only. Use this when an enterprise load balancer or ingress terminates TLS upstream.
providednginx 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:

VariableDefault 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/
caution

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:

VariablePurpose
DOCS_SITE_URLPublic site URL used by the docs build.
DOCS_BASE_URLBase path for the docs site. Use / for a docs subdomain.
VITE_DOCS_URLURL 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_postgres for database data.
  • tow_backend_data for backend data, including /app/data/tow.yaml, uploads, and onboarding backup output.
warning

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