Run your own freeq server with TLS, the web client, and optional federation.
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
docker run -d \
-p 6667:6667 -p 8080:8080 \
-v freeq-data:/data \
ghcr.io/chad/freeq:latest
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
| 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 |
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.
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/*.
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 |
| 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 |
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.
freeq-server --motd "Welcome to my server"
# or
freeq-server --motd-file /path/to/motd.txt
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;
}
}
[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
| 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
*.secretor*.pem/*.keyfiles to version
control. They are excluded by.gitignorebut always verify before pushing.
See Security Hardening Guide for key rotation procedures.
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.
# 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.
# Back up all key files
cp /data/*.secret /backup/keys/
chmod 600 /backup/keys/*
Critical: The
db-encryption-key.secretfile is required to read
encrypted messages. If lost, message history is irrecoverable.
.db files to --db-path location.secret files to --data-dir locationThese are hardcoded. For additional rate limiting, configure your reverse proxy.
# 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 ...
See Security Hardening Guide for: