Files
portainer-deploy-action/README.md
2026-06-27 16:43:36 +00:00

8.6 KiB

Portainer Stack Deploy Action

A reusable Gitea composite action that creates or updates a Docker Compose stack via the Portainer API. Idempotent — creates the stack if it doesn't exist, updates it (with fresh image pulls) if it does. Includes built-in healthcheck verification, image pinning, and structured JSON output for orchestrators.

This action handles deployment only. Your workflow must build and push images in a preceding step. See the full example below.

This action is the deploy leaf in the gitea-deploy-system. It can be called directly (as below) or by the orchestrator with a rendered compose + env file.

Quick Start

# .gitea/workflows/deploy.yml
name: Build & Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build and push image
        run: |
          docker build -t git.homelab.local:8443/${GITHUB_REPOSITORY}:latest .
          echo "${{ secrets.GITEA_TOKEN }}" | docker login git.homelab.local:8443 -u ${{ github.actor }} --password-stdin
          docker push git.homelab.local:8443/${GITHUB_REPOSITORY}:latest

      - name: Deploy to Portainer
        uses: kaykayyali/portainer-deploy-action@main
        with:
          portainer_url: 'http://portainer:9000'
          portainer_token: ${{ secrets.PORTAINER_API_TOKEN }}
          stack_name: 'iron-requiem'        # REQUIRED — set explicitly
          compose_file: 'docker-compose.yml'
          registry_url: 'git.homelab.local:8443'
          registry_user: ${{ github.actor }}
          registry_pass: ${{ secrets.GITEA_TOKEN }}
          healthcheck_url: 'https://iron-requiem.damascusfront.net/'

Inputs

Name Required Default Description
portainer_url No http://portainer:9000 Full URL to your Portainer instance
portainer_token Yes Portainer API access token. Long-lived OR short-lived (Hermes-issued).
endpoint_id No 1 Portainer environment/endpoint ID
stack_name Yes Name of the stack in Portainer. No default — must be set explicitly to avoid the bug where Gitea path-style repo names (e.g. owner/repo) produce invalid stack names.
compose_file No docker-compose.yml Path to compose file in your repo
env_file No Path to a .env file. Lines become stack env vars.
env_vars No Comma-separated KEY=VALUE pairs (override env_file on conflict).
image No Pin stack to this image:tag (e.g. git.homelab.local:8443/owner/repo:abc123d). Overrides image: line in compose_file.
image_digest No Pin to a specific sha256:... digest. For rollbacks.
rollback No false If true, re-deploys the previous image. Reads from container labels.
previous_image_count No 2 How many previous images to keep in stack history.
force_pull No true Always pullImage: true. Set false if you trust local cache.
prune No true Prune stopped containers on update.
registry_url No Private registry URL if Portainer needs auth to pull images
registry_user No Registry username (pair with registry_url)
registry_pass No Registry password or token (pair with registry_url)
healthcheck_url No URL to poll after deploy. HTTP 2xx = success. Empty = skip.
healthcheck_retries No 12 Max retries for healthcheck polling
healthcheck_delay No 10 Seconds between healthcheck attempts
fail_on_healthcheck No true If false, healthcheck failure is reported as a warning, not a fatal error.
output_file No Path to write a JSON result file. Schema below.

Output (when output_file is set)

{
  "status": "success",
  "action": "update",
  "stack_id": 7,
  "stack_name": "iron-requiem",
  "image": "git.homelab.local:8443/kaykayyali/iron-requiem:abc123d",
  "image_digest": "",
  "duration_seconds": 42,
  "healthcheck_status": "passed",
  "error": ""
}

The orchestrator parses this to decide what to tell Hermes. If the action fails, status is failure and error contains the diagnostic.

How it works

  1. Validate — check required inputs, mutual exclusions (image vs image_digest vs rollback).
  2. Env — merge env_file (if set) with env_vars (if set), env_vars wins on conflict.
  3. Compose — if image or image_digest is set, rewrite the first image: line in the compose.
  4. Check — fetch existing stacks from Portainer, look for a match by name.
  5. Rollback (if rollback=true) — read container labels to find the previous image digest.
  6. Create or Update — POST or PUT to Portainer with pullImage and (for update) prune flags.
  7. Verify — if healthcheck_url is set, poll it. Report success/failure/skip.
  8. Report — write JSON to output_file if set. Always log a one-line summary.

Pitfalls (read before filing issues)

  • stack_name is required. The old default (derive from repo name) was broken on Gitea because path-style names like kaykayyali/iron-requiem produce invalid Portainer stack names.
  • image vs image_digest are mutually exclusive. The action refuses to run if both are set.
  • Rollback relies on container labels. The gitea-deploy-orchestrator writes a gitea-deploy-orchestrator.image-digest label on every deploy. If you call this action directly (not via the orchestrator) and want rollback to work, you must set the label yourself or the previous image lookup will fail.
  • Env file format. KEY=VALUE per line, # for comments, blank lines ignored. Values are passed verbatim to Portainer (no shell expansion).
  • The jq requirement. This action needs jq in the runner image. The default runs-on: ubuntu-latest has it. The default container: alpine does not — add apk add jq first.

Registry Auth

If your compose file references images in a private registry (e.g., Gitea's container registry), Portainer needs credentials to pull them. Set all three:

registry_url: 'git.homelab.local:8443'
registry_user: ${{ github.actor }}
registry_pass: ${{ secrets.GITEA_TOKEN }}

These become a registries array in the Portainer API payload.

Secrets

Add these in your Gitea repo: Settings → Secrets → Add secret

Secret Purpose
PORTAINER_API_TOKEN Portainer access token with permission to manage stacks. In the orchestrator flow, this is replaced by a short-lived token issued by Hermes at deploy time — see gitea-deploy-orchestrator.
GITEA_TOKEN Gitea token with write:package scope (for pushing images + registry auth)

Healthcheck Verification

Without a healthcheck, the action can only confirm that Portainer accepted the compose file — not that containers actually started correctly. Set healthcheck_url to a known-good endpoint:

  • For web apps: https://app.example.com/health or just the root /
  • For APIs: a /healthz or /ready endpoint that returns 2xx
  • For services without HTTP: leave empty and rely on Portainer logs directly

If fail_on_healthcheck: false, a healthcheck failure is reported in the JSON output but does not fail the action. Use this for "fire and observe" deploys where you want to see what happens.

FAQ

Why doesn't this action build Docker images?

Separation of concerns. Build and push are two lines in your workflow — you control caching, platforms, tags, and registries. This action focuses on the one thing Portainer does uniquely: managing compose stacks.

Why is stack_name required?

The old default derived it from github.event.repository.name, which on Gitea includes the owner: kaykayyali/iron-requiem. Portainer rejected that. The new version makes you set it explicitly so the failure is at config time, not at deploy time.

How do I roll back a bad deploy?

Set rollback: true and the action re-deploys the previous image digest it found in container labels. The orchestrator exposes this as a webhook (/webhooks/rollback).

See also