-
Notifications
You must be signed in to change notification settings - Fork 0
Deploying
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.
/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.
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.shGenerate 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.
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 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.jsonThen 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.
-
Binds
127.0.0.1only. 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). -
/metricsis 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 withsecrets require an encryption key (-spec-key-file)(see Troubleshooting).
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.
- Provisioning a Podman Host — prepare the targets first.
- Operating — day-2 concerns.