Domain, HTTPS and reverse proxies
Put Antimatter on a domain name with HTTPS, using the Caddy profile of the compose file or a reverse proxy of your own, and get the site URL and client addresses right.
The site URL
The site URL (ServiceSettings.SiteURL) is the address people use to reach Antimatter, such as https://chat.example.com. The server uses it in links in emails and notifications, in sign-in redirects (OAuth, OpenID Connect, SAML), for the mobile and desktop apps, and to check where requests come from. Set it to exactly the address in the browser's address bar: scheme, domain and port if not the default, with no trailing slash.
- With the site's compose file:
SITE_URLin.env. - Anywhere else:
AM_SERVICESETTINGS_SITEURL, or System Console › Environment › Web Server › Site URL.
If the site URL is wrong, sign-in with an identity provider fails, links in emails point to the wrong place, and the apps may not connect. After changing the domain, update it here and in every identity provider's redirect URLs.
HTTPS with the bundled Caddy profile
The site's compose.yaml includes a Caddy service in the https profile. It gets a certificate from Let's Encrypt, renews it by itself, redirects HTTP to HTTPS and forwards everything, websockets included, to Antimatter.
- Point a DNS record (A, and AAAA for IPv6) of your domain at the server.
- Open ports 80/tcp, 443/tcp and 443/udp (HTTP/3) in the firewall. Let's Encrypt must reach port 80 or 443 from the Internet.
- Add the domain to
.envand set the site URL:echo "DOMAIN=chat.example.com" >> .env sed -i 's|^SITE_URL=.*|SITE_URL=https://chat.example.com|' .env - Start with the profile:
docker compose --profile https up -d
Add --profile https to every later docker compose up, or put COMPOSE_PROFILES=https in .env so that plain docker compose up -d includes it. Caddy keeps its certificates in the https volume.
Antimatter's own port stays published on the host (HTTP_PORT, 8065 by default). With Caddy in front, keep it for the server itself only:
echo "HTTP_PORT=127.0.0.1:8065" >> .env
docker compose --profile https up -d
Caddy reaches Antimatter over the internal edge network, not through that port, so this changes nothing for your users.
With your own reverse proxy (nginx)
If the server already runs nginx, or another proxy, leave the https profile off and point the proxy at Antimatter's port 8065. The proxy must:
- pass websocket upgrades (the web app and the mobile apps keep a websocket open on
/api/v4/websocket, and some apps open their own); - accept request bodies at least as large as the largest upload (Maximum File Size, 100 MB by default);
- keep long-lived connections open (websockets, large uploads and downloads);
- pass the
Hostheader and the client's address.
A complete server block, adapted from the production template in antimatter-docker. Replace the domain and the certificate paths:
upstream antimatter {
server 127.0.0.1:8065;
keepalive 64;
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' '';
}
proxy_cache_path /var/cache/nginx/antimatter levels=1:2 keys_zone=antimatter_cache:10m
max_size=3g inactive=120m use_temp_path=off;
server {
listen 80;
listen [::]:80;
server_name chat.example.com;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name chat.example.com;
ssl_certificate /etc/ssl/chat.example.com/fullchain.pem;
ssl_certificate_key /etc/ssl/chat.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_timeout 1d;
ssl_session_cache shared:AntimatterSSL:10m;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=31536000" always;
# At least FileSettings.MaxFileSize (100 MB by default).
client_max_body_size 100M;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffers 256 16k;
proxy_buffer_size 16k;
location ~ ^/api/v[0-9]+/(users/)?websocket$ {
client_body_timeout 60s;
send_timeout 300s;
lingering_timeout 5s;
proxy_connect_timeout 90s;
proxy_send_timeout 300s;
proxy_read_timeout 90s;
proxy_pass http://antimatter;
}
location / {
proxy_read_timeout 600s;
proxy_cache antimatter_cache;
proxy_cache_revalidate on;
proxy_cache_min_uses 2;
proxy_cache_use_stale timeout;
proxy_cache_lock on;
proxy_pass http://antimatter;
}
}
Check the file with nginx -t and apply it with systemctl reload nginx. The server sends X-Content-Type-Options, Referrer-Policy, Permissions-Policy and X-Frame-Options itself; only HSTS is up to the proxy. Raise its max-age once HTTPS works, and add includeSubDomains only if every subdomain uses HTTPS.
With Docker Compose and nginx on the host, the upstream above points at the published port 8065. Publish it on 127.0.0.1 only (HTTP_PORT=127.0.0.1:8065), so nobody bypasses the proxy.
The antimatter-docker repository has a full setup with nginx in a container and Let's Encrypt through certbot, in deploy/compose.
Other proxies
Any proxy works if it meets the four points above. Call audio and video never go through the proxy: they use their own port, see Calls and voice.
Client addresses behind a proxy
Behind a proxy, every request seems to come from the proxy. To log the real client address, apply rate limits per client and use IP filtering, tell the server which header carries the address, and which proxies it can believe. Neither setting has a System Console field: set them in config.json or with variables.
| Key and variable | Default | What it does |
|---|---|---|
ServiceSettings.TrustedProxyIPHeader, AM_SERVICESETTINGS_TRUSTEDPROXYIPHEADER | empty: the server uses the address of the connection | The headers that carry the client's address, for example X-Forwarded-For. The first one present in a request is used. |
ServiceSettings.TrustedProxyIPs, AM_SERVICESETTINGS_TRUSTEDPROXYIPS | empty | The addresses or CIDR ranges of your proxies, for example 172.29.71.0/28. The headers only count for requests that come from one of them. |
In a variable, separate several values with spaces: AM_SERVICESETTINGS_TRUSTEDPROXYIPHEADER="X-Forwarded-For X-Real-IP". The server refuses a configuration with an entry in TrustedProxyIPs that is neither an IP address nor a CIDR range.
How the server finds the client's address
Each proxy adds the address it received the request from to the right of X-Forwarded-For. Whatever the client sent in the header itself stays on the left. So the server reads the header from the right, skips the addresses of your trusted proxies, and takes the first other address as the client's. A client can't choose its address by sending its own header.
- Behind one proxy,
TrustedProxyIPHeaderalone gives the right address: the server takes the right-most address of the header. It logs a warning at startup until you also setTrustedProxyIPs. Without it, anyone who reaches port 8065 directly can set the header, so publish that port on127.0.0.1or on a private network only. - Behind several proxies, for example a CDN or a load balancer in front of your own proxy, list all of them in
TrustedProxyIPs. Otherwise the server takes the outer proxy's address for the client's. - The proxy can replace the header with the address it sees, as the nginx example above does (
X-Forwarded-For $remote_addr), or append to it (nginx's$proxy_add_x_forwarded_for). Both work.
Which addresses to list
List the address the proxy's requests come from, as Antimatter sees it.
- With the site's compose file and Caddy, this is already done: Caddy reaches Antimatter over the
edgenetwork, and the compose file setsTrustedProxyIPHeadertoX-Forwarded-ForandTrustedProxyIPsto that network's range,EDGE_SUBNET(172.29.71.0/28unless you change it in.env). ChangeEDGE_SUBNETif it overlaps one of your networks. - The production setup in antimatter-docker does the same for its nginx container, with
FRONTEND_SUBNET. - With nginx on the host and Antimatter in Docker, requests reach Antimatter through the published port, usually from the gateway address of the compose project's network (
docker network inspectshows it). SetAM_SERVICESETTINGS_TRUSTEDPROXYIPSto that address incompose.override.yaml. - Without Docker, with nginx on the same host, it is
127.0.0.1(and::1if nginx connects over IPv6).
TLS without a proxy
The server can also serve HTTPS itself, in System Console › Environment › Web Server: Connection Security set to TLS (ServiceSettings.ConnectionSecurity), with TLS Certificate File and TLS Key File (ServiceSettings.TLSCertFile, ServiceSettings.TLSKeyFile), or Use Let's Encrypt (ServiceSettings.UseLetsEncrypt). The server must then listen on port 443 (Listen Address, ServiceSettings.ListenAddress, :8065 by default), and ServiceSettings.Forward80To443 redirects port 80. A reverse proxy is easier to run and renew, and is what this guide assumes.
Server timeouts and request size
Settings of the server's own web server, in System Console › Environment › Web Server:
| Setting | Key | Default |
|---|---|---|
| Listen Address | ServiceSettings.ListenAddress | :8065 |
| Read Timeout | ServiceSettings.ReadTimeout | 300 seconds |
| Write Timeout | ServiceSettings.WriteTimeout | 300 seconds |
| Idle timeout (no System Console field) | ServiceSettings.IdleTimeout | 60 seconds |
| Maximum Payload Size (Bytes) | ServiceSettings.MaximumPayloadSizeBytes | see Security |
Keep the proxy's own timeouts at least as long as these for normal requests, and its body size at least Maximum File Size.