DevOps

How to Deploy Woodpecker CI 1.0.3 in Docker Swarm Behind Caddy v2.7.5

Woodpecker is a simple, highly extensible CI engine. It focuses on executing pipelines inside containers. If you use containers in your daily workflow, you'll love Woodpecker.

Rajasekhar Gundala··6 min read

Woodpecker is a simple CI engine with tremendous extensibility. It focuses entirely on executing pipelines inside containers. If you are using containers in your daily workflow, you will love Woodpecker.

In this post, I am going to show you how to deploy Woodpecker CI 1.0.3, a container-native continuous delivery platform, into our Docker Swarm Cluster using Docker Compose, sitting securely behind Caddy v2.7.5.

If you want to learn more about Woodpecker CI, please check out the links below:

  1. Official Website
  2. Documentation
  3. GitHub Repository

Let’s start with the actual deployment.

Prerequisites

Please ensure you fulfill the following requirements before proceeding with the deployment:

  1. A Docker Swarm Cluster configured with GlusterFS for persistent storage.
  2. Caddy deployed as the ingress reverse proxy to expose microservices externally.

Introduction to Woodpecker CI

Woodpecker CI is a simple CI engine born as a fork of Drone CI. It focuses on executing pipelines inside isolated Docker containers that are automatically downloaded at runtime.

Woodpecker uses a pipeline file (.woodpecker.yml) that you commit directly to your Git repository. This file contains a single pipeline or multiple pipelines instructing the server on how to build an application and publish the Docker container to a registry of your choice.

Pipeline steps can be named whatever you like. You can run any command in the commands section, as steps are simply containers and file changes are incremental.

Persisting Woodpecker Data with GlusterFS

Containers are fast to deploy and make efficient use of system resources. However, their filesystems are ephemeral. If a container restarts, local data is lost.

To overcome this, we use GlusterFS. I previously set up a replicated GlusterFS volume to ensure data is mirrored across all nodes in the cluster.

GlusterFS Replicated Volume

The volume is mounted across all nodes. When data is written to the /mnt partition, it is instantly replicated.

If any node fails, the application automatically restarts on another node without losing data. This is the primary advantage of a replicated volume.

For disaster recovery, we need to persist the /var/lib/woodpecker/ directory.

Create the required persistent folder in the /mnt directory:

cd /mnt
sudo mkdir -p woodpeckerdata

Watch the video below for a complete guide on setting up a GlusterFS Replicated Volume.


Prepare the Deployment Environment

We will use Docker Compose to define the deployment environment.

Navigate to the /opt directory on your Swarm manager node and create the configuration directory for Woodpecker:

cd /opt
sudo mkdir -p woodpecker
cd woodpecker
sudo touch woodpecker.yml

Woodpecker Docker Compose Configuration

Open woodpecker.yml using your editor:

sudo nano woodpecker.yml

Paste the following Docker Compose configuration. I am utilizing SQLite as a backend database for simplicity. Note that I am using Gitea as my Git provider in the environment variables.

version: "3.7"

