Skip to content
Alexander Birkner edited this page Aug 31, 2026 · 3 revisions

Setup

Silo can run as a local Docker Compose stack for evaluation and development, or on Kubernetes via Helm for production. Both paths render the same config.yaml schema, documented at the bottom of this page.

Docker Compose

docker compose up -d
docker compose logs silo | grep -A 12 'SILO BOOTSTRAP'   # grab the token

This brings up four services from the root docker-compose.yaml:

service image purpose ports
postgres postgres:16-alpine package index, tokens, users, audit log 5432:5432
seaweedfs chrislusf/seaweedfs:latest stands in for S3-compatible object storage 8333:8333 (S3), 8888:8888 (filer web UI)
createbucket chrislusf/seaweedfs:latest one-shot job that creates the silo bucket, then exits
silo built from the repo Dockerfile the server 8080:8080 (gRPC + HTTP)

The silo service waits on postgres being healthy and createbucket having completed, and is configured with:

environment:
  SILO_CONFIG: /etc/silo/config.yaml
  SILO_DATABASE_URL: postgres://silo:silo@postgres:5432/silo
  RUST_LOG: info
volumes:
  - ./config.example.yaml:/etc/silo/config.yaml:ro

Data persists in the named volumes postgres-data and seaweedfs-data. The credentials in this file (silo/silo, siloadmin/siloadmin) are deliberately obvious — this stack is for development only.

On the first start against an empty database, Silo mints an admin token and an admin user and prints them once; they're stored only as hashes and can't be recovered afterwards. Log in with:

silo login --server http://localhost:8080

Helm

helm install silo oci://ghcr.io/birkneralex/charts/silo -f my-values.yaml

Database

The chart refuses to render unless exactly one of these is set:

# Production: a managed instance.
externalPostgres:
  existingSecret: silo-db      # Secret with a `url` key
# Development only: a bundled single-replica Postgres.
# No replication, no backups, no pooling.
postgres:
  enabled: true

Secrets

Secrets can stay out of values.yaml entirely — the server expands ${VAR} and ${VAR:-default} in its config file from the environment, so the chart writes placeholders and injects real values from Kubernetes Secrets (storage.existingSecret, auth.tokenPepperExistingSecret, signing.*.existingSecret, etc.). An unset variable with no default is a startup error rather than an empty string.

Ingress

One Ingress covers both the CLI's gRPC calls and the dnf/apk/npm/pacman/apt HTTP surface — they share a Service port, and silo tells them apart itself by path and by per-connection protocol detection. The only requirement on the ingress controller is that it speaks real HTTP/2 to the backend instead of downgrading to HTTP/1.1. Disabled by default.

ingress:
  enabled: true
  className: traefik
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    external-dns.alpha.kubernetes.io/hostname: packages.example.com
    traefik.ingress.kubernetes.io/service.serversscheme: h2c
  hosts:
    - host: packages.example.com
      paths: [{path: /, pathType: Prefix}]
  tls:
    - secretName: silo-tls
      hosts: [packages.example.com]

Traefik is recommended — the h2c annotation above tells it to dial the backend over cleartext HTTP/2, and it forwards whatever it gets without caring whether it's gRPC or plain HTTP.

nginx can do the same, but only through plain proxy_pass with proxy_http_version 2, supported by nginx open source only since 1.29.4 (December 2025). Earlier versions can reach a backend over HTTP/2 only through the gRPC-specific grpc_pass module, which can't carry dnf/apk/npm/pacman/apt traffic. If your ingress-nginx build doesn't expose proxy_http_version 2 through a plain annotation, force it with a snippet:

annotations:
  nginx.ingress.kubernetes.io/configuration-snippet: |
    proxy_http_version 2;

Labels and annotations

commonLabels and commonAnnotations reach every resource the chart creates. Per-resource labels/annotations merge on top, for keys that only make sense on one object — e.g. IRSA or Workload Identity on the serviceAccount:

commonLabels:
  team: platform
commonAnnotations:
  example.com/owner: platform-team

serviceAccount:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/silo

commonLabels deliberately never touches spec.selector — that field is immutable, so folding user labels into it would break every subsequent helm upgrade.

Migrations

Migrations are embedded in the binary and applied on startup. sqlx takes its own advisory lock, so every replica running them simultaneously during a rolling deploy is safe. For deployments that prefer running migrations as a separate Job:

silo-server --migrate-only

The config.yaml schema

Both Compose (via the mounted config.example.yaml) and Helm (via the config.* values, rendered into a Secret) produce the same server config:

addr: "0.0.0.0:8080"          # gRPC and HTTP share this port
public_base_url: "https://silo.example.com"   # only strictly needed by npm

database:
  url: "${SILO_DATABASE_URL}"
  max_connections: 10
  connect_timeout_seconds: 120

storage:
  bucket: "silo"
  region: "us-east-1"
  access_key_id: "siloadmin"
  secret_access_key: "siloadmin"
  endpoint: "http://seaweedfs:8333"   # omit for real AWS S3
  allow_http: true                    # required for a non-TLS endpoint

auth:
  bootstrap: true                     # mint an admin token/user on first empty-DB start
  bootstrap_token_name: "bootstrap-admin"
  bootstrap_username: "admin"
  token_pepper: "${SILO_TOKEN_PEPPER}"   # generate with `openssl rand -hex 32`
  session_ttl_hours: 720

audit:
  log_downloads: true
  retention_days: 90

metrics:
  enabled: true
  require_auth: false

prune:
  enabled: false

jobs:
  session_cleanup: "0 */5 * * * *"
  audit_prune: "0 0 * * * *"
  package_prune: "0 0 3 * * *"

oidc:
  issuer: "https://id.example.com"
  client_id: "silo"
  username_claim: "preferred_username"
  scopes: ["openid", "profile"]
  admin_claim: "groups"
  admin_value: "silo-admins"
  exclusive: false

signing:
  gpg:
    key_path: "/etc/silo/gpg-key.asc"
    passphrase: "${SILO_GPG_PASSPHRASE}"
  apk:
    key_path: "/etc/silo/apk-key.pem"
    key_name: "silo@example.com-1a2b3c4d.rsa.pub"
  pacman:
    key_path: "/etc/silo/pacman-key.asc"
    passphrase: "${SILO_PACMAN_GPG_PASSPHRASE}"

signing.* is optional and independent per format; omit any of them to publish unsigned in that format. There is no signing.deb block — apt trusts the same signing.gpg key RPM does, rather than a fifth key slot. See Usage for what each signing block enables client-side. prune and jobs configure retention-based package deletion and the schedule of silo's other background housekeeping — see Maintenance.

Next

See Usage to create tokens and configure a package manager against the server you just stood up.

Clone this wiki locally