-
Notifications
You must be signed in to change notification settings - Fork 0
CI CD
podman-api exposes everything a CI pipeline needs to deploy preview, staging, and production instances from a Forgejo (or any other) pipeline.
PUT /hosts/{host}/instances/{template}/{slug} is the one endpoint you need. It:
- Pulls the container image (unless
?skip_pull=true) - Renders the template with the parameters you supply
- Creates or replaces the pod in-place (idempotent)
- Waits for container healthchecks to pass (up to the daemon's verify timeout)
- If the template declares
ingress:and you passdomains, updates the Caddy reverse-proxy on the host automatically — one Caddyfile per host, regenerated atomically, zero-downtime reload
The response is an Observed object with ready: true/false, container states,
and any readiness warnings.
CI pipelines need a key with the minimum scope to deploy:
podman-api hash-token "$(openssl rand -base64 32)"
# prints: $argon2id$v=19$m=65536,t=3,p=4$...Add it to /etc/podman-api/keys.yaml:
keys:
# ... existing keys ...
- id: ci-forgejo
secret_hash: '$argon2id$v=19$m=65536,t=3,p=4$<output from above>'
scopes: [hosts:read, instances:write]
description: "Forgejo CI runner"Then reload the daemon without restarting (keys reload on SIGHUP):
kill -HUP $(systemctl show -p MainPID --value podman-api)Store the plaintext token as a Forgejo repository secret (PODMAN_API_TOKEN).
The hash in keys.yaml is useless to an attacker — only the plaintext matters.
Scope note:
instances:writeallows create, replace, start, stop, upgrade, and delete. It does not grant access to the template catalog, secrets store, or migrate/evacuate jobs. If the pipeline also needs to manage templates, addtemplates:write.
Set these repository variables/secrets:
| Name | Value |
|---|---|
PODMAN_API_URL |
https://api.example.com |
PODMAN_API_TOKEN |
plaintext token from above |
DEPLOY_HOST |
target host id (matches a hosts/<id>.yaml filename) |
BASE_DOMAIN |
e.g. preview.example.com
|
on: [pull_request]
jobs:
preview:
runs-on: ubuntu-latest
steps:
- name: Deploy preview
run: |
curl -fsS -X PUT \
"$PODMAN_API_URL/hosts/$DEPLOY_HOST/instances/web/${{ gitea.event.pull_request.number }}" \
-H "Authorization: Bearer $PODMAN_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"parameters": {"image": "registry.example.com/myapp:${{ gitea.sha }}"},
"domains": ["pr-${{ gitea.event.pull_request.number }}.${{ env.BASE_DOMAIN }}"]
}'on:
push:
branches: [main]
jobs:
deploy-prod:
runs-on: ubuntu-latest
steps:
- name: Deploy production
run: |
curl -fsS -X PUT \
"$PODMAN_API_URL/hosts/$DEPLOY_HOST/instances/web/prod" \
-H "Authorization: Bearer $PODMAN_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"parameters": {"image": "registry.example.com/myapp:${{ gitea.sha }}"},
"domains": ["app.example.com"]
}'on:
pull_request:
types: [closed]
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Delete preview instance
run: |
curl -fsS -X DELETE \
"$PODMAN_API_URL/hosts/$DEPLOY_HOST/instances/web/${{ gitea.event.pull_request.number }}" \
-H "Authorization: Bearer $PODMAN_API_TOKEN"Caddy routes for the deleted instance are removed automatically on the next apply to that host (or on the next daemon reconcile cycle).
A successful deploy returns HTTP 200 with an Observed object:
{
"template": "web",
"slug": "42",
"ready": true,
"pod": { "status": "Running", ... },
"containers": [{ "name": "web-42-app", "health": "healthy", ... }],
"warnings": []
}ready: false with entries in warnings means the pod started but healthchecks
did not pass within the verify window — the app may still be initialising. Treat
it as a soft warning; poll GET .../instances/web/42 to watch it come up, or
check warnings[0] for the timeout message.
A non-2xx response body always contains {"code": "...", "message": "..."}.
curl -fsS turns any HTTP error into a non-zero exit code, which fails the CI
step automatically.
Domains are optional. If the template does not declare ingress: in its metadata
or ingress is not enabled on the daemon, omit the domains field and the deploy
works fine — the pod is accessible on its published host ports only.
When domains is present the daemon:
- Joins the pod to the shared ingress network
- Regenerates and reloads the host-wide Caddyfile (one file for all domains on that host, sorted for stability)
- Obtains a TLS certificate via ACME automatically (requires the domain to resolve to the host's public IP and ports 80/443 to be reachable)
See Deploying for how to enable ingress (-ingress-network, -caddy-image,
-acme-email flags).