Skip to content
v0.8.4stable

Install and deploy

This guide is for deployers. If somebody has already given you a Gateway Key, go to Use an existing Gateway Key. For listener, timeout, TLS, logging, and security settings, see Configuration reference.

Choose an installation method
EnvironmentBest forMethod
DockerOne host, NAS, development, or an existing container hostOfficial multi-architecture image; run init explicitly once
KubernetesExisting cluster and persistent volumesOne initialization Job, then one-replica Deployment
macOSLocal evaluation and developmentDownload the darwin-arm64 or darwin-amd64 release
LinuxBare metal or VMDownload linux-arm64 or linux-amd64; optionally manage with systemd
WindowsWindows 10/11 or Windows ServerLinux container; WSL2 may follow the Linux procedure

Commands below pin the current GitHub release, v0.8.4. Production container deployments should pin the reviewed image digest.

Halro is one Go binary with an Admin Console; it needs no PostgreSQL, Redis, or separate frontend. Preserve three things separately:

  • config.yaml: listeners, TLS, storage locations, and security boundaries;
  • the data directory: database, Ledger, Audit chain, and Usage files;
  • the Master Key: required to decrypt Provider credentials, excluded from encrypted backups, and stored separately from both data and Backup Keys.

Initialization, doctor, backup, and restore are offline commands: stop Halro first. Never copy only halro.db, and never combine database, Ledger, Audit, or Usage directories from different points in time.

Defaults bind only to 127.0.0.1. For remote access, preferably terminate TLS at Caddy, Traefik, or nginx while Halro remains on loopback. Never expose plaintext Gateway, Admin, or Metrics listeners to the public Internet.

On Apple Silicon:

Terminal window
VERSION=v0.8.4
ARTIFACT=halro-darwin-arm64.tar.gz
BASE_URL="https://github.com/akz142857/Halro/releases/download/$VERSION"
curl -fLO "$BASE_URL/$ARTIFACT"
curl -fLO "$BASE_URL/$ARTIFACT.sigstore.json"
curl -fLO "$BASE_URL/checksums.txt"
curl -fLO "$BASE_URL/checksums.txt.sigstore.json"

Install cosign 2.2 or newer, then verify the checksum list’s release identity, the archive digest, and the archive bundle:

Terminal window
COSIGN_IDENTITY="https://github.com/akz142857/Halro/.github/workflows/release.yml@refs/heads/main"
COSIGN_ISSUER="https://token.actions.githubusercontent.com"
cosign verify-blob --certificate-identity "$COSIGN_IDENTITY" --certificate-oidc-issuer "$COSIGN_ISSUER" --bundle checksums.txt.sigstore.json checksums.txt
grep "[[:space:]]$ARTIFACT$" checksums.txt | shasum -a 256 --check
cosign verify-blob --certificate-identity "$COSIGN_IDENTITY" --certificate-oidc-issuer "$COSIGN_ISSUER" --bundle "$ARTIFACT.sigstore.json" "$ARTIFACT"
tar -xzf "$ARTIFACT"
cd halro-darwin-arm64
./halro version
./halro start --config ./config.yaml

On Intel, replace arm64 with amd64. start creates a loopback-only configuration, Master Key, and data directory when absent. Open http://127.0.0.1:8081/admin/setup. Only after all verification passes, clear a blocking quarantine attribute with xattr -d com.apple.quarantine ./halro.

Use a fixed binary, configuration, data directory, and Master Key under launchd or another process manager, with a TLS reverse proxy in front. When Admin is proxied, set:

admin:
external_origin: https://halro.example.com
security:
trust_proxy_headers: true
trusted_proxy_cidrs: ["127.0.0.1/32"]

Use the proxy’s real source CIDR; do not copy loopback when the proxy runs elsewhere.

On x86-64:

Terminal window
VERSION=v0.8.4
ARTIFACT=halro-linux-amd64.tar.gz
BASE_URL="https://github.com/akz142857/Halro/releases/download/$VERSION"
curl -fLO "$BASE_URL/$ARTIFACT"
curl -fLO "$BASE_URL/$ARTIFACT.sigstore.json"
curl -fLO "$BASE_URL/checksums.txt"
curl -fLO "$BASE_URL/checksums.txt.sigstore.json"
COSIGN_IDENTITY="https://github.com/akz142857/Halro/.github/workflows/release.yml@refs/heads/main"
COSIGN_ISSUER="https://token.actions.githubusercontent.com"
cosign verify-blob --certificate-identity "$COSIGN_IDENTITY" --certificate-oidc-issuer "$COSIGN_ISSUER" --bundle checksums.txt.sigstore.json checksums.txt
grep "[[:space:]]$ARTIFACT$" checksums.txt | sha256sum --check
cosign verify-blob --certificate-identity "$COSIGN_IDENTITY" --certificate-oidc-issuer "$COSIGN_ISSUER" --bundle "$ARTIFACT.sigstore.json" "$ARTIFACT"
tar -xzf "$ARTIFACT"
cd halro-linux-amd64
./halro version
./halro start --config ./config.yaml

