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 setupto generate secrets) - git to clone the repository
- A reverse proxy (Caddy, nginx, Traefik, …) to terminate TLS and route traffic to port
8080
Quick Start
Three commands and you're running:
# 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/healthzhere. - 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):
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.
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:
- Set
SERVER_INVITE_CODE_REQUIRED=true - Set
SERVER_INVITE_CODE=your-secret-code - 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
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 whenCOMPOSE_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)
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
# 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;
}
}
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.