Running Titan services with Docker (dev)

Inventory, env-file conventions, and from-scratch instructions. Written 2026-07 after auditing the running containers against the repo; the compose files were recovered from commit 09ce283fb8 and now fully define every service.

Service inventory

Stack Compose file Services (host ports)
titan core docker-compose.yaml rails :3000, webpack :3035, sidekiq, postgres :5432, redis :6379, mailhog :1025/:8025, billing :4242
bot svc/bot/docker-compose.yml bot :8000, pgvector :5434, langfuse :3030, langfuse-db
typebot svc/typebot/docker-compose.dev.yml main :8082–8084, typebot-db :5433, minio :9000/:9001, createbuckets
notea svc/notea/docker-compose.yml notea :3001, minio :9002

Images are built from docker/dockerfiles/*.Dockerfile with context: svc/<name> (same pattern as .github/workflows/ci.yaml), except typebot main (svc/typebot/typebot.Dockerfile) and notea (docker/dockerfiles/notea.dev.Dockerfile).

Env files

Each stack reads .env in its own directory (gitignored). Two supporting file kinds live next to it:

  • .env.example.docker (committed) — full key set with Docker-network hostnames filled in and secrets blanked (# FILL markers). Start here:

    cp .env.example.docker .env   # then fill every line marked "# FILL"
    
  • .env.docker (gitignored — contains real secrets) — faithful export of the running container’s env, taken 2026-07 via docker inspect with image-baked vars filtered out. Use as the reference for a working setup. Backups of the pre-audit files are kept as .env.bak.*.

Both exist for: repo root (rails/sidekiq/webpack), svc/bot/, svc/billing/, svc/typebot/, svc/notea/.

Rules:

  • One KEY=VALUE per line. No line continuations, no export, no CRLF.
  • Hostnames resolve by container name across the shared titan_network: titan_rails_1, titan_postgres_1, titan_redis_1, bot_bot_1, typebot-main-1. Keep container_name: values in compose files stable or these references break.
  • Secrets come from the team vault or your .env.bak.* / .env.docker files.

Known quirks:

  • typebot: DATABASE_URL and NEXT_PUBLIC_*_BASEPATH are baked into the typebot-main image (Dockerfile ENV); keep them in .env anyway for rebuilds.
  • typebot’s BOT_OUTGOING_URL must use bot_bot_1 (the old titan-bot hostname resolves nowhere — fixed in the env files 2026-07, but the long-running container still has the broken value until recreated).
  • billing’s REDIS_HOST must be titan_redis_1 (underscores). The dash spelling in the old container never resolved — likely why billing sat exited.

Networks

Network Declared in Purpose
titan_network root compose (owner); external in bot/notea/typebot Shared cross-service DNS by container name
bot_network svc/bot (owner) bot ↔ pgvector ↔ langfuse
typebot_network svc/typebot (owner) main ↔ typebot-db ↔ minio
notea_internal svc/notea (owner) notea ↔ its minio

Start the titan core stack first (it creates titan_network), or run docker network create titan_network manually.

Leftover networks on the long-running containers are redundant and can be cleaned up: bot_bot_network, bot_default, typebot_default, titan_default (dead — members already share titan_network), plus rails/sidekiq memberships in bot_network/typebot_network and bot’s membership in typebot_network. Use docker network disconnect <net> <container>, then docker network rm.

Running from scratch

One-time:

cp .env.example.docker .env    # in root and each svc dir; fill FILL lines

Boot order:

  1. titan core (repo root): docker-compose up -d postgres redis mailhog, then rails (its entrypoint runs db prepare/migrations), then sidekiq, webpack, billing.
  2. bot (svc/bot): needs pgvector (:5434) and rails reachable; docker-compose up -d.
  3. typebot (svc/typebot): docker-compose -f docker-compose.dev.yml up -d — db (:5433) + minio first; createbuckets seeds the bucket; then main.
  4. notea (svc/notea): minio (:9002) then notea.

Gotchas:

  • Port map (avoid collisions): 3000 rails · 3035 webpack · 5432 pg · 5433 typebot-db · 5434 pgvector · 6379 redis · 1025/8025 mailhog · 4242 billing · 8000 bot · 3030 langfuse · 8082–8084 typebot · 9000/9001 typebot-minio · 9002 notea-minio · 3001 notea.
  • REDIS_PASSWORD is read by the redis service’s command from the root .env (env_file: .env) — an empty value silently starts redis unauthenticated.
  • postgres is pinned to postgres:14 to match the existing data volume; never point a different major version at it.
  • Old containers hold the fixed container_names: docker compose down (per project) before up with recreated definitions, otherwise name conflicts.
  • The 2-month-old containers predate these files; recreating them picks up the current env files (including the BOT_OUTGOING_URL / REDIS_HOST fixes).

Before committing / pushing the infra files

  • svc/notea/docker-compose.yml hardcodes CHATWOOT_PLATFORM_TOKEN in the notea service environment — move it to svc/notea/.env (value already there) and rotate the token.
  • svc/typebot/typebot.Dockerfile contains a hardcoded SENTRY_AUTH_TOKEN (blank + rotate) and a pnpm-lock.yamle typo that silently skips the lockfile.
  • svc/notea/.env.sample also carries a real-looking CHATWOOT_PLATFORM_TOKEN (committed long ago) — rotate.

Suggested additions (not yet implemented)

  • Healthchecks on postgres/redis + depends_on: condition: service_healthy.
  • restart: unless-stopped on app services (billing sat exited for 2 months unnoticed).
  • Local LangFuse (+db, port 3030) appears unused — bot points at https://cloud.langfuse.com; consider removing the local pair.
  • No compose/env story for svc/call and svc/my (Dockerfiles exist in docker/dockerfiles/, .env.example exists; compose files TODO).