Docker Compose
Docker Compose is the quickest way to run the complete Chatto stack on one server. The example includes Chatto, NATS for persistent data, LiveKit for calls, and Caddy for automatic HTTPS.
Quick Start
Section titled “Quick Start”You need Docker with Compose v2, a server with a public IP address, and SMTP
credentials. The commands below use chat.example.com for Chatto and
livekit.chat.example.com for calls.
The LiveKit hostname is only an example. You can use any hostname you control,
such as calls.example.com; pass your Chatto hostname to init-env.sh, then
set CHATTO_LIVEKIT_URL and the matching proxy route to your chosen LiveKit
hostname.
-
Get the example
Terminal window git clone --depth 1 https://github.com/chattocorp/chatto.gitcd chatto/examples/dockercompose -
Point both domains at your server
Create DNS records for
chat.example.comandlivekit.chat.example.com. Allow inbound TCP 80, 443, and 7881 plus UDP 3478 and 50000-50200 in your host and cloud firewalls. -
Generate the configuration
Terminal 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. -
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 Chatto
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.
That is enough for a complete single-server deployment. The remaining sections explain how the pieces fit together and how to customize them.
Understand the Stack
Section titled “Understand the Stack”- Chatto serves the web app and APIs.
- NATS stores Chatto’s persistent data. Running it separately is the recommended production topology: you can restart or replace Chatto without restarting its data store, run multiple Chatto replicas against the same data, and grow into a NATS cluster later.
- LiveKit provides voice and video calls. Remove it if you do not need calls.
- Caddy provides automatic HTTPS and reverse proxying. Replace it if you already use another proxy.
Why a Separate NATS Server?
Section titled “Why a Separate NATS Server?”The standalone-binary setup embeds NATS inside Chatto, which is wonderfully simple for one process. Compose runs the same NATS JetStream storage as its own small container. This keeps the app and its data lifecycle separate and avoids a migration before adding Chatto replicas or zero-downtime app updates.
This choice does not lock in your data. chatto backup creates a portable
JetStream archive that chatto restore can load into either embedded or
external NATS. You can therefore move between the standalone-binary, Compose,
and clustered deployment models without rebuilding your community. See
Backup & Restore for the commands and key
handling guidance.
Prefer to Configure It by Hand?
Section titled “Prefer to Configure It by Hand?”You usually do not need to: init-env.sh generates the secrets and config files
for you. If you manage secrets elsewhere, start with .env.example, fill in its
placeholders, and make sure the LiveKit key and secret are the same in .env
and livekit.yaml.
Container and Operator Details
Section titled “Container and Operator Details”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.
Use Your Existing Reverse Proxy
Section titled “Use Your Existing Reverse Proxy”Already running Caddy, nginx, Apache, Traefik, or another user-facing web server? You can keep it. Chatto needs only two HTTP reverse-proxy routes:
- Your Chatto HTTPS hostname routes to Chatto on port 4000.
- Your chosen LiveKit secure WebSocket hostname routes to LiveKit on port 7880.
For example, the hostnames could be chat.example.com and
calls.example.com. They do not need to share a parent domain or use the name
livekit. Both need publicly trusted TLS certificates.
If your proxy joins the Compose network, route directly to chatto:4000 and
livekit:7880. If it runs on the Docker host, replace those services’ expose
entries with loopback-only mappings such as 127.0.0.1:4000:4000 and
127.0.0.1:7880:7880. Then remove the caddy service and its volumes from
compose.yml.
What the Included Caddy Does
Section titled “What the Included Caddy Does”If you do not already have a proxy, the included Caddy configuration obtains certificates, creates both routes, and load-balances Chatto replicas:
chat.example.com { reverse_proxy { dynamic a chatto 4000 { refresh 5s versions ipv4 } lb_policy round_robin }}
livekit.chat.example.com { reverse_proxy livekit:7880}The first route carries the Chatto web app, APIs, realtime connections, and LiveKit webhooks. The second carries LiveKit’s API and WebSocket signaling. Caddy handles WebSocket upgrades automatically.
The dynamic DNS upstream discovers each scaled Chatto container separately. Caddy refreshes the list and distributes new requests across healthy replicas; existing realtime connections stay with the replica that accepted them.
Network Ports
Section titled “Network Ports”The example publishes LiveKit’s Layer 4 media ports directly from its container to the host:
| Public port | Destination | Protocol and purpose |
|---|---|---|
| TCP 80 | Caddy | HTTP redirect and ACME HTTP challenge |
| TCP 443 | Caddy | HTTPS and secure WebSocket traffic for both domains |
| TCP 7881 | LiveKit 7881 | WebRTC media fallback when UDP is unavailable |
| UDP 3478 | LiveKit 3478 | Embedded TURN/STUN relay |
| UDP 50000-50200 | Same LiveKit ports | Direct WebRTC media |
The standard Caddy image does not include the optional Caddy L4 plugin, and this single-host setup does not need it. Open the matching host and cloud firewall rules, and do not expose NATS port 4222 publicly.
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, TCP 7881, 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" - "7881:7881/tcp" - "3478:3478/udp"The generated and checked-in LiveKit configs enable the matching TCP fallback and relay:
rtc: tcp_port: 7881
turn: enabled: true udp_port: 3478Direct UDP provides the best media path. TCP 7881 gives LiveKit an ICE/TCP fallback when UDP is unavailable, while TURN/UDP helps with symmetric NATs and many mobile, Firefox, and restrictive-network failures. This keeps the Compose example certificate-free beyond the HTTPS certificates managed by Caddy.
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 TCP 7881 and UDP 3478 and 50000-50200 are open in your firewall. For a LAN-only host without an external IP address, set rtc.use_external_ip: false in livekit.generated.yaml before starting or restarting LiveKit.
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.