Skip to content

Deploying

Tej Pochiraju edited this page Jun 5, 2026 · 5 revisions

Deploying podman-api

This is about installing the daemon on the host where your CMS lives. It does not need to be on the same network as the podman hosts — it only needs the SSH keys to reach them.

Opinionated layout

/etc/podman-api/
├── hosts/
│   └── prod-1.yaml      # one file per target (see Provisioning a Podman Host)
├── keys.yaml            # Argon2id bearer tokens
└── spec.key             # optional 32-byte key; only needed for secret-bearing deploys

Templates are not files on disk — they live in the always-on state store (-state-db, default /var/lib/podman-api/state.db) and are managed over the /templates API (see Operating). The store seeds a small bundled set (postgres, basic-web) the first time it boots on an empty catalog.

Install with the bundled script

contrib/install.sh creates a dedicated podman-api system user, builds the binary with the correct tags (via make build), installs it to /usr/local/bin, drops the systemd unit, and seeds an empty config tree. It does not start the service — review config first.

sudo contrib/install.sh
# or use a pre-built binary:
sudo BINARY=/path/to/podman-api contrib/install.sh

Bearer keys

Generate an Argon2id hash with the binary itself, then add it to keys.yaml:

podman-api hash-token "my-plaintext-token"
# $argon2id$v=19$m=65536,t=3,p=4$...
keys:
  - id: cms-prod
    secret_hash: '$argon2id$v=19$m=65536,t=3,p=4$...'
    scopes: [hosts:read, instances:*, secrets:*]
    description: "Prod CMS"
  - id: prom-scraper
    secret_hash: '$argon2id$v=19$m=65536,t=3,p=4$...'
    scopes: [hosts:read]

Scopes: hosts:read, hosts:write, instances:read, instances:write, secrets:read, secrets:write, templates:read, templates:write, jobs:read (plus instances:* / secrets:* / templates:* shorthands). Template scopes are distinct from instance scopes — managing the catalog needs templates:write, deploying an instance from a template needs instances:write. Rotation is live — see Operating.

Run

podman-api \
  -addr=127.0.0.1:8080 \
  -metrics-addr=127.0.0.1:9090 \
  -hosts-dir=/etc/podman-api/hosts \
  -keys-file=/etc/podman-api/keys.yaml \
  -state-db=/var/lib/podman-api/state.db
# the store is ALWAYS opened; -state-db is its LOCATION (default /var/lib/podman-api/state.db).
# its directory must exist and be writable by the daemon user — see Provisioning a Podman Host.
# optional: -spec-key-file=/etc/podman-api/spec.key (32-byte key; required only for secret-bearing deploys)
#           -audit-log-file=/var/log/podman-api/audit.log
# gated features (the store underneath them is always present): -migrate/-evacuate are always on with the store;
#   -ingress-enabled  -prune-enabled  guard ingress / scheduled prune (see Operating).
# jobs: -jobs-retention=168h (prune old terminal jobs)  -evacuate-concurrency=2 (1..32; per-request "concurrency" overrides)  -job-workers=8 (background job pool size)
# migrate verify: -migrate-verify-timeout=60s (raise for slow app warm-up / healthcheck start_period)  -migrate-verify-volumes=true (content-check each copied volume; =false skips it on huge volumes)

Then:

systemctl enable --now podman-api
curl http://127.0.0.1:8080/healthz      # {"status":"ok"}

Templates: the catalog and deploying from it

Templates live in the state store and are managed over the /templates API (scopes templates:read / templates:write). The bundled postgres and basic-web templates are seed data — loaded only when the catalog is empty, then owned by the operator (edits and deletes survive restarts; a populated store is never re-seeded). Full CRUD reference is in Operating; the short version:

TOK=...   # a key with templates:* and instances:*

# list / inspect (structured JSON)
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:8080/templates

# clone the basic-web starter and edit it for your own image
curl -s -X POST -H "Authorization: Bearer $TOK" \
  http://127.0.0.1:8080/templates/basic-web/clone \
  -d '{"new_id":"my-app"}'
curl -s -X PUT -H "Authorization: Bearer $TOK" \
  http://127.0.0.1:8080/templates/my-app -d @my-app.json

Then deploy an instance from a template via POST /instances (scope instances:write). Template parameters are typed and may declare defaults; a deploy that omits a parameter takes its declared default (one-click), and the persisted spec records the effective parameters. See Operating for the parameter model and the structured GET /templates shape.

Security model

  • Binds 127.0.0.1 only. The binary never exposes plaintext on a public port. TLS is the reverse proxy's job — the recommended setup is Caddy in front terminating Let's Encrypt (contrib/Caddyfile.example).
  • /metrics is on a separate listener (-metrics-addr), not the main port — its labels include host/template/path and are not safe to expose publicly. Scrape it over an SSH tunnel or a VPC-internal bind.
  • Bearer tokens are stored as Argon2id PHC strings; plaintext exists only at issuance and in the client config.
  • Input validation (slug/template/secret/container names must match ^[a-z0-9][a-z0-9-]{0,38}[a-z0-9]$) happens at the API edge, before anything reaches the renderer or podman.
  • Secrets arrive in the JSON body of POST /instances, are written as Kubernetes Secret objects on the target host, and are zeroed in the in-memory request struct after use. Persisting them in the store (so migrate/evacuate can re-supply them) needs an encryption key: set -spec-key-file. Without it the store still runs (templates and no-secret deploys work), but a secret-bearing deploy is rejected with secrets require an encryption key (-spec-key-file) (see Troubleshooting).

Deleting instances — mind the prune flags

DELETE /hosts/{host}/instances/{template}/{slug} keeps volumes and per-instance secrets by default. Pass ?prune_volumes=true&prune_secrets=true to also reap them. Delete is an idempotent reconcile: if the pod is already gone, a delete that requests pruning still removes orphans and returns 204. Forgetting prune_secrets leaves the secret on the host — see Troubleshooting.

Related

Clone this wiki locally