podman-api

module
v1.0.28 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 29, 2026 License: MIT

README

podman-api

A small REST wrapper around Podman's libpod REST API that lets a CMS (or any orchestrator) deploy and manage pods described by YAML templates across a fleet of Podman hosts.

It is opinionated, single-binary, and deliberately narrow: one host group, bearer-token auth, audit log to stdout, Prometheus metrics on a separate listener, kubernetes-style secrets, idempotent applies. Use it when Kubernetes is too much and ad-hoc podman run over SSH is too little.

Documentation

This README is the quick reference. The wiki is the operator's handbook:

  • Building — why the build needs tags, make targets, static/cross builds.
  • Provisioning a Podman Host — turn a fresh box into an SSH-drivable target.
  • Deploying — install the daemon: user, config tree, systemd, TLS.
  • Operating — key rotation, audit-log shipping, metrics, health checks.
  • Troubleshooting — common failures and their fixes.

What it does

  • Lists, applies, upgrades, starts/stops, and deletes pod instances on remote Podman hosts via SSH-tunneled podman.sock.
  • Renders parameterised templates (Go text/template over Kubernetes-style YAML) into Pod manifests, then plays them with podman play kube.
  • Manages per-host and per-instance secrets as Kubernetes Secret resources (so secretKeyRef works).
  • Pre-pulls every container image before play, so a bad tag fails fast with a clear registry error instead of a 30-second timeout.
  • Streams logs as plain text or SSE.
  • Migrates and evacuates instances across hosts as async jobs. Movement is cold-copy (stop → copy volumes → re-apply → verify → reap source) with rollback if the destination doesn't come up.
  • Backs up and restores instance volumes on demand: POST .../backup snapshots every volume; POST /backups/{id}/restore restores in-place with content verification. See Backing up and Restoring.
  • Exposes Prometheus metrics on a separate, opt-in listener so they aren't world-readable on the public port.

What it doesn't do

  • No multi-replica HA; one daemon, one config tree.
  • No image registry, no scheduler, no rolling deploy primitives. (Upgrade is single-pod replace-in-place.)
  • No webhook callbacks; callers poll (including the async migrate/evacuate jobs).
  • No daemon-side placement/scheduler — cross-host moves work, but the client chooses destinations (the daemon executes the move).
  • No live migration — cross-host moves are cold (stop → copy volumes → re-apply → verify → reap the source).

Architecture

   ┌────────────┐  HTTPS    ┌────────────────┐  SSH-tunneled  ┌────────────┐
   │ CMS / curl │ ───────▶  │  podman-api    │ ─────────────▶ │ podman.sock│
   │            │           │  (this binary) │   libpod REST  │  on hostN  │
   └────────────┘           └────────────────┘                └────────────┘
                              │            │
                              │            └─▶  /metrics  (separate addr, optional)
                              │
                              └─▶  audit log (stdout, JSON lines)