Use arm64 on ARM64. For a remote server, tunnel Admin with ssh -L 8081:127.0.0.1:8081 user@your-server, then open the loopback setup URL locally.

Terminal window
sudo useradd --system --home /var/lib/halro --shell /usr/sbin/nologin halro
sudo install -m 0755 ./halro /usr/local/bin/halro
sudo install -d -o halro -g halro -m 0700 /var/lib/halro /var/lib/halro-secrets
sudo install -d -o root -g halro -m 0750 /etc/halro
curl -o /tmp/halro-config.yaml https://raw.githubusercontent.com/akz142857/Halro/v0.8.4/configs/config.example.yaml
sudo install -o root -g halro -m 0640 /tmp/halro-config.yaml /etc/halro/config.yaml

Use fixed storage paths:

storage:
data_dir: /var/lib/halro/data
master_key:
mode: file
file: /var/lib/halro-secrets/master.key

Validate and initialize once:

Terminal window
sudo -u halro /usr/local/bin/halro config check --config /etc/halro/config.yaml
sudo -u halro /usr/local/bin/halro init --config /etc/halro/config.yaml

Create /etc/systemd/system/halro.service:

[Unit]
Description=Halro LLM Gateway
After=network-online.target
Wants=network-online.target
[Service]
User=halro
Group=halro
ExecStart=/usr/local/bin/halro serve --config /etc/halro/config.yaml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
TimeoutStopSec=150s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/lib/halro /var/lib/halro-secrets
[Install]
WantedBy=multi-user.target
Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now halro
sudo systemctl status halro
sudo journalctl -u halro -f

Keep all listeners local until a TLS reverse proxy and correct external origin/trusted proxy range are configured.

Use Docker Desktop 4.34 or newer with WSL2, Linux containers, and Host networking. Host networking and Enhanced Container Isolation cannot both be enabled; use WSL2 or a Linux VM if ECI must remain enabled.

Terminal window
New-Item -ItemType Directory -Force C:\halro | Out-Null
Set-Location C:\halro
curl.exe -o config.yaml https://raw.githubusercontent.com/akz142857/Halro/v0.8.4/configs/config.example.yaml
docker compose run --rm halro-init
docker compose up -d halro
docker compose cp halro:/run/secrets/halro-master.key .\halro-master.key
if ((Get-Item .\halro-master.key).Length -ne 32) { throw "Master Key is not 32 bytes" }
icacls .\halro-master.key /inheritance:r
icacls .\halro-master.key /grant:r "$($env:USERNAME):(R)"

Move the exported key into independent secret management or offline media. Use a Linux VM for Windows Server production.

Follow the Linux procedure in Ubuntu or another WSL2 distribution. Prefer a Linux VM or container host for production because WSL2 lifecycle, network, and mount behavior are development-oriented.

The accepted GHCR image is v0.8.4 for linux/amd64 and linux/arm64. It is distroless, has no shell, runs as uid/gid 65532, defaults to serve, and serve never initializes automatically.

Terminal window
curl -o config.yaml https://raw.githubusercontent.com/akz142857/Halro/v0.8.4/configs/config.example.yaml

Keep the full file and change at least:

storage:
data_dir: "/var/lib/halro/data"
master_key:
mode: file
file: "/run/secrets/halro-master.key"

data_dir must be a child of the mount, not /var/lib/halro, because Halro creates an atomic publication lock beside it.

Save the following as compose.yaml:

