Docker Compose
Introduction
Section titled “Introduction”This guide walks through deploying Chatto with Docker Compose, in a more fully-featured setup compared to what we set up in the Standalone Binary guide. Specifically, this includes:
- A separate NATS server for Chatto to connect to (instead of using the embedded server.)
- A working LiveKit service to power your voice and video calls.
- A Caddy reverse proxy.
- Configuration through environment variables instead of
chatto.toml.
Consider all of these optional; if you don’t want to use Caddy, feel free to replace it with something else; if you don’t need calls, just leave LiveKit disabled. We do recommend, however, to separate Chatto from NATS at this point because it will make future production use cases (scaling, high availability, zero-downtime deployments) a lot easier.
Architecture
Section titled “Architecture”Prerequisites
Section titled “Prerequisites”- Docker and Docker Compose v2
- A domain pointing to your server (e.g.,
chat.example.com) - A
livekit.*subdomain pointing to the same server (e.g.,livekit.chat.example.com) - Firewall allowing inbound TCP 80/443 and UDP 3478, 50000-50200
- SMTP credentials for email/password registration, email verification, and password reset
-
Copy the Compose example
From an existing Chatto checkout:
Terminal window cp -R examples/dockercompose chattocd chattoFrom GitHub without cloning the full repository:
Terminal window git clone --depth 1 --filter=blob:none --sparse https://github.com/chattocorp/chatto.git chatto-sourcegit -C chatto-source sparse-checkout set examples/dockercomposecp -R chatto-source/examples/dockercompose chattorm -rf chatto-sourcecd chatto -
Create
.envand LiveKit secretsTerminal window ./init-env.sh chat.example.com admin@example.comReplace
chat.example.comwith your Chatto domain andadmin@example.comwith the email address you will use for the first account. The script generates.envandlivekit.generated.yamlwith matching NATS, Chatto, and LiveKit secrets.If you prefer to manage every value yourself, copy
.env.exampleto.env, replace all placeholder secrets with output fromopenssl rand -hex 32, and editlivekit.yamlso its key, secret, and webhook URL match.env. -
Configure SMTP
Edit
.envand replace theCHATTO_SMTP_*placeholders with your provider settings. Direct email/password registration, email verification, and password reset require SMTP.The SMTP example uses STARTTLS submission on port
587. For providers that require SMTPS on port465, setCHATTO_SMTP_TLS=implicit.Set
CHATTO_OWNERS_EMAILSto the email address you will use for the first account. Registration sends a code by email and creates the account with that email already verified; if it matches this list, Chatto assigns the owner role automatically. -
Start the stack
Terminal window docker compose up -dCaddy will automatically obtain TLS certificates for both domains. Visit
https://chat.example.comto create your first account with the email address fromCHATTO_OWNERS_EMAILS.
The image runs Chatto as a non-root user. By default that user is 1000:1000; set PUID and PGID in your shell or Compose environment if the files mounted into /config or /data are owned by a different host user or group. The entrypoint does not recursively change ownership of mounted directories, so make sure writable mounts are owned by the configured IDs.
The generated .env also enables Chatto’s local operator API socket at /tmp/chatto/operator.sock inside the app container. Run operator commands with docker compose exec as the chatto user, and use list --search to find a stable user ID before mutating an account:
docker compose exec -u chatto chatto /chatto operator user listdocker compose exec -u chatto chatto /chatto operator user list --search admin@example.comdocker compose exec -u chatto chatto /chatto operator user set-password USER_IDDo not mount or publish the operator socket unless the target container or host is fully trusted; socket access is root-equivalent Chatto authority.
Persistent Data
Section titled “Persistent Data”| Volume | Contents |
|---|---|
nats_data | NATS/JetStream streams, KV stores — all chat data lives here |
caddy_data | TLS certificates from Let’s Encrypt |
caddy_config | Caddy runtime configuration cache |
Back up nats_data regularly. By default, file attachments and media are also stored in NATS — for heavier usage, consider offloading them to S3-compatible storage. Chatto also supports running multiple replicas behind a load balancer for high availability.
Optional Features
Section titled “Optional Features”Direct Registration and SMTP
Section titled “Direct Registration and SMTP”The setup above enables direct email/password registration, which requires SMTP because Chatto sends registration codes by email. SMTP is also used for email verification and password reset.
If you do not want public email/password registration after the first owner exists, set CHATTO_AUTH_DIRECT_REGISTRATION=false. Users can still sign in through configured SSO providers.
Push Notifications
Section titled “Push Notifications”For notifications when the browser is closed. Generate VAPID keys with npx web-push generate-vapid-keys, then add to .env:
CHATTO_PUSH_ENABLED=trueCHATTO_PUSH_VAPID_PUBLIC_KEY=BNx...CHATTO_PUSH_VAPID_PRIVATE_KEY=...CHATTO_PUSH_VAPID_SUBJECT=admin@example.comVideo Processing
Section titled “Video Processing”The official Chatto Docker image includes ffmpeg. To enable video transcoding, add to .env:
CHATTO_VIDEO_ENABLED=true# Optional tuning# CHATTO_VIDEO_MAX_CONCURRENT=2# CHATTO_VIDEO_MAX_UPLOAD_SIZE=100MB# CHATTO_VIDEO_TEMP_DIR=/tmpDisabling Voice and Video Calls
Section titled “Disabling Voice and Video Calls”If you don’t need voice and video calls, simplify the stack by:
- Removing the
livekitservice and itsdepends_onreferences fromcompose.yml - Deleting the selected LiveKit config (
livekit.generated.yamlorlivekit.yaml) - Removing the
livekit.*block from theCaddyfile - Removing the
CHATTO_LIVEKIT_*variables from.env
You won’t need the livekit.* subdomain or the LiveKit UDP ports.
TURN Server for Restrictive Networks
Section titled “TURN Server for Restrictive Networks”The default setup exposes LiveKit’s direct UDP media range and its embedded TURN/UDP relay:
ports: - "50000-50200:50000-50200/udp" - "3478:3478/udp"The generated and checked-in LiveKit configs enable the matching relay:
turn: enabled: true udp_port: 3478This helps with symmetric NATs and many mobile, Firefox, and restrictive-network failures while keeping the Compose example certificate-free.
Networks that block UDP entirely still need TURN/TLS. Add that only when you also provide a TURN domain plus certificate/key, or run LiveKit behind L4 TLS forwarding that terminates TLS before LiveKit.
See Voice & Video Calls for the full TURN tradeoffs, including when to use a dedicated TURN server.
Troubleshooting
Section titled “Troubleshooting”Chatto can’t connect to NATS — Ensure NATS_TOKEN and CHATTO_NATS_CLIENT_TOKEN match in .env.
Registration says email delivery is not configured — Direct email/password registration requires SMTP. Set CHATTO_SMTP_ENABLED=true and configure CHATTO_SMTP_HOST, CHATTO_SMTP_PORT, CHATTO_SMTP_TLS, and CHATTO_SMTP_FROM. Use CHATTO_SMTP_TLS=implicit for SMTPS providers on port 465. Add username and password when your SMTP server requires authentication.
The first account is not an owner — Make sure CHATTO_OWNERS_EMAILS contains the verified email address for that account. For an already-verified account, restart Chatto after updating the variable; Chatto applies matching owner roles on boot.
Caddy not getting certificates — Verify DNS records point to your server and ports 80/443 are open. Check docker compose logs caddy for ACME errors.
Container startup order — depends_on with condition: service_healthy ensures NATS and LiveKit are ready before Chatto starts. If health checks fail, check the individual service logs.
Calls don’t connect — Verify the LiveKit API key/secret in .env matches the selected LiveKit config (livekit.generated.yaml or livekit.yaml). Ensure CHATTO_LIVEKIT_URL uses the public wss://livekit.* subdomain (browsers connect to it directly). Check that UDP 3478 and 50000-50200 are open in your firewall.
Calls fail for some users — Confirm the built-in TURN/UDP relay is reachable on UDP 3478. Users behind firewalls that block UDP entirely need TURN/TLS. See TURN server for restrictive networks.