← All docs

Self-Hosting Guide

Run your own freeq server with TLS, the web client, and optional federation.

Quick Start

From source

git clone https://github.com/chad/freeq
cd freeq
cargo build --release -p freeq-server

# Start with defaults (port 6667, no TLS, in-memory)
./target/release/freeq-server --bind 0.0.0.0:6667

With Docker

docker run -d \
  -p 6667:6667 -p 8080:8080 \
  -v freeq-data:/data \
  ghcr.io/chad/freeq:latest

With Docker Compose

git clone https://github.com/chad/freeq
cd freeq
cp .env.example .env    # edit with your values
docker compose up -d

For TLS termination with nginx:

docker compose --profile with-tls up -d

For the OAuth broker (needed for web client AT Protocol login):

docker compose --profile with-broker up -d

Configuration Reference

Listeners

Flag Default Description
--bind 127.0.0.1:6667 Plain TCP listener
--tls-bind 127.0.0.1:6697 TLS listener (requires cert + key)
--web-addr (none) HTTP/WebSocket listener

TLS

freeq-server \
  --bind 0.0.0.0:6667 \
  --tls-bind 0.0.0.0:6697 \
  --tls-cert /path/to/cert.pem \
  --tls-key /path/to/key.pem

Use Let's Encrypt with auto-renewal for production. See the nginx config
below for TLS termination at the reverse proxy instead.

Web Client

cd freeq-app && npm install && npm run build && cd ..

freeq-server \
  --bind 0.0.0.0:6667 \
  --web-addr 0.0.0.0:8080 \
  --web-static-dir freeq-app/dist

The web client is served at the root path. WebSocket IRC is at /irc.
REST API endpoints are at /api/v1/*.

Persistence

freeq-server --db-path /data/irc.db --data-dir /data
Flag Default Description
--db-path (none — in-memory) SQLite database file
--data-dir parent of --db-path Directory for keys and iroh state
--max-messages-per-channel 10000 Prune oldest messages beyond this count

Identity & Auth

Flag / Env Description
--server-name IRC server name (appears in messages)
--challenge-timeout-secs SASL challenge validity window (default: 60)
--oper-password / OPER_PASSWORD Enable OPER command with this password
--oper-dids / OPER_DIDS DIDs auto-granted server operator on connect
BROKER_SHARED_SECRET HMAC secret shared with auth broker
GITHUB_CLIENT_ID GitHub OAuth for credential verifier
GITHUB_CLIENT_SECRET GitHub OAuth secret

Federation

freeq-server \
  --iroh \
  --s2s-peers <peer-id> \
  --s2s-allowed-peers <peer-id>
Flag Default Description
--iroh off Enable iroh QUIC transport
--iroh-port random UDP port for iroh
--s2s-peers (none) Peer endpoint IDs to connect to on startup
--s2s-allowed-peers (none — open) Allowlist for incoming peer connections
--s2s-peer-trust (none) Trust levels per peer: id:full, id:relay, id:readonly
--server-did (none) Server DID for federation identity (e.g. did:web:irc.example.com)

See Federation, S2S Auth, Server DID Setup, and Security Guide for details.

MOTD

freeq-server --motd "Welcome to my server"
# or
freeq-server --motd-file /path/to/motd.txt

nginx Reverse Proxy

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

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

    location /irc {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_read_timeout 86400;
    }

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

systemd Service

[Unit]
Description=freeq IRC server
After=network.target

[Service]
Type=simple
User=freeq
WorkingDirectory=/opt/freeq
ExecStart=/opt/freeq/freeq-server \
  --bind 0.0.0.0:6667 \
  --tls-bind 0.0.0.0:6697 \
  --tls-cert /etc/letsencrypt/live/irc.example.com/fullchain.pem \
  --tls-key /etc/letsencrypt/live/irc.example.com/privkey.pem \
  --web-addr 127.0.0.1:8080 \
  --web-static-dir /opt/freeq/freeq-app/dist \
  --db-path /opt/freeq/data/irc.db \
  --data-dir /opt/freeq/data \
  --server-name irc.example.com
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Data Files

File Purpose
irc.db Message history, channels, user data (SQLite)
irc-policy.db Policy rules and credentials (SQLite)
msg-signing-key.secret Server message signing key (ed25519)
verifier-signing-key.secret Credential verifier signing key
db-encryption-key.secret Database encryption-at-rest key
iroh-key.secret iroh QUIC endpoint identity key

All key files are generated automatically on first run.

⚠️ WARNING: Never commit *.secret or *.pem/*.key files to version
control. They are excluded by .gitignore but always verify before pushing.
See Security Hardening Guide for key rotation procedures.

Encryption at Rest

Message text is encrypted with AES-256-GCM before writing to SQLite. The key
is stored in db-encryption-key.secret. Messages are transparently decrypted
on read. Back up this key — losing it makes all stored messages unreadable.

Backups

Database

# Hot backup (SQLite VACUUM INTO)
sqlite3 /data/irc.db "VACUUM INTO '/backup/irc-$(date +%Y%m%d).db'"
sqlite3 /data/irc-policy.db "VACUUM INTO '/backup/irc-policy-$(date +%Y%m%d).db'"

Or simply copy the .db file while the server is stopped.

Keys

# Back up all key files
cp /data/*.secret /backup/keys/
chmod 600 /backup/keys/*

Critical: The db-encryption-key.secret file is required to read
encrypted messages. If lost, message history is irrecoverable.

Restore

  1. Stop the server
  2. Copy backup .db files to --db-path location
  3. Copy backup .secret files to --data-dir location
  4. Start the server

Connection Limits

  • Per-IP: 20 concurrent connections (TCP and WebSocket)
  • Rate limiting: 10 commands/sec per client (token bucket, exempt during registration)
  • S2S: 100 events/sec per peer

These are hardcoded. For additional rate limiting, configure your reverse proxy.

Logging

# Default: human-readable
RUST_LOG=info freeq-server ...

# Structured JSON (for log aggregation)
RUST_LOG=info FREEQ_LOG_JSON=1 freeq-server ...

# Debug logging for specific modules
RUST_LOG=freeq_server::s2s=debug,info freeq-server ...

Security

See Security Hardening Guide for:

  • S2S federation allowlists
  • Key management and rotation
  • Production configuration checklist
Supported by
Powered by