services:
halro-init:
image: ghcr.io/akz142857/halro:v0.8.4
user: "65532:65532"
command: ["init", "--config", "/etc/halro/config.yaml"]
network_mode: host
volumes:
- ./config.yaml:/etc/halro/config.yaml:ro
- halro-data:/var/lib/halro
- halro-secrets:/run/secrets
halro:
image: ghcr.io/akz142857/halro:v0.8.4
user: "65532:65532"
command: ["serve", "--config", "/etc/halro/config.yaml"]
network_mode: host
restart: unless-stopped
volumes:
- ./config.yaml:/etc/halro/config.yaml:ro
- halro-data:/var/lib/halro
- halro-secrets:/run/secrets:ro
volumes:
halro-data:
halro-secrets:
Terminal window
docker compose run --rm halro-init
docker compose up -d halro
docker compose ps
docker compose cp halro:/run/secrets/halro-master.key ./halro-master.key
chmod 600 ./halro-master.key
test "$(wc -c < ./halro-master.key | tr -d ' ')" -eq 32

Move the key to independent secret management. Use docker compose logs -f halro for logs. Host networking keeps the three loopback listeners on host loopback. Container health does not prove that external ports, certificates, proxy, or firewall are correct; test from the real client location.

4. Configure HTTPS certificates for production

Section titled “4. Configure HTTPS certificates for production”

The recommended path is client → HTTPS reverse proxy → 127.0.0.1:8080/8081 → Halro. Keep tls.enabled: false and set the actual Admin origin, required MFA, and exact proxy CIDRs:

tls: {enabled: false, certificates: []}
admin:
external_origin: "https://admin.example.com"
mfa_policy: "required"
security:
trust_proxy_headers: true
trusted_proxy_cidrs: ["127.0.0.1/32", "::1/128"]

Never use 0.0.0.0/0. Restrict Admin by VPN, firewall, identity proxy, or IP allowlist. Alternatively, let Halro terminate TLS directly:

server:
gateway_listen: "0.0.0.0:8080"
admin_listen: "0.0.0.0:8081"
metrics_listen: "127.0.0.1:9090"
tls:
enabled: true
certificates:
- cert_file: /etc/halro/tls/fullchain.pem
key_file: /etc/halro/tls/privkey.pem
admin:
external_origin: "https://admin.example.com:8081"
mfa_policy: "required"

Mount ./tls:/etc/halro/tls:ro into both services. Keep private keys out of images and Git. SIGHUP reloads contents at unchanged paths; path or list changes require restart.

5. Deliver the first setup credential safely

Section titled “5. Deliver the first setup credential safely”

When admin.external_origin is non-empty, Admin is non-loopback, or admin.setup_token_file is explicitly configured, setup requires a one-time Token. Generate a read-only file with the exact Halro version being deployed:

Terminal window
halro admin setup-token generate \
--ttl 30m \
--output /secure/path/setup-token

The command exclusively creates a 0600 file, refuses to overwrite a path, and never prints the Token to stdout, stderr, or logs. Send the file to secret management or mount it in Compose:

admin:
setup_token_file: /run/secrets/halro/setup-token
setup_token_ttl: 30m
services:
halro:
command: ["serve", "--config", "/etc/halro/config.yaml"]
volumes:
- ./config.yaml:/etc/halro/config.yaml:ro
- /secure/path/setup-token:/run/secrets/halro/setup-token:ro

Give the envelope’s first line to the setup approver through the organization’s existing secret channel; do not grant log or container-exec access. The absolute expiry does not extend after restart. A missing, malformed, or expired file makes serve fail closed without log fallback.

Unattended deployment uses offline, idempotent admin bootstrap --if-needed --operation-id ... --password-file .... After administrator creation, remove the workload’s mount dependency before revoking the Secret; Halro no longer reads the file once an administrator exists. See Production Admin bootstrap and secret lifecycle for the full Docker/Kubernetes state machine, Audit verification, and compromise recovery.

The production Kubernetes/GitOps install state machine shipped in v0.8.3, and the v0.8.4 image carries it. The automated Job path currently supports only storage.master_key.mode: key_slots:

PVC / config / Secrets / default-deny NetworkPolicy
One-shot Bootstrap Job (init --if-needed → admin bootstrap)
Verification Job (doctor → audit verify)
Delete Jobs and bootstrap Secret; revoke temporary KMS identity
Start the one-replica production Deployment with halro serve
Terminal window
kubectl -n halro create secret generic halro-admin-bootstrap \
--from-file=admin-password=/secure/path/admin-password
kubectl -n halro apply -f deploy/kubernetes/halro-bootstrap-job.yaml
kubectl -n halro wait --for=condition=complete \
job/halro-bootstrap-install-id --timeout=15m
kubectl -n halro delete job halro-bootstrap-install-id --wait=true
kubectl -n halro apply -f deploy/kubernetes/halro-bootstrap-verify-job.yaml
kubectl -n halro wait --for=condition=complete \
job/halro-bootstrap-verify-install-id --timeout=10m

