A3S Docs
A3S Gateway

Quick Start

Install A3S Gateway (v1.0.11) and run a minimal reverse proxy to an LLM/HTTP backend

Quick Start

This guide gets A3S Gateway v1.0.11 running as a reverse proxy in front of an LLM/HTTP backend. Everything here is copy-pasteable and runs as written.

The gateway is a single statically-linked binary (a3s-gateway). Configuration is a Traefik-style entrypoint → router → middleware → service model written in ACL. Config files must use the .acl extension — HCL/TOML/YAML/JSON files are rejected at load.

Install

Pick one. Homebrew and Cargo install the native a3s-gateway binary; Docker runs the published image from GitHub Container Registry.

Homebrew (macOS)

brew install a3s-lab/tap/a3s-gateway

Cargo

cargo install a3s-gateway

Building from source requires Rust 1.88 or newer (the current MSRV).

Docker (ghcr.io)

The published Linux images build with the kube and redis cargo features enabled (Kubernetes provider + distributed Redis rate limiting). The default cargo install / brew builds enable no extra features.

docker run -d \
  -v $(pwd)/gateway.acl:/etc/a3s-gateway/gateway.acl \
  -p 8080:8080 \
  ghcr.io/a3s-lab/gateway:latest \
  --config /etc/a3s-gateway/gateway.acl

Minimal Configuration

Create a gateway.acl with one entrypoint, one router, and one service that proxies to an LLM/HTTP backend.

# gateway.acl — proxy /v1 traffic to a local LLM backend
entrypoints "web" {
  address = "0.0.0.0:8080"
}

routers "llm" {
  rule        = "PathPrefix(`/v1`)"
  service     = "llm-backend"
  entrypoints = ["web"]
}

services "llm-backend" {
  load_balancer {
    strategy        = "round-robin"
    request_timeout = "60s"
    servers = [
      { url = "http://127.0.0.1:8001" },
    ]
  }
}

How this maps to the schema:

  • entrypoints — a named network listener. address (host:port) is required; protocol defaults to http (also tcp / udp). Listening on 0.0.0.0:8080 matches the published Docker image, which exposes 8080.
  • routersrule and service are required. The rule uses backtick arguments and the && combinator (Host, Path, PathPrefix, Method, Headers). PathPrefix is a raw starts_with, so PathPrefix(/v1) matches /v1, /v1/chat/completions, and /v1xyz. An empty entrypoints list (or omitting it) binds the router to all entrypoints.
  • services — wraps a load_balancer. request_timeout defaults to "30s"; LLM responses can be slow, so this example raises it to "60s". Durations accept ms / s / m suffixes (a bare number means seconds); empty, 0s, or non-numeric values are rejected at load. A service must have at least one server (or a revision) or validation fails.

The router service and entrypoints, and each middleware name, are cross-validated at load — a reference to a name that does not exist is a hard config error.

Start the Gateway

# Start with the ACL config (defaults to ./gateway.acl if --config is omitted)
a3s-gateway --config gateway.acl

# Start with debug logging
a3s-gateway --config gateway.acl --log-level debug

# Validate the configuration without starting
a3s-gateway validate --config gateway.acl

Inspect what was parsed without binding any sockets:

a3s-gateway config --config gateway.acl summary
a3s-gateway config --config gateway.acl entrypoints
a3s-gateway config --config gateway.acl routes
a3s-gateway config --config gateway.acl services
a3s-gateway config --config gateway.acl middlewares
a3s-gateway config --config gateway.acl json

Verify

Send a request through the gateway. The router matches PathPrefix(/v1) and forwards to llm-backend, which proxies to http://127.0.0.1:8001:

curl -i http://localhost:8080/v1/models

If the backend is reachable you get its response. The gateway always adds X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto, and X-Forwarded-Port to the upstream request (this is unconditional — there are no config keys for forwarded headers).

A POST works the same way — paths and bodies stream straight through:

curl -i http://localhost:8080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"my-model","messages":[{"role":"user","content":"ping"}]}'

If no backend is healthy you get 502; an upstream that exceeds request_timeout returns 504.

Secrets via env()

Any string-valued ACL attribute can read from the environment with env("VAR"). A missing variable is a config error. This is the only function call supported in ACL values.

services "llm-backend" {
  load_balancer {
    servers = [
      { url = env("LLM_UPSTREAM_URL") },
    ]
  }
}
export LLM_UPSTREAM_URL="http://127.0.0.1:8001"
a3s-gateway --config gateway.acl

Next Steps

  • RoutingHost/Path/PathPrefix/Method/Headers matchers, priority, and most-specific-wins ordering: see Routing.
  • Middlewarerate-limit, jwt, api-key, cors, circuit-breaker, and more, attached via middlewares = [...]: see Middleware.
  • Services — load-balancing strategies, active/passive health checks, sticky sessions, failover, autoscaling, and revision traffic splitting: see Services.
  • Deployment — Helm, Docker, and systemd: see Deployment.
  • Observability & management API — Prometheus metrics and the dedicated management listener: see Observability.

On this page