DocumentationSELF-HOSTED / DEVELOPMENT RELEASEView source ↗
Build and deploy

Deploy from CI with the API

Trigger a reviewed deployment from CI, update selected services and wait for rollout results.

Deploy from your existing CI pipeline after it builds and pushes an image. Hakopod accepts the release, resolves image tags to immutable digests, and reports the rollout separately.

Before you start#

Create a named, expiring API key scoped to the target project, environment and application with deployments:read and deployments:write. Keep it in your CI provider's secret store. Configure persistent registry pull credentials in Hakopod if the image is private; a successful CI push does not give the cluster permission to pull.

The examples use Bash, curl and jq. Set these CI variables:

Variable Value
HAKOPOD_URL Your API origin, such as https://deploy.example.com, without a trailing slash.
HAKOPOD_TOKEN The scoped API key, stored as a CI secret.
APP_ID The application ID shown in the dashboard.
IMAGE The image tag or digest your CI just pushed.
CI_DEPLOYMENT_KEY A unique key of 8–128 characters for this release. Reuse it only when retrying the same request.

Use the API origin with /api/v1, not the dashboard's internal /api proxy. Do not enable shell tracing around bearer tokens.

Review and deploy one service#

Fetch the current application so unrelated settings and the concurrency revision are retained. Change the image of api, review a plan, and submit it:

set -euo pipefail
umask 077

curl --fail-with-body --silent --show-error --max-time 30 \
  -H "Authorization: Bearer $HAKOPOD_TOKEN" \
  "$HAKOPOD_URL/api/v1/applications/$APP_ID" > application.json

jq --arg image "$IMAGE" '
  {project, environment, expected_revision: .revision, spec, service: "api"}
  | .spec.services.api.image = $image
' application.json > request.json

curl --fail-with-body --silent --show-error --max-time 120 \
  -H "Authorization: Bearer $HAKOPOD_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @request.json \
  "$HAKOPOD_URL/api/v1/plan" > plan.json

jq -s '
  .[0] as $request | .[1] as $plan
  | if $request.expected_revision != $plan.expected_revision
    then error("Application changed. Fetch and review a fresh plan.")
    else $request + {
      spec: $plan.spec,
      expected_revision: $plan.expected_revision
    }
    end
' request.json plan.json > deployment.json

curl --fail-with-body --silent --show-error --max-time 120 \
  -H "Authorization: Bearer $HAKOPOD_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $CI_DEPLOYMENT_KEY" \
  --data-binary @deployment.json \
  "$HAKOPOD_URL/api/v1/deployments" > accepted.json

jq '{id, revision, status}' accepted.json

The plan's changes and warnings are available for your CI approval policy. Treat the saved configuration files as private pipeline artifacts; environment configuration can contain sensitive data. Remove them when the job finishes.

For a full application release, omit service and services and submit the complete application configuration. Other image tags are resolved again in that mode. A single-service release keeps unselected services on their accepted image digests.

Deploy a selected group#

Release availability: the services selector is scheduled for the next release after alpha.13. Alpha.13 and earlier support the single service selector and application-wide requests. Upgrade before sending a selected-group request.

On a version supporting selected groups, use services instead of service. For example, replace the first jq command above with:

jq --arg image "$IMAGE" '
  {project, environment, expected_revision: .revision, spec,
   services: ["api", "worker"]}
  | .spec.services.api.image = $image
  | .spec.services.worker.image = $image
' application.json > request.json

The rest of the plan and deployment flow stays the same. Both selected services are accepted in one application revision; unrelated services retain their configuration and image digests. The array accepts 1–20 unique names present in the submitted configuration. Empty lists, duplicate names, missing services and both selectors together are rejected.

Shared application variables, secret defaults, networks and volumes require an application-wide plan. Group rollouts are not atomic across containers. Each service follows its readiness checks and configured recovery rules.

Wait for the rollout#

HTTP 202 Accepted means queued, not healthy. Poll the deployment ID and fail the pipeline if the rollout fails. This loop makes at most 120 checks, five seconds apart. Request time adds to the wait; adjust the bound for your workload:

DEPLOYMENT_ID=$(jq -er '.id' accepted.json)
for attempt in $(seq 1 120); do
  curl --fail-with-body --silent --show-error --max-time 30 \
    -H "Authorization: Bearer $HAKOPOD_TOKEN" \
    "$HAKOPOD_URL/api/v1/deployments/$DEPLOYMENT_ID" > status.json
  STATUS=$(jq -er '.status' status.json)
  case "$STATUS" in
    succeeded) echo "Deployment succeeded"; exit 0 ;;
    failed|cancelled) echo "Deployment $STATUS; inspect it in Hakopod"; exit 1 ;;
  esac
  sleep 5
done
echo "Still running. Inspect deployment $DEPLOYMENT_ID in Hakopod."
exit 1

The loop ends the CI job when it succeeds or fails. Use a separate shell step if later pipeline work must continue. Timing out this loop does not cancel the accepted deployment.

Retries and common errors#

  • 409 Conflict: fetch the application again and review a new plan. Do not blindly replace expected_revision. A changed request needs a new idempotency key.
  • Uncertain submission: query GET /api/v1/idempotency/{key} with the same API key before creating another release. Preserve the original request body and idempotency key for retries.
  • 401 or 403: check the API key's expiry, permissions and project/environment/application scope.
  • Image pull failure: confirm the exact image exists and Hakopod has matching registry credentials. CI push credentials and cluster pull credentials have different lifetimes.
  • Readiness failure: inspect deployment events and pod logs; verify the container's listening port, healthcheck and environment.

A new deployment resolves a mutable image tag to its current digest. Restart preserves the previously resolved digest; it is not a request to fetch a newly published latest tag. A deployment of an unchanged digest and configuration need not restart healthy pods.

Continue with Deployments and rollback, Logs and metrics or API keys.