Bootstrap/Verification Jobs and Runtime never overlap. They use a dedicated ServiceAccount, temporary KMS Lifecycle identity, and install-only GitOps lifecycle; Runtime needs no Secret API, log, or exec permission. See Production Admin bootstrap and secret lifecycle for the full security and recovery boundary.

The following uses the accepted v0.8.4 image, preferably pinned by reviewed manifest digest. It is a manual single-node File Master Key template, not the new official automated Bootstrap Job. A secure post-generation export protocol for automating File Master Key custody remains future work.

The minimal design uses separate halro-data and halro-key PVCs, both backed up outside the cluster. AWS KMS production deployments may start from the hardened deploy/kubernetes/halro-aws-kms.yaml template in the main repository.

server:
gateway_listen: "0.0.0.0:8080"
storage:
data_dir: "/var/lib/halro/data"
master_key:
mode: file
file: "/var/lib/halro-secrets/master.key"
Terminal window
kubectl create namespace halro
kubectl -n halro create secret generic halro-config --from-file=config.yaml=./config.yaml

2. Create persistent volumes and initialize once

Section titled “2. Create persistent volumes and initialize once”

Create separate ReadWriteOnce PVCs (20Gi data and 1Gi key), then run a one-shot Job as uid/gid 65532 with args: ["init", "--config", "/etc/halro/config.yaml"]. Mount the config Secret read-only at /etc/halro, data at /var/lib/halro, and key PVC at /var/lib/halro-secrets.

Terminal window
kubectl apply -f halro-storage-init.yaml
kubectl -n halro wait --for=condition=complete job/halro-init --timeout=120s
kubectl -n halro logs job/halro-init

If initialization fails, inspect logs. Do not repeatedly delete keys or data; confirm the PVCs are new and empty, or restore a matching data-and-key set.

3. Back up the Master Key outside the cluster

Section titled “3. Back up the Master Key outside the cluster”

Before starting Halro, mount halro-key read-only into a temporary, reviewed utility Pod without a service-account token. Copy and verify the key, then delete the Pod:

Terminal window
kubectl apply -f halro-key-export.yaml
kubectl -n halro wait --for=condition=Ready pod/halro-key-export --timeout=120s
kubectl -n halro cp halro-key-export:/key/master.key ./halro-master.key
chmod 600 ./halro-master.key
test "$(wc -c < ./halro-master.key | tr -d ' ')" -eq 32
kubectl -n halro delete pod halro-key-export

Move it to external secret management or offline media and configure an independent snapshot for the key PVC before continuing.

The Deployment must specify replicas: 1, strategy.type: Recreate, a 150-second termination grace period, uid/gid/fsGroup 65532, dropped capabilities, read-only root filesystem, and separate read-only config/key mounts. The command is serve --config /etc/halro/config.yaml -allow-insecure-public-listen; the flag acknowledges that cluster networking, not Halro TLS, protects the internal listener. Use exec readiness and liveness probes against loopback.

Terminal window
kubectl apply -f halro.yaml
kubectl -n halro rollout status deployment/halro
kubectl -n halro get pods
kubectl -n halro logs deployment/halro
kubectl -n halro port-forward service/halro 8080:8080
kubectl -n halro port-forward deployment/halro 8081:8081

Open http://127.0.0.1:8081/admin/setup, then stop the Admin port-forward. Do not expose Admin as a public Service. Terminate TLS at Ingress/Gateway, plan separate restricted Admin and Metrics entry points, and pin the image as ghcr.io/akz142857/halro@sha256:....

Create resources in this order:

Credential → Provider → Deployment → Route → Project → Gateway Key

Follow First Admin Console setup for model discovery, capability evidence, Price Version, Policy, Key scopes, and negative checks. Store the one-time Gateway Key in application secret management.

Terminal window
curl -fsS http://127.0.0.1:8080/health/ready
export HALRO_GATEWAY_KEY='gw_...'
curl http://127.0.0.1:8080/v1/chat/completions \
-H "Authorization: Bearer $HALRO_GATEWAY_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"chat","max_tokens":64,"messages":[{"role":"user","content":"Hello"}]}'

Before production: pin the release or digest, enforce TLS and network controls, back up the Master Key independently, then stop Halro and complete an encrypted backup, verification, and isolated restore drill.