DEV Community

Cover image for Automating Docker Deployments with GitHub Actions, Cloudflare Tunnels, and Portainer
shape93
shape93

Posted on

Automating Docker Deployments with GitHub Actions, Cloudflare Tunnels, and Portainer

Image description
During these Easter holidays, I found myself debating whether to experiment with my self-hosted home lab setup. Spoiler alert: I did.

When I started this journey years ago, the possibilities seemed endless—even without top-tier hardware. But as my Docker services multiplied, managing them became messy. Without proper version control, backups, and orchestration, things spiraled quickly. Enter webhook-driven deployments—a flexible approach using GitHub Actions, Cloudflare Tunnels, and Portainer. Let’s dive in!


⚠️ Disclaimers

  1. Cloudflare Tunnels: This guide assumes you’ve already set up Cloudflare Tunnels to expose services remotely.
  2. Portainer Business Edition: Required for GitOps/webhook features. You can get a free license for small setups (up to 3 nodes).

Step 1: Setting Up the GitHub Repository

Start by creating a GitHub repository (private or public—sensitive data will use secrets). Clone it locally or use GitHub Codespaces for editing.

Repo setup

Example docker-compose.yml (Paperless-ngx)

services:
  broker:
    image: redis:7
    restart: unless-stopped
    volumes:
      - redisdata:/data

  db:
    image: postgres:15
    restart: unless-stopped
    volumes:
      - /mnt/sdb1/paperless-new/db:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: paperless
      POSTGRES_USER: paperless
      POSTGRES_PASSWORD: paperless

  webserver:
    image: ghcr.io/paperless-ngx/paperless-ngx:latest
    restart: unless-stopped
    depends_on:
      - db
      - broker
    ports:
      - "8000:8000"
    healthcheck:
      test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
      interval: 30s
      timeout: 10s
      retries: 5
    volumes:
      - /mnt/sdb1/paperless-new/data:/usr/src/paperless/data
      - /mnt/sdb1/paperless-new/media:/usr/src/paperless/media
      - /mnt/sdb1/export:/usr/src/paperless/export
    env_file: stack.env
    environment:
      PAPERLESS_REDIS: redis://broker:6379
      PAPERLESS_DBHOST: db

volumes:
  redisdata:
Enter fullscreen mode Exit fullscreen mode

Step 2: GitHub Authentication for Portainer

Create a GitHub Personal Access Token

  1. Navigate to GitHub Tokens.
  2. Name the token (e.g., Portainer-GitOps) and set expiration.
  3. Grant repo permissions (read/write for private repos). GitHub token permissions
  4. Copy the token—you’ll need it for Portainer.

Step 3: Configuring Portainer Stack

  1. In Portainer, navigate to Stacks > Add Stack.
  2. Under Build Method, select Repository.
  3. Enable authentication and input:
    • Username: Your GitHub handle
    • Password: The token from Step 2
  4. Configure GitOps:
    • Repository URL: https://github.com/your-username/repo-name
    • Compose Path: docker-compose.yml (adjust if needed)
    • Enable Automatic Updates: Toggle Webhook
    • Enable Re-pull image and Redeploy when changes are pulled

Portainer stack setup

  1. Add environment variables (e.g., worker counts):
   PAPERLESS_WEBSERVER_WORKERS=1
   PAPERLESS_TASK_WORKERS=1
Enter fullscreen mode Exit fullscreen mode
  1. Deploy the stack!

Step 4: Cloudflare Tunnel Service Token

  1. In Cloudflare Zero Trust, go to Access > Service Auth.
  2. Create a new Service Token. Note the Client ID and Secret.
  3. Edit your Portainer application under Applications:
    • Add a Bypass policy tied to the service token. Cloudflare policy setup

Step 5: GitHub Actions Workflow

Configure Secrets in GitHub

Under repo Settings > Secrets > Actions, add:

  • PORTAINER_WEBHOOK_URL: From Portainer’s webhook setup
  • CF_ACCESS_CLIENT_ID: Cloudflare Service Token Client ID
  • CF_ACCESS_CLIENT_SECRET: Cloudflare Service Token Secret

Create the Workflow File

Add .github/workflows/deploy.yml:

name: Update Portainer Stack

on:
  push:
    branches: [main]
  workflow_dispatch:  # Manual trigger

jobs:
  update-stack:
    # Only run if commit message contains [deploy]
    if: contains(github.event.head_commit.message, '[deploy]')
    runs-on: ubuntu-latest

    steps:
      - name: Trigger Portainer Webhook
        env:
          PORTAINER_WEBHOOK_URL: ${{ secrets.PORTAINER_WEBHOOK_URL }}
          CF_ACCESS_CLIENT_ID: ${{ secrets.CF_ACCESS_CLIENT_ID }}
          CF_ACCESS_CLIENT_SECRET: ${{ secrets.CF_ACCESS_CLIENT_SECRET }}
        run: |
          curl -X POST "$PORTAINER_WEBHOOK_URL" \
            -H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
            -H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET"
Enter fullscreen mode Exit fullscreen mode

Step 6: Testing the Pipeline

  1. Commit changes with [deploy] in the message:
   git commit -m "chore: update compose [deploy]"
Enter fullscreen mode Exit fullscreen mode
  1. Push to trigger the action: GitHub Actions success

Portainer will now redeploy your stack automatically! Failed deployments roll back gracefully, and you can reuse the repo as a template for future projects.


Final Thoughts

This setup brings GitOps practices to self-hosting—version control, CI/CD, and secure access. Suggestions? Let me know! 🚀


Top comments (0)