Docker Deployment
Status: Complete
Full reference for the Docker Compose setup, service configuration, and customization options.
docker-compose.yml Reference
Chatalot ships with a docker-compose.yml that defines three services:
Services
chatalot (Application Server)
The main service. Builds from the multi-stage Dockerfile which:
- Stage 1: Compiles the Rust server binary (with dependency caching)
- Stage 2: Builds the WASM crypto module using
wasm-pack - Stage 3: Builds the Svelte web client
- Stage 4: Creates a minimal Debian runtime image with the binary, static files, and migrations
chatalot:
build: .
container_name: chatalot-server
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgres://chatalot:${DB_PASSWORD}@postgres:5432/chatalot
JWT_PRIVATE_KEY_PATH: /run/secrets/jwt_private_key
JWT_PUBLIC_KEY_PATH: /run/secrets/jwt_public_key
TOTP_ENCRYPTION_KEY: ${TOTP_ENCRYPTION_KEY:-}
RUST_LOG: "${RUST_LOG:-chatalot_server=info,tower_http=info}"
FILE_STORAGE_PATH: /app/data/files
MAX_FILE_SIZE_MB: "100"
LISTEN_ADDR: "0.0.0.0:8080"
STATIC_FILES_PATH: /app/static
REGISTRATION_MODE: ${REGISTRATION_MODE:-invite_only}
ADMIN_USERNAME: ${ADMIN_USERNAME:-}
PUBLIC_URL: ${PUBLIC_URL:-}
COMMUNITY_CREATION_MODE: ${COMMUNITY_CREATION_MODE:-admin_only}
GIPHY_API_KEY: ${GIPHY_API_KEY:-}
volumes:
- file_storage:/app/data/files
secrets:
- jwt_private_key
- jwt_public_key
ports:
- "8080:8080"
Key details:
- Waits for PostgreSQL using the depends_on health check condition
- JWT keys are mounted as Docker secrets (read-only files at /run/secrets/)
- File uploads are persisted in a named Docker volume
- Port 8080 is exposed to the host
postgres (Database)
postgres:
image: postgres:17
container_name: chatalot-db
restart: unless-stopped
environment:
POSTGRES_DB: chatalot
POSTGRES_USER: chatalot
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U chatalot"]
interval: 10s
timeout: 5s
retries: 5
Key details:
- Not exposed to the host network by default (only accessible within the chatalot-net Docker network)
- Health check ensures the database is ready before the application starts
- Data is persisted in a named Docker volume
coturn (TURN Server)
An optional coturn TURN relay server for voice and video calls. TURN relays media traffic for users who cannot establish direct peer-to-peer connections (typically due to restrictive firewalls or symmetric NATs).
coturn:
image: coturn/coturn:latest
container_name: chatalot-turn
network_mode: host
command: >
-n
--listening-port=${TURN_LISTENING_PORT:-3478}
--use-auth-secret
--static-auth-secret=${TURN_AUTH_SECRET}
--realm=chatalot
--external-ip=${TURN_EXTERNAL_IP:-0.0.0.0}
...
profiles:
- turn
Key details:
- Not started by default -- enable with docker compose --profile turn up -d
- Host networking -- uses network_mode: host so it can relay UDP traffic without port mapping overhead
- Ephemeral credentials (since v0.24.0) -- coturn runs in --use-auth-secret mode; there are no static TURN users or passwords. The chatalot server mints short-lived HMAC credentials per call. (TURN_USER/TURN_PASSWORD are no longer used.)
- Requires TURN_AUTH_SECRET -- shared HMAC secret; coturn refuses to start without it. Set the same value on the chatalot server, plus TURN_URLS (so clients are told the relay address) and TURN_EXTERNAL_IP (your public IP)
- Security hardened -- denies relay to private IP ranges, limits bandwidth per session
- Firewall ports -- requires 3478/tcp+udp (signaling) and 49152-49200/udp (media relay)
See Configuration > Voice & Video for the full setup guide.
cloudflared (Cloudflare Tunnel)
Two tunnel services are defined behind Docker Compose profiles (they do not start by default):
# Named tunnel (persistent domain, requires token)
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run
environment:
TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN}
profiles:
- production
# Quick tunnel (temporary public URL, no token needed)
cloudflared-quick:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate --url http://chatalot:8080
profiles:
- quick-tunnel
See Cloudflare Tunnel for details.
Volumes
| Volume | Mount Point | Purpose |
|---|---|---|
postgres_data |
/var/lib/postgresql/data |
PostgreSQL data files |
file_storage |
/app/data/files |
Uploaded files |
Secrets
| Secret | Source File | Container Path |
|---|---|---|
jwt_private_key |
./secrets/jwt_private.pem |
/run/secrets/jwt_private_key |
jwt_public_key |
./secrets/jwt_public.pem |
/run/secrets/jwt_public_key |
Network
All services share a bridge network called chatalot-net. This allows the application to reach PostgreSQL via the hostname postgres without exposing the database to the host.
Customization with docker-compose.override.yml
Docker Compose automatically merges docker-compose.override.yml (if present) with the main file. Use this for local customizations that should not be committed to git.
Change the Host Port
Expose PostgreSQL to the Host
Warning: Do not expose the database port on a public-facing server.
Use a Pre-Built Image
Instead of building from source:
# docker-compose.override.yml
services:
chatalot:
image: registry.seglamater.app/seglamater/chatalot:latest
build: !reset null
Add Traefik Labels
# docker-compose.override.yml
services:
chatalot:
labels:
- "traefik.enable=true"
- "traefik.docker.network=web"
- "traefik.http.routers.chatalot.rule=Host(`chat.example.com`)"
- "traefik.http.routers.chatalot.entrypoints=websecure"
- "traefik.http.routers.chatalot.tls=true"
- "traefik.http.services.chatalot.loadbalancer.server.port=8080"
networks:
- web
networks:
web:
external: true
Use an External PostgreSQL
To use an existing PostgreSQL server instead of the bundled container:
# docker-compose.override.yml
services:
chatalot:
depends_on: [] # Remove the postgres dependency
postgres:
profiles:
- disabled # Prevent postgres from starting
Then update DATABASE_URL in .env to point to your external database.
Building
First Build
The first build takes 5-10 minutes due to Rust compilation. Docker cache mounts are used to speed up subsequent builds -- only changed source files trigger recompilation.
Rebuild After Code Changes
Build Without Starting
Clean Build (No Cache)
Container Health Check
The Chatalot container includes a built-in health check:
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD curl -f http://localhost:8080/api/health || exit 1
Check the health status:
Container User
The application runs as a non-root user (chatalot:chatalot) inside the container for security. The Dockerfile creates this user and sets appropriate file ownership.
Common Commands
# Start all services
docker compose up -d
# Start with Cloudflare Tunnel
docker compose --profile production up -d
# Start with Quick Tunnel
docker compose --profile quick-tunnel up -d
# Stop all services
docker compose down
# Stop and remove volumes (DESTRUCTIVE -- deletes all data)
docker compose down -v
# View logs
docker compose logs -f chatalot
docker compose logs -f postgres
# Restart a single service
docker compose restart chatalot
# View resource usage
docker stats chatalot-server chatalot-db
# Execute a command in the running container
docker exec -it chatalot-server sh
docker exec -it chatalot-db psql -U chatalot
Next Step
To build and run without Docker, see Manual Deployment.