Running Abbenay in a Container
July 16, 2026 · View on GitHub
This guide covers building and running Abbenay as a container image using
the provided Containerfile.
Pre-built images from GHCR
Multi-arch images (linux/amd64 and linux/arm64) are published
automatically to the GitHub Container Registry on every merge to main
and on every release tag.
# Latest stable release
podman pull ghcr.io/redhat-developer/abbenay:latest
# Latest from main (development builds)
podman pull ghcr.io/redhat-developer/abbenay:main
# Specific release
podman pull ghcr.io/redhat-developer/abbenay:v1.0.0
Then run it the same way as a locally built image (see Running below).
Overview
The container packages the Abbenay SEA binary into a minimal UBI 9 image.
The start command runs all services:
| Service | Endpoint |
|---|---|
| Web dashboard | http://localhost:8787 |
| REST API | http://localhost:8787/api/* |
| OpenAI-compatible API | http://localhost:8787/v1/chat/completions |
| MCP server | http://localhost:8787/mcp (when --mcp is passed) |
| gRPC (TCP) | localhost:50051 (for Python/programmatic clients) |
What's different from bare-metal
- No keytar / keychain. The container has no D-Bus session or
gnome-keyring, so
api_key_keychain_namewill not work. Useapi_key_env_var_namein your config and pass keys as environment variables. - Config is mounted, not baked in. Bind-mount your
config.yamlinto the container at runtime.
Building the image locally
If you prefer to build from source instead of pulling from GHCR:
podman build -f Containerfile -t abbenay:latest .
The multi-stage build compiles the SEA binary in a Node.js builder stage
and copies only the binary and its sidecars (proto/, static/) into a
UBI 9 minimal runtime image. The final image contains no Node.js, npm,
or build tooling.
Configuration
Create a config.yaml that uses api_key_env_var_name instead of
api_key_keychain_name. A ready-to-use example is provided at
config.container.example.yaml
in the repo root.
Minimal example:
providers:
openrouter:
engine: openrouter
api_key_env_var_name: "OPENROUTER_API_KEY"
models:
anthropic/claude-sonnet-4: {}
anthropic/claude-haiku-3.5: {}
Mount this file into the container at:
/home/abbenay/.config/abbenay/config.yaml
Running
podman run -d --name abbenay \
-v ./config.yaml:/home/abbenay/.config/abbenay/config.yaml:ro \
-e OPENROUTER_API_KEY=sk-or-... \
-e ABBENAY_API_TOKEN=change-me \
-p 8787:8787 \
-p 50051:50051 \
abbenay:latest
With consumer authentication (for programmatic clients like APME):
podman run -d --name abbenay \
-v ./config.yaml:/home/abbenay/.config/abbenay/config.yaml:ro \
-e OPENROUTER_API_KEY=sk-or-... \
-e ABBENAY_API_TOKEN=change-me \
-e APME_TOKEN=secret123 \
-p 8787:8787 \
-p 50051:50051 \
abbenay:latest
Verify it's running
curl -H "Authorization: Bearer $ABBENAY_API_TOKEN" http://127.0.0.1:8787/api/health
Set ABBENAY_API_TOKEN in the container environment (required for the
built-in healthcheck and all HTTP routes).
View logs
podman logs -f abbenay
Overriding the command
The default CMD is
start --port 8787 --host 0.0.0.0 --grpc-port 50051 --grpc-host 0.0.0.0 --grpc-tls,
which runs all services with HTTP and TLS-protected gRPC accessible from outside the
container. You can override it to run a subset:
# Web dashboard and REST API only
podman run -d -p 8787:8787 \
-v ./config.yaml:/home/abbenay/.config/abbenay/config.yaml:ro \
-e OPENROUTER_API_KEY=sk-or-... \
abbenay:latest web --port 8787
# OpenAI-compatible API only
podman run -d -p 8787:8787 \
-v ./config.yaml:/home/abbenay/.config/abbenay/config.yaml:ro \
-e OPENROUTER_API_KEY=sk-or-... \
abbenay:latest serve --port 8787
# gRPC daemon only (TLS required for 0.0.0.0)
podman run -d -p 50051:50051 \
-v ./config.yaml:/home/abbenay/.config/abbenay/config.yaml:ro \
-e OPENROUTER_API_KEY=sk-or-... \
abbenay:latest daemon --grpc-port 50051 --grpc-host 0.0.0.0 --grpc-tls
Binding to
0.0.0.0without--grpc-tlsand without--insecureis refused at startup.
Multiple providers
Pass one environment variable per provider key:
podman run -d --name abbenay \
-v ./config.yaml:/home/abbenay/.config/abbenay/config.yaml:ro \
-e OPENROUTER_API_KEY=sk-or-... \
-e ANTHROPIC_API_KEY=sk-ant-... \
-e OPENAI_API_KEY=sk-... \
-p 8787:8787 \
abbenay:latest
Your config.yaml references these by name:
providers:
openrouter:
engine: openrouter
api_key_env_var_name: "OPENROUTER_API_KEY"
models: { ... }
anthropic:
engine: anthropic
api_key_env_var_name: "ANTHROPIC_API_KEY"
models: { ... }
openai:
engine: openai
api_key_env_var_name: "OPENAI_API_KEY"
models: { ... }
Python gRPC client
When the daemon runs in a container with --grpc-tls (the default image CMD),
the Python client must trust the daemon CA:
from abbenay_grpc import AbbenayClient
# Copy ca.crt out of the container (runtime tls/ dir) or mount it, then:
async with AbbenayClient(
host="localhost",
port=50051,
tls=True,
ca_cert="/path/to/ca.crt",
) as client:
async for chunk in client.chat("openrouter/anthropic/claude-sonnet-4", "Hello!"):
if chunk.text:
print(chunk.text, end="")
If you intentionally start the container with --insecure instead of
--grpc-tls, omit tls / ca_cert (plaintext TCP — not recommended).
The is_daemon_running() and get_daemon_pid() convenience methods
check the local filesystem and do not apply to remote connections. Use
health_check() instead:
client = AbbenayClient(host="container-host", port=50051, tls=True, ca_cert="/path/to/ca.crt")
await client.connect()
healthy = await client.health_check()
Kubernetes / OpenShift
Deployment with mounted Secret
apiVersion: v1
kind: Secret
metadata:
name: abbenay-keys
type: Opaque
stringData:
OPENROUTER_API_KEY: "sk-or-..."
# Must match the Bearer value in the probe httpHeaders below.
# Kubernetes does not expand env vars in httpGet.httpHeaders.
ABBENAY_API_TOKEN: "replace-with-a-strong-token"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: abbenay-config
data:
config.yaml: |
providers:
openrouter:
engine: openrouter
api_key_env_var_name: "OPENROUTER_API_KEY"
models:
anthropic/claude-sonnet-4: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: abbenay
spec:
replicas: 1
selector:
matchLabels:
app: abbenay
template:
metadata:
labels:
app: abbenay
spec:
containers:
- name: abbenay
image: abbenay:latest
ports:
- containerPort: 8787
name: http
- containerPort: 50051
name: grpc
envFrom:
- secretRef:
name: abbenay-keys
volumeMounts:
- name: config
mountPath: /home/abbenay/.config/abbenay/config.yaml
subPath: config.yaml
readOnly: true
livenessProbe:
httpGet:
path: /api/health
port: 8787
httpHeaders:
- name: Authorization
value: Bearer replace-with-a-strong-token
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /api/health
port: 8787
httpHeaders:
- name: Authorization
value: Bearer replace-with-a-strong-token
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: config
configMap:
name: abbenay-config
---
apiVersion: v1
kind: Service
metadata:
name: abbenay
spec:
selector:
app: abbenay
ports:
- name: http
port: 8787
targetPort: 8787
- name: grpc
port: 50051
targetPort: 50051
Notes
- The image runs as non-root user
abbenay(UID 1001), compatible with OpenShift's restricted SCC. - The built-in
HEALTHCHECKusescurlagainst/api/healthwithAuthorization: Bearer ${ABBENAY_API_TOKEN}. Set that env var when running the container. In Kubernetes, the samplelivenessProbe/readinessProbesend the same Bearer token viahttpHeaders— keep that value identical toABBENAY_API_TOKENin the Secret (Kubernetes does not expand environment variables inhttpGet.httpHeaders). - Sessions are ephemeral by default. To persist sessions across restarts,
mount a volume at
/home/abbenay/.local/share/abbenay/sessions/.
Security: HTTP bind and authentication
| Flag | Default | Effect |
|---|---|---|
--host / ABBENAY_HTTP_HOST / server.host | 127.0.0.1 | HTTP bind (dashboard, /api/*, /v1/*, /mcp) |
| Value | Effect |
|---|---|
127.0.0.1 (default) | Loopback only — safe for local development |
0.0.0.0 | All interfaces — required inside containers so published ports are reachable |
The container's default CMD uses --host 0.0.0.0 because container
networking requires listeners to accept connections from outside the
container's network namespace. The daemon logs a warning when HTTP is bound
beyond loopback.
HTTP authentication is on by default. Set ABBENAY_API_TOKEN (or
server.api_token / server.api_token_env) and pass
Authorization: Bearer <token> on every request. CORS is allowlist-only
(never *).
WARNING:
ABBENAY_HTTP_AUTH=0disables HTTP auth for local development only. Combining it with--host 0.0.0.0(or any non-loopback bind) fails closed — the HTTP server refuses to start.
When exposing HTTP, always set a strong ABBENAY_API_TOKEN and restrict
server.cors_origins. Keep ABBENAY_HTTP_AUTH enabled (the default).
Security: gRPC bind, TLS, and --insecure
The --grpc-host flag controls which network interface the TCP gRPC
listener binds to. Non-loopback binds fail closed unless TLS is enabled
or --insecure is set explicitly.
| Value | Effect |
|---|---|
127.0.0.1 (default) | Loopback only — plaintext allowed for local development |
0.0.0.0 / non-loopback | Requires --grpc-tls or --insecure |
Flags
| Flag | Purpose |
|---|---|
--grpc-tls | Enable TLS; auto-generates self-signed certs |
--insecure | Allow plaintext on non-loopback binds (escape hatch; not recommended) |
Auto-generated certificates
With --grpc-tls, the daemon writes:
<runtime-dir>/tls/server.crt<runtime-dir>/tls/server.key(mode 0600)<runtime-dir>/tls/ca.crt(same as server cert — trust anchor for clients)
The certificate CN / default SSL target name is abbenay-grpc. Clients must
trust ca.crt (and typically override the SSL target name to abbenay-grpc
when connecting by IP).
Client trust
- Python:
AbbenayClient(host=..., tls=True, ca_cert=".../ca.crt") - grpc-web-control: pass
tls: trueandcaPathfor TCP targets - Unix socket: remains plaintext local IPC (no TLS required)
Insecure tradeoffs
--insecure on 0.0.0.0 restores the old plaintext behavior. API keys, chat,
provider config, and tools travel unencrypted. Prefer --grpc-tls. Always
configure a consumers section in config.yaml when exposing gRPC beyond a
trusted network.