Skip to content

Docker Compose

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.

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

    From an existing Chatto checkout:

    Terminal window
    cp -R examples/dockercompose chatto
    cd chatto

    From GitHub without cloning the full repository:

    Terminal window
    git clone --depth 1 --filter=blob:none --sparse https://github.com/chattocorp/chatto.git chatto-source
    git -C chatto-source sparse-checkout set examples/dockercompose
    cp -R chatto-source/examples/dockercompose chatto
    rm -rf chatto-source
    cd chatto
  2. Create .env and LiveKit secrets

    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.

    If you prefer to manage every value yourself, copy .env.example to .env, replace all placeholder secrets with output from openssl rand -hex 32, and edit livekit.yaml so its key, secret, and webhook URL match .env.

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

  4. Start the stack

    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.

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.

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 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"
- "3478:3478/udp"

The generated and checked-in LiveKit configs enable the matching relay:

turn:
enabled: true
udp_port: 3478

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

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