Deploying
Running the image, what sits in front of it, and the limits that matter.
Arc is published as a multi-architecture image for linux/amd64 and linux/arm64:
docker pull kryptonhq/arc:latestTags are latest and a version per release (0.2.0, …). Pin the version in production.
Each tag is started against a throwaway Postgres in CI before it is published, so a tag
that exists has booted at least once.
docker run -p 4000:4000 \
-e DATABASE_URL=postgres://arc:arc@postgres:5432/arc \
-e SECRET_KEY_BASE=... -e ARC_ENCRYPTION_KEY=... \
-e PHX_HOST=arc.example.com \
-e ARC_OIDC_ISSUER=... -e ARC_OIDC_CLIENT_ID=... -e ARC_OIDC_CLIENT_SECRET=... \
-e ARC_ADMIN_EMAILS=you@example.com \
kryptonhq/arc:0.2.0Migrations run at startup, so a fresh Postgres plus the image is a working install. See configuration for the full list of variables.
In front of Arc
TLS terminates at your proxy, which must forward WebSocket upgrades:
location / {
proxy_pass http://arc;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 300s;
}Raise the idle timeout: connections are meant to stay open, and a proxy that closes them after 60 seconds turns into a reconnect storm.
Tell Arc which proxies to believe about the client's address, as CIDRs:
ARC_TRUSTED_PROXIES=10.0.0.0/8Only then is X-Forwarded-For used: for the per-address connection limit and for the
client_ip field in logs. Unset, every client appears to be the proxy, the header is
ignored, and a client cannot forge its address.
The dashboard's session cookie carries the Secure flag, so it is only sent over TLS.
That is right behind a TLS-terminating proxy; for an install that is deliberately plain
http, set PHX_PUBLIC_SCHEME=http and the flag is dropped. Every dashboard response
also carries a Content-Security-Policy that permits Arc's own assets and the fonts it
loads, and nothing else.
Health checks
| Path | Answers 200 when |
|---|---|
/health/live | The VM is running |
/health/ready | Migrations are applied, the app cache is warm, Postgres answered the last check, and the node is not draining |
Point load balancers and Kubernetes readiness probes at /health/ready, so traffic waits
until a node can actually serve it. Use /health/live for liveness only — restarting a
node because it is still warming would be a loop.
Readiness follows Postgres: every five seconds the node runs SELECT 1 and caches the
answer (probes never touch the database themselves). While Postgres is unreachable the
node reports 503 database unreachable so new traffic goes elsewhere. Connections it
already holds keep working, because publishing, subscribing and channel authorisation
never need the database; only the dashboard, webhook persistence and sign-in do.
Metrics
/metrics is on the same listener as everything else. It is public unless
ARC_METRICS_AUTH_TOKEN is set, in which case the scraper sends it as a bearer token.
Set it in production: the endpoint reports per-app connection counts.
File descriptors
Every connection is a socket. Raise the limit well above your connection ceiling:
ulimits:
nofile:
soft: 65536
hard: 65536Shutting down
On SIGTERM, Arc drains in the order a load balancer needs:
/health/readyturns503 drainingat once, and new WebSocket connections are refused with4101.- It waits
ARC_DRAIN_SECONDS(default 5) so the balancer's health check notices and stops routing here. - Every connection is closed with
4101(reconnect after backoff), so clients move to another node, and webhook events still in the batch window are persisted.
Only then does the node stop. The platform's grace period must be longer than the drain window plus a few seconds, or the VM is killed mid-drain:
# Kubernetes
terminationGracePeriodSeconds: 30
# Compose
stop_grace_period: 30sRoll one node at a time and wait for the new node's /health/ready before the next.