Self-Hosting Guide

Everything you need to run Agoria Chat on your own server. The default setup uses Docker Compose and takes about five minutes from clone to a running instance.

Prerequisites

You need the following on the host machine before you start:

  • Docker ≥ 24 with the Compose plugin. Verify with docker compose version
  • openssl (available on all major Linux distros and macOS; used by make setup to generate secrets)
  • git to clone the repository
  • A reverse proxy (Caddy, nginx, Traefik, …) to terminate TLS and route traffic to port 8080
Note: Agoria does not handle TLS itself. You must put a reverse proxy in front of it and configure HTTPS separately. Caddy is the simplest option as it auto-provisions Let's Encrypt certificates.

Quick Start

Three commands and you're running:

bash
# Clone the repo
git clone https://github.com/C05CDA/agoria.git
cd agoria

# Interactive wizard: sets PUBLIC_BASE_URL, voice, storage, etc.
make setup

# Build images, run migrations, launch everything in background
make start

make setup walks you through all configuration options and writes a .env file at the project root. make start then builds the Docker images, runs any pending database migrations, and brings the full stack up in the background.

Once running, two ports are exposed on the host and must be proxied:

  • Port 8080: API server. Your reverse proxy should route /api/* and /healthz here.
  • Port 3000: Web frontend (Nginx serving the Vue app). Route everything else here.

See the Reverse Proxy Setup section for ready-to-use config examples.

If you enabled voice (COMPOSE_PROFILES=voice), you must also open the following ports on your server firewall. The WebRTC media ports need direct client access and must not be proxied:

  • Port 7880/tcp: LiveKit signaling. Route /rtc/* and /twirp/* through your reverse proxy to this port.
  • Ports 7882–7892/udp: WebRTC media (direct). Open on the firewall; do not proxy.
  • Port 17478/tcp+udp: TURN relay fallback (direct). Open on the firewall; do not proxy.

See the Voice Channels section for full details.

Updating

Pull the latest code and run make start again. Migrations are re-applied automatically (they are idempotent):

bash
git pull
make start

Environment Variables

Configuration lives in .env at the project root. make setup generates this for you. If you prefer to configure manually, use .env.example as the reference. Note that some variables like DATABASE_URL are constructed internally by Docker Compose and don't need to be set by hand.

Variable Default Description
PUBLIC_BASE_URL http://localhost Public URL of your instance (e.g. https://chat.example.com). Used for CORS and redirect URLs.
POSTGRES_PASSWORD generated PostgreSQL password. Keep this secret and never commit it.
LIVEKIT_API_KEY generated LiveKit API key. Leave blank (along with LIVEKIT_API_SECRET and LIVEKIT_URL) to disable voice channels.
LIVEKIT_API_SECRET generated LiveKit API secret.
LIVEKIT_URL empty URL of the LiveKit server. When using the bundled container (COMPOSE_PROFILES=voice) this is set automatically to http://livekit:7880. For an external LiveKit server use wss://livekit.example.com.
COMPOSE_PROFILES empty Set to voice to start the bundled LiveKit container alongside the other services.
GIPHY_API_KEY empty Giphy API key. Leave blank to disable the GIF picker in the composer.
SERVER_INVITE_CODE_REQUIRED false When true, new users must supply an invite code to register.
SERVER_INVITE_CODE empty The invite code value used when SERVER_INVITE_CODE_REQUIRED is true.
UPLOADS_DRIVER local Storage backend: local (filesystem) or s3.
S3_ENDPOINT empty S3-compatible endpoint URL. Leave blank to use AWS default. Required when UPLOADS_DRIVER=s3.
S3_BUCKET empty S3 bucket name. Required when UPLOADS_DRIVER=s3.
S3_ACCESS_KEY_ID empty S3 / AWS access key ID.
S3_SECRET_ACCESS_KEY empty S3 / AWS secret access key.
S3_REGION empty S3 region (e.g. us-east-1).
S3_USE_PATH_STYLE false Set to true to use path-style S3 addressing (required for MinIO and some self-hosted S3-compatible services).

Voice Channels (LiveKit)

Voice is powered by LiveKit, an open-source WebRTC SFU. Agoria ships a bundled LiveKit container for convenience, but you can also point it at any existing LiveKit deployment.

Option A: Bundled container (simplest)

Set COMPOSE_PROFILES=voice in your .env and make start will bring up LiveKit alongside the other services. make setup handles this automatically when you enable voice.

Open the following ports on your server firewall (the voice SFU needs direct client access; do not proxy these through nginx or Caddy):

  • 7880/tcp: LiveKit signaling. Your reverse proxy routes /rtc/* and /twirp/* here.
  • 7882-7892/udp: Direct WebRTC media. Open on your firewall but do NOT proxy.
  • 17478/tcp+udp: TURN relay fallback. Open on your firewall but do NOT proxy.

Option B: External LiveKit

Point Agoria at your existing LiveKit server by setting LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET. Leave COMPOSE_PROFILES unset.

Disable voice entirely: Leave LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET all empty. Voice channel creation will be hidden in the UI.

File Storage

Uploaded files (avatars, attachments, emoji) can be stored locally or on any S3-compatible service.

Local filesystem (default)

Set UPLOADS_DRIVER=local. Files are stored inside the api container's volume. Ensure your reverse proxy serves the /uploads/ path from that volume if you want CDN-style caching.

S3-compatible (AWS, Cloudflare R2, MinIO)

Set UPLOADS_DRIVER=s3 and populate the S3_* variables. For Cloudflare R2 set S3_ENDPOINT to your R2 endpoint URL.

Invite Codes

By default, anyone who can reach your instance can register. To require an invite code:

  1. Set SERVER_INVITE_CODE_REQUIRED=true
  2. Set SERVER_INVITE_CODE=your-secret-code
  3. Restart: make start

Share the code with people you want to allow in. Change it at any time and restart to invalidate the old one.

Make Commands

all commands
make setup        # interactive config wizard → generates .env
make start        # build images, migrate DB, start in background
make stop         # stop all containers (data preserved)
make logs         # tail live logs from all services
make migrate-up   # manually apply pending DB migrations
make migrate-down  # roll back the last DB migration
make destroy      # ⚠ wipe all containers AND volumes (irreversible)
make help         # list all available commands

Architecture

A standard make start brings up the following Docker Compose services:

  • api: Go REST + WebSocket backend, exposed on host port 8080
  • web: Vue 3 web client served by Nginx, exposed on host port 3000
  • postgres: PostgreSQL 16 database
  • redis: Redis 7, used for session storage and pub/sub
  • livekit: LiveKit SFU for voice, exposed on 7880/tcp (only when COMPOSE_PROFILES=voice)

Your reverse proxy routes API and WebSocket traffic (/api/*, /healthz) to port 8080 and all other requests (the SPA) to port 3000. See the reverse proxy configs below for copy-paste examples.

WebSocket connections to /api/ws use a lightweight pub/sub hub for real-time events (channel messages, presence, voice participant updates). DM message content is end-to-end encrypted client-side; the server stores and forwards ciphertext only. Server text channels are stored in plaintext and are readable by the server admin, same as any self-hosted chat platform.

Reverse Proxy Setup

Two ports need to be proxied: 8080 (API) and 3000 (web frontend). Use a reverse proxy to terminate TLS and route traffic. Below are complete configs for the most common options.

Caddy (recommended, auto-TLS)

Caddyfile
chat.example.com {
    # API backend
    reverse_proxy /api/* localhost:8080
    reverse_proxy /healthz localhost:8080

    # LiveKit signaling (voice - remove if voice is disabled)
    reverse_proxy /rtc/* localhost:7880 {
        transport http {
            versions 1.1
        }
    }
    reverse_proxy /twirp/* localhost:7880 {
        transport http {
            versions 1.1
        }
    }

    # Web frontend (SPA - must be last)
    reverse_proxy /* localhost:3000
}

nginx

nginx.conf snippet
# Map HTTP Upgrade header to control Connection header
map $http_upgrade $connection_upgrade {
    default  upgrade;
    ''       close;
}

server {
    listen 80;
    server_name chat.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name chat.example.com;

    ssl_certificate     /etc/letsencrypt/live/chat.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/chat.example.com/privkey.pem;

    # WebSocket - must come before the general /api/ block
    location /api/ws {
        proxy_pass          http://localhost:8080;
        proxy_http_version  1.1;
        proxy_set_header    Upgrade           $http_upgrade;
        proxy_set_header    Connection        $connection_upgrade;
        proxy_set_header    Host              $host;
        proxy_set_header    X-Real-IP         $remote_addr;
        proxy_set_header    X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header    X-Forwarded-Proto $scheme;
    }

    # API backend
    location /api/ {
        proxy_pass         http://localhost:8080;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }

    location /healthz {
        proxy_pass http://localhost:8080;
    }

    # LiveKit signaling (voice - remove if voice is disabled)
    location /rtc/ {
        proxy_pass          http://localhost:7880;
        proxy_http_version  1.1;
        proxy_set_header    Upgrade $http_upgrade;
        proxy_set_header    Connection $connection_upgrade;
        proxy_set_header    Host $host;
    }

    location /twirp/ {
        proxy_pass         http://localhost:7880;
        proxy_http_version 1.1;
        proxy_set_header   Host $host;
    }

    # Web frontend (SPA)
    location / {
        proxy_pass         http://localhost:3000;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }
}
WebSocket upgrade: The map block is required. Without it, nginx sends Connection: upgrade for all requests, which breaks keep-alive for regular HTTP. The map sets Connection: upgrade only when the client actually requested an upgrade, and Connection: close otherwise.