root cf274f50fa fix: robust payloads, env vars, registry auth, healthcheck verification
- Use jq for JSON payload construction (no more string interpolation)
- Add env_vars input — parsed into proper Portainer env array
- Add registry_url/user/pass inputs for private registry auth
- Fix pullImage: true on create path (was only on update)
- Add healthcheck polling loop with configurable retries/delay
- Fix stack_name default — resolve in script, not action default expression
- Rewrite README with full build+push+deploy example
2026-05-30 00:07:18 +00:00

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.

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

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://192.168.1.200:9000'
          portainer_token: ${{ secrets.PORTAINER_API_TOKEN }}
          registry_url: 'git.homelab.local:8443'
          registry_user: ${{ github.actor }}
          registry_pass: ${{ secrets.GITEA_TOKEN }}
          env_vars: 'NODE_ENV=production,PUBLIC_API_URL=https://api.example.com'
          healthcheck_url: 'https://app.damascusfront.net/health'

Inputs

Name Required Default Description
portainer_url No http://portainer:9000 Full URL to your Portainer instance
portainer_token Yes Portainer API access token
endpoint_id No 1 Portainer environment/endpoint ID
stack_name No repo name Name of the stack in Portainer. Falls back to github.event.repository.name if empty.
compose_file No docker-compose.yml Path to compose file in your repo
env_vars No Comma-separated KEY=VALUE pairs passed as stack environment variables
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

How It Works

  1. Check — fetches existing stacks from Portainer, looks for a match by name
  2. Create or Update — if the stack exists, PUTs an update with pullImage: true and prune: true. If not, POSTs a new stack with optional registry credentials
  3. Verify — if healthcheck_url is set, polls the endpoint up to healthcheck_retries times with healthcheck_delay seconds between attempts. Fails the workflow if the healthcheck never passes

Both create and update paths pass pullImage: true, so :latest tags always pull fresh images.

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, so the target Docker engine can authenticate.

Secrets

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

Secret Purpose
PORTAINER_API_TOKEN Portainer access token with permission to manage stacks
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

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.

Can I use this with images from Docker Hub?

Yes — just omit registry_url/registry_user/registry_pass. Portainer pulls from Docker Hub by default.

What if my stack needs secrets, not just env vars?

Use Portainer's built-in secret management in the UI for sensitive values. The env_vars input is for non-sensitive configuration (NODE_ENV, PUBLIC_API_URL, etc.). Never pass tokens or passwords through env_vars.

Description
No description provided
Readme 45 KiB
Languages
Shell 100%