-
-
Notifications
You must be signed in to change notification settings - Fork 0
Deployment
Nenya supports three deployment targets: bare metal (systemd), container (Podman/Docker), and Kubernetes (Helm).
The install script sets up the binary, example config, and systemd unit:
# Latest version
curl -fsSL https://raw.githubusercontent.com/gumieri/nenya/main/install.sh | sudo sh
# Pinned version
curl -fsSL https://raw.githubusercontent.com/gumieri/nenya/main/install.sh | sudo sh -s -- -v 0.1.0
# Dry run (audit before installing)
curl -fsSL https://raw.githubusercontent.com/gumieri/nenya/main/install.sh | sh -s -- --dry-runOr build from source:
git clone https://github.com/gumieri/nenya.git
cd nenya
go build -o nenya ./cmd/nenya/Nenya supports two configuration modes:
Directory mode (default):
/etc/nenya/
├── config.json # single config file (mutually exclusive with config.d/)
├── config.d/ # directory mode (takes priority over config.json)
│ ├── 00-server.json # server, governance, bouncer, compaction
│ ├── 10-providers.json # provider URL or auth overrides
│ └── 20-agents.json # agent definitions with fallback chains
└── secrets.json # loaded via systemd credentials
Single file mode:
./nenya --config /path/to/config.jsonEnvironment variables:
NENYA_CONFIG_DIR=/etc/nenya ./nenya # directory mode
NENYA_CONFIG_FILE=/path/to/config.json ./nenya # single file mode| Field Type | Behavior |
|---|---|
agents (map) |
Per-key merge — later files add or override individual agents |
providers (map) |
Per-key merge — later files add or override individual providers |
mcp_servers (map) |
Per-key merge |
server, governance, bouncer, etc. (struct) |
Last file wins |
Note: config.d/ and config.json are mutually exclusive — if config.d/ exists and is non-empty, config.json is ignored.
The service file is installed at /etc/systemd/system/nenya.service:
[Unit]
Description=Nenya AI Gateway & Bouncer
Requires=nenya.socket
After=nenya.socket
[Service]
Type=simple
DynamicUser=yes
ExecStart=/usr/bin/nenya
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
# Capability and privilege restrictions
NoNewPrivileges=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
RestrictNamespaces=yes
# Filesystem and process isolation
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
RemoveIPC=yes
UMask=0077
# Network restrictions
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
# Memory protection
MemoryDenyWriteExecute=yes
# Allow mlock for secure memory (tokens stored in locked RAM)
LimitMEMLOCK=infinity
# Disable core dumps to prevent locked memory from reaching disk
LimitCORE=0
# Syscall filtering (seccomp)
SystemCallFilter=@system-service
SystemCallFilter=~@mount @privileged @raw-io @reboot @swap
# Secrets loaded via systemd credential
LoadCredential=secrets:/etc/nenya/secrets.json
[Install]
WantedBy=multi-user.targetNenya supports systemd socket activation for zero-downtime restarts:
# /etc/systemd/system/nenya.socket
[Unit]
Description=Nenya AI Gateway Socket
[Socket]
ListenStream=8080
Accept=no
[Install]
WantedBy=sockets.targetEnable and start:
sudo systemctl enable --now nenya.socket
sudo systemctl enable --now nenya.serviceThe socket holds the listening port (8080) and automatically activates nenya.service on the first connection. When the service restarts, connections queue in the socket and the new process inherits the file descriptor — no dropped requests.
# Single file
sudo mkdir -p /etc/nenya
sudo tee /etc/nenya/secrets.json << 'EOF'
{
"client_token": "nk-$(openssl rand -hex 32)",
"provider_keys": { "gemini": "AIza...", "deepseek": "sk-..." }
}
EOF
sudo chmod 600 /etc/nenya/secrets.json
# Or directory with multiple files (auto-merged)
sudo mkdir -p /etc/nenya/secrets.d
sudo tee /etc/nenya/secrets.d/01-client.json << 'EOF'
{"client_token": "nk-$(openssl rand -hex 32)"}
EOF
sudo tee /etc/nenya/secrets.d/02-providers.json << 'EOF'
{"provider_keys": {"gemini": "AIza...", "deepseek": "sk-..."}}
EOF
sudo chmod 600 /etc/nenya/secrets.d/*.jsonSee Secrets for full documentation.
systemctl reload nenya- Reloads config from the same path used at startup
- Re-discovers model catalogs from all configured providers
- Validates config structure (patterns, enums) but does not ping providers
- Preserves UsageTracker, Metrics, and ThoughtSignatureCache across reloads
- On validation failure: logs error, continues serving with old config
- In-flight requests complete with the gateway they started with
journalctl -u nenya -fEnable debug logging:
# In config
# "server": { "log_level": "debug" }
# Or via -verbose flag in systemd unit
# ExecStart=/usr/bin/nenya -verbose# Check service status
systemctl status nenya
# Test health endpoint
curl http://localhost:8080/healthz
# Test chat request
curl -H "Authorization: Bearer $(jq -r '.client_token' /etc/nenya/secrets.json)" \
-H "Content-Type: application/json" \
-d '{"model":"gemini-3-flash","messages":[{"role":"user","content":"Hello!"}]}' \
http://localhost:8080/v1/chat/completionsContainer image is available on GitHub Container Registry with multi-arch support (amd64/arm64):
ghcr.io/gumieri/nenya:latest
Create minimal config and secrets:
mkdir -p config secrets
cat > config/config.json << 'EOF'
{
"server": { "listen_addr": ":8080" },
"agents": {
"default": {
"strategy": "fallback",
"models": ["gemini-3-flash"]
}
}
}
EOF
cat > secrets/client.json << 'EOF'
{"client_token": "nk-$(openssl rand -hex 32)"}
EOF
cat > secrets/provider_keys.json << 'EOF'
{"provider_keys": {"gemini": "AIza..."}}
EOFpodman run -d \
--name nenya \
-p 8080:8080 \
-v ./config:/etc/nenya:ro \
-v ./secrets:/run/secrets/nenya:ro \
-e NENYA_SECRETS_DIR=/run/secrets/nenya \
--cap-drop=ALL \
--cap-add=IPC_LOCK \
--security-opt=no-new-privileges:true \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64M \
ghcr.io/gumieri/nenya:latestUse the provided deploy/compose.yml:
mkdir -p config secrets
podman compose -f deploy/compose.yml up -dOr with Docker:
docker compose -f deploy/compose.yml up -dservices:
nenya:
image: ghcr.io/gumieri/nenya:latest
container_name: nenya
ports:
- "8080:8080"
volumes:
- ./config:/etc/nenya:ro # read-only config
- ./secrets:/run/secrets/nenya:ro # read-only secrets
environment:
NENYA_SECRETS_DIR: /run/secrets/nenya
cap_drop:
- ALL
cap_add:
- IPC_LOCK # required for secure memory (mlock)
security_opt:
- no-new-privileges:true # prevent privilege escalation
read_only: true # immutable root filesystem
tmpfs:
- /tmp:rw,noexec,nosuid,size=64M # tmpfs for /tmp
restart: unless-stopped| Option | Purpose |
|---|---|
--cap-drop=ALL --cap-add=IPC_LOCK |
Required for secure memory (mlock) |
--security-opt=no-new-privileges:true |
Prevents privilege escalation |
--read-only |
Enforces immutable root filesystem |
--tmpfs /tmp |
tmpfs for /tmp to prevent writes |
| Non-root user (UID 65532) | Container runs as non-root |
Important: IPC_LOCK capability is required for secure memory storage. Without it, Nenya will fail to start with ErrMLockFailure.
| Variable | Description |
|---|---|
NENYA_CONFIG_DIR |
Config root directory (default: /etc/nenya/) |
NENYA_CONFIG_FILE |
Single config file (overrides NENYA_CONFIG_DIR) |
NENYA_SECRETS_DIR |
Secrets directory for merged *.json files |
Verify the signed image with cosign:
# Verify image signature
cosign verify ghcr.io/gumieri/nenya:latest
# Verify SBOM attestation
cosign verify-attestation --type spdx ghcr.io/gumieri/nenya:latestpodman pull ghcr.io/gumieri/nenya:latest
podman stop nenya && podman rm nenya
# Then re-run the podman run command aboveOr with compose:
podman compose pull
podman compose up -d --force-recreateThe Helm chart provides the following components:
| Component | Description |
|---|---|
| Deployment | Security-hardened with non-root user, read-only fs, tmpfs |
| Service | ClusterIP (default) |
| ConfigMap | Optional: inline config files |
| Secret | Optional: inline secrets (stringData) |
| Ingress | Optional: ingress with TLS |
Key security features: non-root user (UID 65532), read-only root filesystem, dropped capabilities (ALL) with only IPC_LOCK added, tmpfs at /tmp.
- Kubernetes 1.19+
- Helm 3.8+
- kubectl configured with cluster access
kubectl create namespace nenya# From individual files
kubectl create secret generic nenya-secrets \
--from-file=provider_keys.json=secrets/provider_keys.json \
--from-file=client.json=secrets/client.json \
-n nenya
# From literal values
kubectl create secret generic nenya-secrets \
--from-literal=provider_keys.json='{"provider_keys":{"gemini":"AIza..."}}' \
--from-literal=client.json='{"client_token":"nk-..."}' \
-n nenya# From config directory
kubectl create configmap nenya-config \
--from-file=config.json=config/config.json \
-n nenya
# From literal
kubectl create configmap nenya-config \
--from-literal=config.json='{"server":{"listen_addr":":8080"},"agents":{"default":{"strategy":"fallback","models":["gemini-3-flash"]}}}' \
-n nenya# View all configurable options
helm show values ./deploy/chart/nenya
# Install
helm install nenya ./deploy/chart/nenya \
--namespace nenya \
--set secrets.existingSecret=nenya-secrets \
--set config.existingConfigMap=nenya-configBasic:
# values.yaml
replicaCount: 1
image:
repository: ghcr.io/gumieri/nenya
pullPolicy: IfNotPresent
tag: "latest"
secrets:
existingSecret: nenya-secrets
config:
existingConfigMap: nenya-config
service:
type: ClusterIP
port: 8080High availability:
# values.yaml
replicaCount: 3
service:
type: LoadBalancer
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: nenyaWith ingress:
# values.yaml
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: nenya.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: nenya-tls
hosts:
- nenya.example.comInstall with custom values:
helm install nenya ./deploy/chart/nenya \
--namespace nenya \
--values values.yaml \
--set secrets.existingSecret=nenya-secrets \
--set config.existingConfigMap=nenya-configGenerate manifests without Helm:
helm template nenya ./deploy/chart/nenya \
--namespace nenya \
--set secrets.existingSecret=nenya-secrets \
--set config.existingConfigMap=nenya-config \
> nenya-manifests.yaml
kubectl apply -f nenya-manifests.yamlkubectl get pods -n nenya
kubectl get svc -n nenya
# Port forward to test
kubectl port-forward -n nenya svc/nenya 8080:8080
curl http://localhost:8080/healthzhelm upgrade nenya ./deploy/chart/nenya \
--namespace nenya \
--set secrets.existingSecret=nenya-secrets \
--set config.existingConfigMap=nenya-confighelm uninstall nenya -n nenyagit clone https://github.com/gumieri/nenya.git
cd nenya
go build -o nenya ./cmd/nenya/
sudo cp nenya /usr/local/bin/<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.github.gumieri.nenya</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/nenya</string>
<string>-config</string>
<string>/usr/local/etc/nenya</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>5</integer>
<key>WorkingDirectory</key>
<string>/usr/local/etc/nenya</string>
<key>EnvironmentVariables</key>
<dict>
<key>CREDENTIALS_DIRECTORY</key>
<string>/usr/local/etc/nenya/secrets.d</string>
</dict>
<key>SoftResourceLimits</key>
<dict>
<key>Memory</key>
<integer>1073741824</integer>
</dict>
</dict>
</plist>Install and load:
sudo cp com.github.gumieri.nenya.plist /Library/LaunchDaemons/
sudo chown root:wheel /Library/LaunchDaemons/com.github.gumieri.nenya.plist
sudo launchctl load /Library/LaunchDaemons/com.github.gumieri.nenya.plistUnload/stop:
sudo launchctl unload /Library/LaunchDaemons/com.github.gumieri.nenya.plistmacOS does not support systemd credentials. Use a secrets directory with strict permissions or the macOS Keychain:
-
File-based:
chmod 600on secrets files underCREDENTIALS_DIRECTORY(see Secrets#macOS) -
Keychain: Use
security add-generic-password/security find-generic-passwordfor encrypted at-rest storage
/usr/local/etc/nenya/
├── 00-server.json # server, context, governance, bouncer, compaction
├── 10-providers.json # provider URL or auth overrides
├── 20-agents.json # agent definitions
└── secrets.d/ # loaded via CREDENTIALS_DIRECTORY
├── 01-client.json
└── 02-providers.json
- Non-root: Runs as UID 65532 with dropped capabilities (Linux containers/systemd)
-
mlock:
IPC_LOCKprevents secrets from swapping to disk (requiresLimitMEMLOCK=infinityon Linux; not available on macOS) -
Read-only root: Immutable filesystem; private
/tmp -
Seccomp: Restricted syscalls via
seccomp=unconfined(custom profile recommended; Linux only) -
No new privileges:
NoNewPrivileges=trueprevents privilege escalation (Linux systemd/containers) -
macOS hardening: Use strict file permissions (
chmod 600) or Keychain for secrets (see Secrets#macOS)
Check logs:
# systemd
journalctl -u nenya -n 50 --no-pager
# Container
podman logs nenya
# Kubernetes
kubectl describe pod -n nenya -l app.kubernetes.io/name=nenyaCommon issues:
-
ErrMLockFailure: MissingIPC_LOCK/LimitMEMLOCK=infinity - Config validation error: Check JSON syntax in config files
- Port already in use: Another service using port 8080
- Image pull issues: Check registry access and pull policy
curl http://localhost:8080/healthzExpected response: {"status":"ok","engine":"ollama"} (or similar)
- Configuration — Config reference
- Secrets — Secrets format and loading
- Quick Start — Minimal setup to run Nenya
Getting Started
- Home — Project overview
- Quick Start — Install and run in 5 minutes
- Client Setup — OpenCode, Cursor, and other clients
- Deployment — Bare metal, container, Kubernetes
Core Concepts
- Configuration — Config reference and examples
- Providers — 24 providers, capabilities, special behaviors
- Routing — Latency-aware routing and fallback chains
- Architecture — Package overview and request lifecycle
- MCP Integration — MCP server integration
Reference
- Passthrough Proxy — Raw provider endpoint proxying
- Secrets — Systemd credentials and container secrets
- Model Discovery — Dynamic model catalog fetching
- API Endpoints — Endpoint reference
- Adapters — Provider adapter system
- Billing — Billing-aware routing and quota tracking
- Caching — Exact-match and semantic caching
- Provider Capabilities — Service kinds matrix
- Unknown MaxContext — Unknown context window behavior
Operations
- Demo — Test all pipeline tiers
- Troubleshooting — Common issues and solutions
- FAQ — Frequently asked questions
- Security — Security policy and vulnerability reporting
Project
- Roadmap — Planned features
- Disclaimer — Legal disclaimer