Hosts are declared once in hosts/*.yaml. Templates live in the store. Bearer keys live in auth/keys.yaml and reload without restart via SIGHUP.

Build

make build          # -> bin/podman-api

The podman v5 bindings require build tags to exclude CGO graph-driver headers. make build carries them. See Building for cross-compile and CI details.

Configure and run

See Deploying for the full setup guide (installer, systemd unit, TLS, bearer keys). The short version:

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 \
  -spec-key-file=/etc/podman-api/spec.key

A contrib/install.sh script creates a dedicated user, installs the binary, and enables the service.

When the background inventory poller is enabled (-inventory-refresh-interval, default 30s), /metrics additionally exposes per-container state rendered from the warm inventory cache:

Metric Labels Meaning
podman_api_container_restarts_total host, template, slug, container podman's restart count. Resets to 0 when the pod is recreated, so a redeploy appears as a counter reset.
podman_api_container_running host, template, slug, container 1 if the container status is running.
podman_api_container_healthy host, template, slug, container 1 healthy, 0 unhealthy or starting, -1 when the container declares no healthcheck.
podman_api_instance_ready host, template, slug 1 if every container reports a healthy or absent healthcheck.
podman_api_host_reachable host 1 if the most recent inventory refresh succeeded.
podman_api_inventory_age_seconds host Seconds since the last successful refresh.

The last two are not optional decoration. When a host becomes unreachable the cache keeps serving last-known-good data, so podman_api_container_running stays at 1 for the duration of the outage. Any alert built on the container metrics should be gated on podman_api_host_reachable == 1, with a separate alert on staleness.

Resource metrics (#209). With the poller enabled, the same listener also exports per-container resource usage:

Metric Labels Meaning
podman_api_container_cpu_seconds_total host, template, slug, container Cumulative CPU time consumed by the container.
podman_api_container_memory_bytes host, template, slug, container Current memory usage.
podman_api_container_memory_limit_bytes host, template, slug, container Declared memory limit. Absent, not zero, when the container declares no limit.
podman_api_container_network_receive_bytes_total host, template, slug, container Bytes received, summed across interfaces.
podman_api_container_network_transmit_bytes_total host, template, slug, container Bytes transmitted, summed across interfaces.
podman_api_container_block_read_bytes_total host, template, slug, container Bytes read from block devices.
podman_api_container_block_write_bytes_total host, template, slug, container Bytes written to block devices.
podman_api_container_processes host, template, slug, container Number of processes running in the container.
podman_api_volume_size_bytes host, template, slug, volume On-disk size of a managed volume, from podman system df.
podman_api_volume_usage_age_seconds host Seconds since the last successful volume-usage walk.

Container stats are sampled with one containers.Stats call per host on each inventory tick — but only when that tick's inventory refresh for the host succeeded. A host whose refresh fails is not also charged a second timeout for stats; instead its cached samples are dropped, so its container series go absent on the next scrape rather than freezing at their last value, exactly like the liveness metrics above. Disable the sampler with -container-stats=false (default true — an existing deployment picks up one extra call per host per tick on upgrade, with no flag change needed).

A succeeded refresh is not a guarantee of a sample, and the difference shows up as a sawtooth: stats share the refresh's one per-host budget (-inventory-refresh-timeout, default 20s) rather than getting a second one, so a host whose refresh chronically eats most of that budget leaves the stats call whatever is left — sometimes enough, sometimes not. The symptom is that host's resource series flipping absent/present tick to tick, with container stats unavailable / container stats available again alternating in the log. That is the shared budget working as designed (two independent timeouts would make the per-host cost 2 × timeout and stretch the whole fleet's inventory freshness); the lever is the host's refresh latency, or a larger -inventory-refresh-timeout. The metrics stay honest either way — a missed sample renders absent, never stale.

The cumulative series (_total suffix) reset when a pod is recreated, exactly as podman_api_container_restarts_total does; graph them with rate() or increase(), not as raw counters. Podman's own CPU/memory percentages are not exported: for a non-streaming stats call they're averaged against the container's start time, so a container busy at boot and idle since would read as permanently hot — the raw counters and rate() avoid that.

Volume usage runs on its own, much slower loop, independent of the inventory tick, on -volume-usage-interval (default 1h, 0 disables it) with -volume-usage-timeout (default 5m) — podman system df walks the whole store and can take minutes, so it must never delay an inventory refresh. It also does an immediate first walk at startup, so on a large store that walk can run for minutes concurrently with boot. A failed walk leaves the previous sizing in place; podman_api_volume_usage_age_seconds climbing is how a wedged or slow walk becomes visible.

Attributing a size to an instance needs the volume in the inventory, so the inventory sweep now populates every volume the template declares, without inspecting it — the sweep must make no podman call per volume or it would cost one round trip per volume per host per tick. That changes what the list route reports: GET /hosts/{host}/instances lists a template's declared volumes, including any that do not exist on the host yet, always with size_bytes 0, whereas GET /hosts/{host}/instances/{template}/{slug} still inspects and so lists only volumes that really exist, with their real sizes. Treat the list route's volumes as names, and the single-instance route as the authority on existence and size.

Neither sampler can affect podman_api_host_reachable: the Grafana Infrastructure Alerts rules gate on it, so a slow system df or a stats timeout must never be able to silence them.

Admin UI

An embedded, server-rendered admin UI (HTMX + PureCSS) is served at /ui. Disabled unless -operator-file <path> is set. Pass -ui-secure-cookie when serving over HTTPS.

Generate a password hash:

podman-api hash-token <your-password>

API reference

A complete OpenAPI 3.0 spec lives at api/openapi.yaml and is served by the binary at GET /openapi.yaml (no auth).

GET    /healthz
GET    /metrics                                                      separate listener

GET    /hosts
GET    /hosts/{host}
GET    /hosts/{host}/healthz
GET    /hosts/{host}/ports-in-use

GET    /templates
GET    /templates/{id}
GET    /templates/{id}/render?<params>
POST   /templates                            body: {id, body, ...}
PUT    /templates/{id}                       body: {body, ...}
DELETE /templates/{id}?force=
POST   /templates/{id}/clone                 body: {new_id}

GET    /hosts/{host}/secrets
PUT    /hosts/{host}/secrets/{name}          body: {"value": "..."}
DELETE /hosts/{host}/secrets/{name}

GET    /hosts/{host}/instances?template=<id>
GET    /hosts/{host}/instances/{template}/{slug}
POST   /hosts/{host}/instances
PUT    /hosts/{host}/instances/{template}/{slug}
DELETE /hosts/{host}/instances/{template}/{slug}?prune_volumes=&prune_secrets=

POST   /hosts/{host}/instances/{template}/{slug}/start
POST   /hosts/{host}/instances/{template}/{slug}/stop
POST   /hosts/{host}/instances/{template}/{slug}/restart
POST   /hosts/{host}/instances/{template}/{slug}/upgrade  body: {"image": "..."}
PATCH  /hosts/{host}/instances/{template}/{slug}/parameters  body: {"parameters": {...}}
PATCH  /hosts/{host}/instances/{template}/{slug}/secrets     body: {"secrets": {...}}

GET    /hosts/{host}/instances/{template}/{slug}/logs?container=&tail=&follow=
GET    /hosts/{host}/instances/{template}/{slug}/volumes
DELETE /hosts/{host}/volumes/{name}

POST   /migrate    body: {from_host, to_host, template, slug, parameters?}
POST   /evacuate   body: {from_host, map:{slug: to_host}}

POST   /hosts/{host}/instances/{template}/{slug}/backup
GET    /hosts/{host}/instances/{template}/{slug}/backups?limit=
POST   /backups/{id}/restore
DELETE /backups/{id}

GET    /jobs?state=&kind=&parent_id=
GET    /jobs/{id}

Notes:

  • POST /migrate and POST /evacuate require -state-db (else 501), validate synchronously, and return 202 {job_id}. Poll GET /jobs/{id} for progress.
  • ?skip_pull=true on POST/PUT skips the pre-pull step.
  • The two PATCH routes change one instance in place and reuse its sealed per-instance secrets, so they work on an instance whose secret plaintext nobody can read back (a full PUT requires every declared secret). Both merge — omitted parameters/secrets keep their stored value, and neither can delete one. Both replace the pod.
  • On DELETE, prune_volumes and prune_secrets default to false. Pass both as true to reap volumes and secrets. DELETE is idempotent — a prune-requested delete on an already-gone pod still removes orphaned volumes/secrets and returns 204.

Directories

Path Synopsis
Package apispec exposes the openapi.yaml spec embedded in the binary so it can be served at /openapi.yaml without needing the file at runtime.
Package apispec exposes the openapi.yaml spec embedded in the binary so it can be served at /openapi.yaml without needing the file at runtime.
cmd
podman-api command
Command podman-api is the HTTP service that translates CMS REST calls into libpod REST calls against one or more Podman hosts.
Command podman-api is the HTTP service that translates CMS REST calls into libpod REST calls against one or more Podman hosts.
Package extension provides public interfaces that a private commercial module can implement to extend podman-api without modifying OSS internals.
Package extension provides public interfaces that a private commercial module can implement to extend podman-api without modifying OSS internals.
internal
api
Package api wires HTTP routes to the instance.Service.
Package api wires HTTP routes to the instance.Service.
backup
Package backup holds the OSS backup primitives around instance.Service: the local-directory blob store and the backup/restore job adapters (#66).
Package backup holds the OSS backup primitives around instance.Service: the local-directory blob store and the backup/restore job adapters (#66).
evacuate
Package evacuate adapts host-evacuation orchestration to the jobs runner.
Package evacuate adapts host-evacuation orchestration to the jobs runner.
ingress
Package ingress derives a host's ingress routes from the store and pushes them to an operator-managed Caddy instance via the admin API.
Package ingress derives a host's ingress routes from the store and pushes them to an operator-managed Caddy instance via the admin API.
inventory
Package inventory keeps each host's cached instance inventory warm by refreshing it on a schedule, so UI/API reads are served without a live podman sweep and an unreachable host never stalls a request.
Package inventory keeps each host's cached instance inventory warm by refreshing it on a schedule, so UI/API reads are served without a live podman sweep and an unreachable host never stalls a request.
jobs
Package jobs runs queued jobs from a store.JobStore through registered per-kind handlers, on a bounded background worker pool.
Package jobs runs queued jobs from a store.JobStore through registered per-kind handlers, on a bounded background worker pool.
migrate
Package migrate adapts the instance migrate algorithm to the jobs runner.
Package migrate adapts the instance migrate algorithm to the jobs runner.
obs
Package obs holds the observability primitives: structured audit log middleware and Prometheus metrics.
Package obs holds the observability primitives: structured audit log middleware and Prometheus metrics.
podman/fake
Package fake is an in-memory implementation of podman.Client used by tests.
Package fake is an in-memory implementation of podman.Client used by tests.
prune
Package prune implements scheduled host-health cleanup: a "prune" job kind run by the jobs runner, fed by a per-host scheduler that fires on a configurable interval or disk high-water threshold.
Package prune implements scheduled host-health cleanup: a "prune" job kind run by the jobs runner, fed by a per-host scheduler that fires on a configurable interval or disk high-water threshold.
store
Package store is the daemon's durable desired-state record: one encrypted row per instance, written on Apply and removed on Delete.
Package store is the daemon's durable desired-state record: one encrypted row per instance, written on Apply and removed on Delete.
ui
Package ui implements the server-rendered single-operator admin UI.
Package ui implements the server-rendered single-operator admin UI.
Package templates exposes the YAML pod-spec templates shipped with the binary.
Package templates exposes the YAML pod-spec templates shipped with the binary.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL