Deployment
Deploy A3S Gateway (v1.0.11) with Homebrew, Cargo, Docker, or Helm/Kubernetes — production configuration, RBAC, and hot reload
Deployment
A3S Gateway v1.0.11 ships as a single self-contained binary. The same binary runs as a standalone reverse proxy, a Docker container, or a Kubernetes ingress controller. This page covers every supported install path and the production-relevant behavior of each.
Version note. The crate version is
1.0.11(Cargo.toml). The MSRV is Rust1.88. Published Linux binaries and OCI images are built with thekubeandredisCargo features enabled (see Cargo features); a default-feature build has neither.
Install paths at a glance
| Method | Best for | Features built in |
|---|---|---|
| Homebrew | Local / native macOS | default build |
| Cargo | From source, custom feature set | whatever you select |
Docker (ghcr.io/a3s-lab/gateway) | Containers, Compose, CI | kube + redis |
| Helm / Kubernetes | Cluster ingress controller | kube + redis |
Homebrew
Native macOS install via the A3S tap:
brew install a3s-lab/tap/a3s-gateway
# or, equivalently
brew tap a3s-lab/tap
brew install a3s-gateway
a3s-gateway --config gateway.aclThis is a default-feature build. The kube provider and rate-limit-redis
middleware are not compiled in — a providers.kubernetes block only logs a
warning, and a rate-limit-redis middleware fails config load. Use Docker or a
cargo install with explicit features if you need those on macOS.
Cargo
# Default build (no kube / no redis)
cargo install a3s-gateway
# With the distributed rate limiter and the Kubernetes provider
cargo install a3s-gateway --features kube,redisThe config file must use the .acl extension — TOML/YAML/JSON/HCL files are
rejected at load. Validate before you ship:
a3s-gateway validate --config gateway.aclBinary + systemd
# Run directly
a3s-gateway --config gateway.acl
# Optional: override the first entrypoint's listen address at startup
a3s-gateway --config gateway.acl --listen 0.0.0.0:8080
# Validate a config without starting
a3s-gateway validate --config gateway.acl
# Systemd unit
cat > /etc/systemd/system/a3s-gateway.service <<'EOF'
[Unit]
Description=A3S Gateway
After=network.target
[Service]
ExecStart=/usr/local/bin/a3s-gateway --config /etc/gateway/gateway.acl
Restart=always
User=a3s-gateway
# Required only if the management listener uses bearer auth (default token env var):
# Environment=A3S_GATEWAY_ADMIN_TOKEN=...
[Install]
WantedBy=multi-user.target
EOF
systemctl enable --now a3s-gatewayCLI reference
The binary exposes a small set of subcommands. The global flags --config
(default gateway.acl), --listen, and --log-level (default info) apply to
the run command (no subcommand).
# Validate without starting
a3s-gateway validate --config gateway.acl
# Inspect parsed config (subcommand goes AFTER the --config flag)
a3s-gateway config --config gateway.acl summary # compact summary
a3s-gateway config --config gateway.acl entrypoints # list entrypoints
a3s-gateway config --config gateway.acl routes # list routers
a3s-gateway config --config gateway.acl services # list services + backend counts
a3s-gateway config --config gateway.acl middlewares # list middleware names
a3s-gateway config --config gateway.acl providers # list enabled providers
a3s-gateway config --config gateway.acl json # full parsed config as JSON
# Talk to a running management listener (see Management API below)
a3s-gateway management events --url http://127.0.0.1:9090/api/gateway
a3s-gateway management validate --url http://127.0.0.1:9090/api/gateway --file gateway.acl
a3s-gateway management reload --url http://127.0.0.1:9090/api/gateway --file gateway.acl
# Self-update
a3s-gateway updateThe management subcommands accept --token / --token-env for bearer auth and
--ca-cert / --client-cert / --client-key for TLS / mTLS against a
TLS-protected management listener (plus --insecure for local diagnostics only).
Docker
The published image is ghcr.io/a3s-lab/gateway (Alpine-based, runs as a
non-root gateway user, multi-platform: linux/amd64, linux/arm64,
darwin/arm64). It is built with kube + redis enabled.
docker run -d \
-v "$(pwd)/gateway.acl:/etc/gateway/gateway.acl" \
-p 8080:8080 \
-p 9090:9090 \
ghcr.io/a3s-lab/gateway:latest \
--config /etc/gateway/gateway.aclThe image EXPOSEs 80 443 8080. Map whatever ports your entrypoints actually
bind. The example above also publishes 9090 so the optional
management listener is reachable.
Healthcheck caveat. The image ships a Docker
HEALTHCHECKthat runswget http://localhost:8080/health. The data plane has no built-in/healthendpoint — that path only succeeds if you define a router for it (or proxy it to an upstream). The real health endpoint lives on the management listener at{path_prefix}/health(defaulthttp://127.0.0.1:9090/api/gateway/health), not on a traffic entrypoint. For orchestrators, prefer a TCP readiness/liveness probe against a bound entrypoint port (this is what the Helm chart does), or point an HTTP probe at the managementhealthendpoint.
docker compose
services:
gateway:
image: ghcr.io/a3s-lab/gateway:latest
command: ["--config", "/etc/gateway/gateway.acl"]
ports:
- "8080:8080"
- "9090:9090" # management listener (optional)
volumes:
- ./gateway.acl:/etc/gateway/gateway.acl:ro
environment:
# Only needed if management.enabled = true with default bearer auth
A3S_GATEWAY_ADMIN_TOKEN: "${A3S_GATEWAY_ADMIN_TOKEN}"
restart: unless-stoppedHelm / Kubernetes
The chart lives at deploy/helm/a3s-gateway/. It is intentionally minimal: a
Deployment, a Service, and a ConfigMap that carries your gateway.acl.
Chart version / appVersion is 1.0.2 and the default values.yaml ships an
image.tag of 0.2.1 — both lag the 1.0.11 binary, so pin image.tag
explicitly in production.
Install
helm install gateway deploy/helm/a3s-gateway \
--set image.repository=ghcr.io/a3s-lab/gateway \
--set image.tag=1.0.11 \
--set image.pullPolicy=IfNotPresentThe default
image.repositoryisa3s-gatewaywithpullPolicy: Never(tuned for a local OrbStack/k8s shared-daemon dev loop). For a real cluster you must override the repository, tag, and pull policy as shown above.
Custom configuration
The gateway is configured entirely through its gateway.acl, surfaced as the
config value and mounted from a ConfigMap at /etc/gateway/gateway.acl. The
Deployment runs --config /etc/gateway/gateway.acl.
Provide a complete config from a file:
helm install gateway deploy/helm/a3s-gateway \
--set image.tag=1.0.11 \
--set-file config=./gateway.aclExpose it externally as a LoadBalancer instead of the default NodePort:
helm install gateway deploy/helm/a3s-gateway \
--set image.tag=1.0.11 \
--set service.type=LoadBalancerHelm values
These are the only values the chart honors. (Earlier docs advertised
autoscaling.*, ingress.*, and resources.* blocks — those templates do not
exist in the chart and are silently ignored.)
Prop
Type
The Deployment hard-codes:
containerPort: 8080andService.targetPort: 8080— your primary HTTP entrypoint must bind:8080(the bundled demo config usesaddress = "0.0.0.0:8080").- TCP readiness and liveness probes on
8080(readiness: 2s delay / 5s period; liveness: 5s delay / 10s period). There is no HTTP health probe by default — see the healthcheck caveat above.
Scaling is via plain replicaCount; the chart does not template an HPA. If you
want horizontal autoscaling, attach a standard Kubernetes HorizontalPodAutoscaler
to the generated Deployment out-of-band.
Chart structure
deploy/helm/a3s-gateway/
├── Chart.yaml # version/appVersion 1.0.2
├── values.yaml
└── templates/
├── configmap.yaml # gateway.acl from .Values.config
├── deployment.yaml # pod spec + config volume + TCP probes
└── service.yaml # NodePort (default) / ClusterIP / LoadBalancerRunning as an Ingress Controller
The gateway can act as a Kubernetes ingress controller by enabling the
kubernetes provider. It watches networking.k8s.io/v1 Ingress resources
and synthesizes routers/services that are merged into the running config (static
config always wins on a name collision). This requires the image built with the
kube feature (the published ghcr.io image qualifies).
Provider configuration
providers {
kubernetes {
namespace = "" # "" = all namespaces; else a single namespace
label_selector = "app=my-service" # optional; applied to the Ingress list
watch_interval_secs = 30 # poll interval (default 30)
ingress_route_crd = false # also watch IngressRoute ConfigMaps (default false)
}
}| Key | Default | Notes |
|---|---|---|
namespace | "" (all namespaces) | Empty = cluster-wide watch; otherwise namespaced. |
label_selector | "" (no filter) | Only applied to the Ingress list, not the IngressRoute watcher. |
watch_interval_secs | 30 | Both the Ingress and IngressRoute watchers poll on this interval. On failure they rebuild the kube client and back off exponentially (1s up to max(interval, 30s)). |
ingress_route_crd | false | When true, also spawns the Traefik-style IngressRoute watcher (see below). |
Without the
kubefeature, aproviders.kubernetesblock does nothing but log a warning. Use theghcr.ioimage or a--features kubebuild.
Annotations
Generated routers/services are tuned via these annotations on the Ingress
object. All use the a3s-gateway.io/ prefix and are comma-separated lists where
applicable:
| Annotation | Effect | Default |
|---|---|---|
a3s-gateway.io/entrypoints | Entrypoint names the router binds to | empty (all entrypoints) |
a3s-gateway.io/middlewares | Ordered middleware chain | empty |
a3s-gateway.io/strategy | LB strategy: round-robin / weighted / least-connections / random | round-robin (invalid → round-robin) |
a3s-gateway.io/priority | Router priority (i32); higher wins (Traefik-style) | 0 |
a3s-gateway.io/protocol | tcp or udp generate a stream entrypoint and skip the HTTP router; anything else → HTTP | http |
a3s-gateway.io/listen | Listen address for tcp/udp protocol entrypoints | none (no entrypoint created if absent) |
a3s-gateway.io/request-timeout | Per-route upstream timeout (humantime, e.g. 600s) on the generated service | 30s |
Routing rules are derived from the Ingress spec: a non-empty host becomes
Host(`h`); pathType: Exact becomes Path(`p`) while any other
pathType becomes PathPrefix(`p`); a / or empty path is dropped (host-only
rule, or a catch-all PathPrefix(`/`) when nothing else is present).
Backends resolve to the in-cluster DNS name
http://{svc}.{namespace}.svc.cluster.local:{port} (port from
backend.service.port.number, else 80). Generated names are
{namespace}-{ingressName}-{backendServiceName}. As of v1.0.5 the provider
hashes router+service content, so in-place Ingress edits are picked up (not
just adds/removes).
a3s-gateway.io/typeis not an Ingress annotation. It is the fixed ConfigMap label selector (a3s-gateway.io/type=ingressroute) used by the IngressRoute watcher. Do not place it on anIngress.
IngressRoute (Traefik-style)
Setting ingress_route_crd = true enables a second watcher. Note that in v1.0.11
this does not read an installed CRD — it lists ConfigMaps labeled
a3s-gateway.io/type=ingressroute and parses each one's .data.spec (JSON) as
an IngressRoute (entrypoints, routes with match/priority/middlewares/
services, and optional TLS secret). The watcher honors namespace but ignores
label_selector (it always uses the fixed a3s-gateway.io/type=ingressroute
selector) and has no change-detection hash, so it republishes every interval.
RBAC
To watch Ingress resources cluster-wide (and ConfigMaps when
ingress_route_crd = true), the gateway's ServiceAccount needs read/watch
access. The bundled chart does not template RBAC, so apply it alongside the
release:
apiVersion: v1
kind: ServiceAccount
metadata:
name: a3s-gateway
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: a3s-gateway
rules:
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch"]
# Only required when ingress_route_crd = true
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: a3s-gateway
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: a3s-gateway
subjects:
- kind: ServiceAccount
name: a3s-gateway
namespace: defaultScope the
ClusterRoleto aRole/RoleBindingin a single namespace if you setproviders.kubernetes.namespaceto a specific namespace.
Management API / Dashboard
The optional management listener is a dedicated, separate TCP socket
(management {} block; there is no separate dashboard block). It never
intercepts user traffic. It is disabled by default.
management {
enabled = true
address = "127.0.0.1:9090" # default
path_prefix = "/api/gateway" # default; must start with "/"
auth_token_env = "A3S_GATEWAY_ADMIN_TOKEN" # env var NAME holding the bearer token
allowed_ips = ["127.0.0.1", "::1"] # default when key omitted
# Optional TLS / mTLS for the management listener:
tls {
cert_file = "/etc/gateway/tls/mgmt.crt"
key_file = "/etc/gateway/tls/mgmt.key"
client_ca_file = "/etc/gateway/tls/client-ca.crt" # required if require_client_cert
require_client_cert = true # enforce mTLS
min_version = "1.3" # exactly "1.2" or "1.3"
}
}Behavioral notes:
auth_token_envholds the name of an env var, not the token. If that env var is unset at startup the management listener fails to start. Settingauth_token_env = ""disables bearer auth entirely.allowed_ipsdefaults to loopback only when the key is omitted. Providing the key replaces the default; an explicitallowed_ips = []disables IP filtering (allow all).- Requests are checked in order: path-prefix → IP allowlist → bearer token.
management.tls.min_versionis validated to be exactly"1.2"or"1.3"(unlike entrypoint TLS, which is not validated at config time).
Endpoints (all under {path_prefix}): GET health, GET metrics (Prometheus
text), GET config, GET routes / routes/{name}, GET services /
services/{name}, GET backends, GET events (?limit=N, default 100, max
500), GET version, plus the two mutating endpoints POST config/validate and
POST config/reload (request body capped at 1 MiB).
# Set the token the env var refers to, then hit the API
export A3S_GATEWAY_ADMIN_TOKEN="$(openssl rand -hex 32)"
curl -H "Authorization: Bearer $A3S_GATEWAY_ADMIN_TOKEN" \
http://127.0.0.1:9090/api/gateway/healthHot reload
The gateway reloads configuration without dropping connections, driven by a
notify-based file watcher. The watcher only runs when a providers.file block
is present with watch = true (the default for that block).
providers {
file {
watch = true # default
directory = "/etc/gateway/conf.d" # optional extra dir, watched recursively
}
}- The watcher monitors the main config file's parent directory (non-recursive)
plus the optional
directory(recursive), debounced at 500 ms. - Only
.aclfiles are treated as config;.toml/.yaml/.jsonfragments in the watched dir are ignored. On reload the main file plus all sorted.aclfragments are concatenated, re-parsed, and validated. - On a parse/validate failure the previous good config is kept and an error is logged — a bad edit does not take down the running gateway.
Reload strategy is chosen automatically:
- Hot-swap (no rebind). If
old.entrypoints == new.entrypointsand neither side has any UDP entrypoint, the runtime state is swapped in place without touching sockets ("Runtime state hot-swapped without rebinding ports"). - Incremental restart. If only some HTTP/TCP entrypoints changed (and no UDP), only the added/changed listeners are rebound; unchanged listeners keep running.
- Full restart. Any UDP entrypoint, or larger changes, force a full abort-and-rebind of all listeners.
You can also drive a reload remotely through the management API:
a3s-gateway management reload \
--url http://127.0.0.1:9090/api/gateway \
--file gateway.aclWhen running under Helm, edit the config value and helm upgrade. Because the
config is mounted from a ConfigMap, you must trigger the pods to pick up the new
file (e.g. a rolling restart, or rely on Kubernetes' eventual ConfigMap
projection plus the in-pod file watcher).
TLS notes for deployments
- Entrypoint TLS terminates with rustls (ring provider), ALPN
["h2","http/1.1"].min_versionaccepts"1.2"(TLS 1.2 and 1.3) or"1.3"(1.3 only); it is a minimum, not an exact version. - The
tls { acme = true }flag enables the ACME manager's 12h detection/renewal loop, but in v1.0.11 issued certificates are not hot-swapped into the live acceptor and the HTTP-01 challenge endpoint is not served on the data plane. Treat config-driven ACME as not end-to-end automatic HTTPS; for production TLS, provision staticcert_file/key_filePEMs (e.g. from cert-manager or your CA) and let the file watcher pick up rotations. DNS-01 / Cloudflare / Route53 ACME is not reachable from the ACL schema.
Cargo features
# default = [] (no features)
# redis -> distributed rate limiting (rate-limit-redis middleware)
# kube -> Kubernetes Ingress / IngressRoute controllerPublished Linux binaries and ghcr.io/a3s-lab/gateway images are built with
kube + redis enabled (the v1.0.1 release fixed earlier Linux artifacts that
shipped without them). Homebrew and a plain cargo install produce a
default-feature build unless you pass --features kube,redis.