Skip to content

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.

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.

  1. Get the example

    Terminal window
    git clone --depth 1 https://github.com/chattocorp/chatto.git
    cd chatto/examples/dockercompose
  2. Point both domains at your server

    Create DNS records for chat.example.com and livekit.chat.example.com. Allow inbound TCP 80, 443, and 7881 plus UDP 3478 and 50000-50200 in your host and cloud firewalls.

  3. Generate the configuration

    Terminal window
    ./init-env.sh chat.example.com admin@example.com

    Replace chat.example.com with your Chatto domain and admin@example.com with the email address you will use for the first account. The script generates .env and livekit.generated.yaml with matching NATS, Chatto, and LiveKit secrets.

  4. Configure SMTP

    Edit .env and replace the CHATTO_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 port 465, set CHATTO_SMTP_TLS=implicit.

    Set CHATTO_OWNERS_EMAILS to 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.

  5. Start Chatto

    Terminal window
    docker compose up -d

    Caddy will automatically obtain TLS certificates for both domains. Visit https://chat.example.com to create your first account with the email address from CHATTO_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.

DOCKER NETWORK BrowserSvelteKit SPA :443CaddyTLS · Load Balancer :4222NATSJetStream · KV Store :4000ChattoConnectRPC · WebSocket :7880LiveKitWebRTC Media HTTPS WSS TCP :7881 · UDP :3478, :50000–50200
  • 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.

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.

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.

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:

Terminal window
docker compose exec -u chatto chatto /chatto operator user list
docker compose exec -u chatto chatto /chatto operator user list --search admin@example.com
docker compose exec -u chatto chatto /chatto operator user set-password USER_ID

Do not mount or publish the operator socket unless the target container or host is fully trusted; socket access is root-equivalent Chatto authority.

VolumeContents
nats_dataNATS/JetStream streams, KV stores — all chat data lives here
caddy_dataTLS certificates from Let’s Encrypt
caddy_configCaddy 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.

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.

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.

The example publishes LiveKit’s Layer 4 media ports directly from its container to the host:

Public portDestinationProtocol and purpose
TCP 80CaddyHTTP redirect and ACME HTTP challenge
TCP 443CaddyHTTPS and secure WebSocket traffic for both domains
TCP 7881LiveKit 7881WebRTC media fallback when UDP is unavailable
UDP 3478LiveKit 3478Embedded TURN/STUN relay
UDP 50000-50200Same LiveKit portsDirect 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.

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.

For notifications when the browser is closed. Generate VAPID keys with npx web-push generate-vapid-keys, then add to .env:

Terminal window
CHATTO_PUSH_ENABLED=true
CHATTO_PUSH_VAPID_PUBLIC_KEY=BNx...
CHATTO_PUSH_VAPID_PRIVATE_KEY=...
CHATTO_PUSH_VAPID_SUBJECT=admin@example.com

The official Chatto Docker image includes ffmpeg. To enable video transcoding, add to .env:

Terminal window
CHATTO_VIDEO_ENABLED=true
# Optional tuning
# CHATTO_VIDEO_MAX_CONCURRENT=2
# CHATTO_VIDEO_MAX_UPLOAD_SIZE=100MB
# CHATTO_VIDEO_TEMP_DIR=/tmp

If you don’t need voice and video calls, simplify the stack by:

  1. Removing the livekit service and its depends_on references from compose.yml
  2. Deleting the selected LiveKit config (livekit.generated.yaml or livekit.yaml)
  3. Removing the livekit.* block from the Caddyfile
  4. Removing the CHATTO_LIVEKIT_* variables from .env

You won’t need the livekit.* subdomain, TCP 7881, or the LiveKit UDP ports.

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: 3478

Direct 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.

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 orderdepends_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.