Arc
Running it

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:latest

Tags 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.0

Migrations 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/8

Only 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

PathAnswers 200 when
/health/liveThe VM is running
/health/readyMigrations 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: 65536

Shutting down

On SIGTERM, Arc drains in the order a load balancer needs:

  1. /health/ready turns 503 draining at once, and new WebSocket connections are refused with 4101.
  2. It waits ARC_DRAIN_SECONDS (default 5) so the balancer's health check notices and stops routing here.
  3. 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: 30s

Roll one node at a time and wait for the new node's /health/ready before the next.

On this page