services:
  woodpecker-server:
    image: woodpeckerci/woodpecker-server:latest-alpine
    volumes:
      - /mnt/woodpeckerdata:/var/lib/woodpecker/
    environment:
      - WOODPECKER_HOST=[https://woodpecker.example.com](https://woodpecker.example.com)
      - WOODPECKER_GITEA=true
      - WOODPECKER_GITEA_CLIENT=gitea-client-id
      - WOODPECKER_GITEA_SECRET=gitea-client-secret
      - WOODPECKER_GITEA_URL=[https://gitea.example.com](https://gitea.example.com)
      - WOODPECKER_AGENT_SECRET=agent-secret
      - WOODPECKER_ADMIN=username
      - WOODPECKER_REPO_OWNERS=username
    networks:
      - caddy
    deploy:
      placement:
        constraints: [node.role == worker]
      replicas: 1
      update_config:
        parallelism: 2
        delay: 10s
      restart_policy:
        condition: on-failure

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:latest-alpine
    command: agent
    depends_on:
      - woodpecker-server
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=agent-secret
      - WOODPECKER_MAX_PROCS=10
      - WOODPECKER_BACKEND=docker
      - WOODPECKER_HOST=[https://woodpecker.example.com](https://woodpecker.example.com)
    networks:
      - caddy
    deploy:
      placement:
        constraints: [node.role == worker]
      replicas: 1
      update_config:
        parallelism: 2
        delay: 10s
      restart_policy:
        condition: on-failure

volumes:
  woodpeckerdata:
    driver: "local"

networks:
  caddy:
    external: true
    attachable: true

Watch the video below to see a walkthrough of deploying Woodpecker CI in a Docker Swarm Cluster.


Caddyfile Configuration

Here is the production-ready Caddyfile required to expose Woodpecker securely. Learn more about writing Caddyfiles here.

{
    email you@example.com
    default_sni woodpecker
    cert_issuer acme
    acme_ca [https://acme-v02.api.letsencrypt.org/directory](https://acme-v02.api.letsencrypt.org/directory)
    
    servers {
        metrics
        protocols h1 h2c h3
        strict_sni_host on
        trusted_proxies cloudflare {
            interval 12h
            timeout 15s
        }
    }
}

woodpecker.example.com {
    log {
        output file /var/log/caddy/woodpecker.log {
            roll_size 20mb
            roll_keep 2
            roll_keep_for 6h
        }
        format console
        level error
    }
    encode gzip zstd
    reverse_proxy woodpecker-server:8000
}

Full Stack Deployment (Combined)

If you prefer to deploy Caddy and Woodpecker together in a single stack, here is the combined docker-compose.yml file.

version: "3.7"

services:
  caddy:
    image: tuneitme/caddy
    ports:
      - "80:80"
      - "443:443"
    networks:
      - caddy
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - /mnt/caddydata:/data
      - /mnt/caddyconfig:/config
      - /mnt/caddylogs:/var/log/caddy
    deploy:
      placement:
        constraints:
          - node.role == manager
      replicas: 1
      update_config:
        parallelism: 2
        delay: 10s
      restart_policy:
        condition: on-failure

  woodpecker-server:
    image: woodpeckerci/woodpecker-server:latest-alpine
    volumes:
      - /mnt/woodpeckerdata:/var/lib/woodpecker/
    environment:
      - WOODPECKER_HOST=[https://woodpecker.example.com](https://woodpecker.example.com)
      - WOODPECKER_GITEA=true
      - WOODPECKER_GITEA_CLIENT=gitea-client-id
      - WOODPECKER_GITEA_SECRET=gitea-client-secret
      - WOODPECKER_GITEA_URL=[https://gitea.example.com](https://gitea.example.com)
      - WOODPECKER_AGENT_SECRET=agent-secret
      - WOODPECKER_ADMIN=username
      - WOODPECKER_REPO_OWNERS=username
    networks:
      - caddy
    deploy:
      placement:
        constraints: [node.role == worker]
      replicas: 1
      update_config:
        parallelism: 2
        delay: 10s
      restart_policy:
        condition: on-failure

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:latest-alpine
    command: agent
    depends_on:
      - woodpecker-server
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=agent-secret
      - WOODPECKER_MAX_PROCS=10
      - WOODPECKER_BACKEND=docker
      - WOODPECKER_HOST=[https://woodpecker.example.com](https://woodpecker.example.com)
    networks:
      - caddy
    deploy:
      placement:
        constraints: [node.role == worker]
      replicas: 1
      update_config:
        parallelism: 2
        delay: 10s
      restart_policy:
        condition: on-failure

volumes:
  caddydata:
    driver: "local"
  caddyconfig:
    driver: "local"
  caddylogs:
    driver: "local"
  woodpeckerdata:
    driver: "local"

networks:
  caddy:
    external: true
    attachable: true

I used a custom Caddy Docker image bundled with specific plugins (like Cloudflare DNS) for my environment: Tuneit Caddy Docker Image

Deploy Woodpecker to Docker Swarm

Now it’s time to deploy our woodpecker.yml using the following command:

docker stack deploy --compose-file woodpecker.yml woodpecker

In Docker Swarm, whatever you deploy via compose is called a “stack,” and it contains multiple “services.”

Check the status of the deployment to ensure it scheduled properly:

docker stack ps woodpecker

Check the Woodpecker server logs to ensure successful startup:

docker service logs woodpecker_woodpecker-server

You will notice that Caddy automatically intercepts the traffic, redirects it to HTTPS, and provisions Let’s Encrypt certificates.

Access and Configure Woodpecker

Open your browser and navigate to woodpecker.example.com (ensure you replace example.com with your actual domain). It will securely redirect to https://woodpecker.example.com/welcome.

Ensure you have configured a DNS A-Record or CNAME pointing woodpecker.example.com to your Swarm ingress load balancer.

Reference Images from the Deployment:

Woodpecker Screenshot 1

Woodpecker Screenshot 2

Woodpecker Screenshot 3

Woodpecker Screenshot 4

Woodpecker Screenshot 5

Woodpecker Screenshot 6

Woodpecker Screenshot 7

Woodpecker Screenshot 8

Woodpecker Screenshot 9

Woodpecker Screenshot 10

Woodpecker Screenshot 11

Woodpecker Screenshot 12

Woodpecker Screenshot 13

Woodpecker Screenshot 14

Woodpecker Screenshot 15

Woodpecker Screenshot 16

The deployment of Woodpecker CI behind Caddy in our Docker Swarm cluster is complete!

If you enjoyed this tutorial, please share your thoughts in the comments below. It helps me bring more self-hosted open-source content to the community.

Stay tuned for more deployment guides!

Share
Written by
Rajasekhar Gundala

Senior Infrastructure & Web Platform Leader.

Continue reading

Weekly Engineering Notes.

A weekly digest on infrastructure, observability, Rust, and the open web. No spam, just technical signals.

Free. Unsubscribe in one click.