---
title: "Deploy"
url: "/docs/operate/deploy/"
kind: "feature"
summary: "Run Arion in production: a systemd unit, the Docker image, Kubernetes manifests, and sizing the runtime under CPU limits."
---

> Arion site index: [/llms.txt](/llms.txt)

# Deploy

Run Arion in production: a systemd unit, the Docker image, Kubernetes manifests, and sizing the runtime under CPU limits.

Arion deploys as a single static binary (`arion`) plus one YAML configuration file — no sidecars, no state on disk. This page shows the three common ways to run it: a systemd service, the Docker image, and Kubernetes.

## Run under systemd

{% <variant mode="standalone" label="Standalone"> %}
Install the binary and configuration, then let systemd supervise it:

```text,name=/etc/systemd/system/arion.service
[Unit]
Description=Arion gateway
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/arion --config /etc/arion/arion.yaml
Restart=on-failure
User=arion
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target
```

```bash
systemctl daemon-reload && systemctl enable --now arion
```

Arion logs to stdout, so `journalctl -u arion` shows the [application log](@/docs/observe/logging.md). Raise `LimitNOFILE` deliberately: every proxied connection consumes file descriptors on both sides. Configuration changes require a restart unless you use an [xDS control plane](@/docs/operate/xds-control-planes.md).
{% </variant> %}

## Run in Docker

The repository ships a `docker/Dockerfile` that builds the binary and bakes in the sample configuration at `/etc/arion/arion-runtime.yaml`, listening on 8000 (HTTP) and 8001 (TCP):

```bash
git submodule update --init --recursive
docker build -t arion-proxy -f docker/Dockerfile .
docker run -d -p 8000:8000 --name arion-proxy arion-proxy
```

Mount your own file over the baked-in one to use a real configuration:

```bash
docker run -d -p 8080:8080 \
  -v $(pwd)/arion.yaml:/etc/arion/arion-runtime.yaml \
  -e ARION_CPU_LIMIT=2 \
  --name arion-proxy arion-proxy
```

The entrypoint substitutes the literal string `CONTROL_PLANE_IP` in the config with the `CONTROL_PLANE_IP` environment variable, which is a convenient hook for pointing at an xDS server (port 50051 in the samples). Two things to know about the stock image: there is no published registry image yet, so build and push your own; and the Dockerfile builds without optional Cargo features, so add `--features metrics,prometheus,access-log,...` to its `cargo build` line for the observability endpoints you want.

## Run on Kubernetes

{% <variant mode="kubernetes" label="Kubernetes"> %}
For Gateway API management, install the [Arion control plane](@/docs/operate/arion-control-plane.md) using its [Helm chart](https://github.com/arion-gateway/arion-ctl/blob/main/charts/arion-ctl/README.md). The controller translates Gateway and route resources into proxy configuration and can provision the proxy workloads, or use proxies you manage yourself.

For a proxy managed directly, use a ConfigMap and Deployment. Ship the config as a ConfigMap, mount it, and probe readiness through the [admin interface](@/docs/observe/admin-interface.md):

```yaml,name=arion-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: arion
spec:
  replicas: 2
  selector: { matchLabels: { app: arion } }
  template:
    metadata: { labels: { app: arion } }
    spec:
      containers:
        - name: arion
          image: registry.example.com/arion-proxy:latest
          args: ["--config", "/etc/arion/arion.yaml"]
          ports:
            - containerPort: 8080
            - containerPort: 9901
          resources:
            requests: { cpu: "1", memory: 256Mi }
            limits: { cpu: "2", memory: 512Mi }
          readinessProbe:
            httpGet: { path: /ready, port: 9901 }
          volumeMounts:
            - { name: conf, mountPath: /etc/arion }
      volumes:
        - name: conf
          configMap: { name: arion-config }
```

Create the ConfigMap with `kubectl create configmap arion-config --from-file=arion.yaml` and apply the Deployment with `kubectl apply -f arion-deploy.yaml`. The readiness probe requires the `admin` block in your bootstrap; keep port 9901 off any Service you expose.
{% </variant> %}

## CPU limits and runtime sizing

Arion sizes its worker threads from the CPUs it believes it has. On Linux it reads the cgroup CPU quota first — v2 `cpu.max`, then v1 `cfs_quota_us`/`cfs_period_us`, rounding fractional quotas up — and only falls back to the host's CPU count when no quota is set. A container limited to `cpu: 2` therefore gets a two-thread proxy runtime, not one sized to the node.

When the cgroup files are not readable (restricted runtimes, unusual sandboxes), set the thread count explicitly with the `ARION_CPU_LIMIT` environment variable or `runtime.num_cpus` in the configuration, matching your CPU limit. The environment variable wins over the file. The remaining runtime knobs are covered in [performance tuning](@/docs/operate/performance-tuning.md).

## Verify

```bash
curl -i http://<host>:9901/ready    # 200 LIVE once the runtimes are up
curl -i http://<host>:8080/         # traffic through your listener
```

A 503 from `/ready` means the runtimes are still coming up. Connection refused means the process exited (a configuration it cannot honor stops it with an error) or the `admin` block is missing — either way, the log has the answer.
