---
layout: post
title: "Migrating from Drone CI to Woodpecker CI"
date: 2026-04-18
tags: [ci, devops, drone, woodpecker, gitea, docker]
---

[Woodpecker CI](https://woodpecker-ci.org/) is a community fork of Drone CI, created when Drone changed its licensing model around 2021 and moved features behind a paid enterprise tier. Woodpecker continues as a fully open-source (Apache 2.0) project with active development, while Drone itself has been largely absorbed into Harness CI and is in maintenance mode.

If you're running Drone with Gitea, Woodpecker is a natural migration target — it has strong Gitea integration and an actively maintained codebase. Here is what the migration actually involves, based on a real setup using Docker Compose and Traefik.

## Docker Compose

The server and runner/agent images change, and there are several environment variable renames. Woodpecker also uses different ports: the web UI runs on **8000** (not 80), and the agent communicates with the server over gRPC on port **9000**.

```yaml
  woodpecker:
    image: woodpeckerci/woodpecker-server:latest
    container_name: woodpecker
    restart: unless-stopped
    environment:
      - WOODPECKER_HOST=https://drone.example.org   # full URL including scheme
      - WOODPECKER_GITEA=true
      - WOODPECKER_GITEA_URL=https://gitea.example.org
      - WOODPECKER_CRON_DISABLED=true
    env_file:
      - ./woodpecker.env
    volumes:
      - woodpecker:/var/lib/woodpecker   # different path from Drone's /data
    networks:
      - services
      - woodpecker
    labels:
      traefik.enable: "true"
      traefik.http.services.woodpecker.loadbalancer.server.port: 8000  # not 80!

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:latest
    restart: unless-stopped
    environment:
      - WOODPECKER_SERVER=woodpecker:9000   # gRPC port, not HTTP
      - WOODPECKER_GRPC_SECURE=false
    env_file:
      - ./woodpecker.env
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - woodpecker
```

Make sure both the server and agent containers are on the **same Docker network**. If the agent can resolve the server's hostname but port 9000 is refused, it is likely a startup ordering issue — simply restarting the agent container after the server is fully up will fix it.

## Environment Variables

Your `drone.env` secrets need to be renamed in a new `woodpecker.env` file:

| `drone.env` | `woodpecker.env` |
|---|---|
| `DRONE_GITEA_CLIENT_ID` | `WOODPECKER_GITEA_CLIENT` |
| `DRONE_GITEA_CLIENT_SECRET` | `WOODPECKER_GITEA_SECRET` |
| `DRONE_RPC_SECRET` | `WOODPECKER_AGENT_SECRET` |
| `DRONE_COOKIE_SECRET` | `WOODPECKER_SECRET` |

Note that `WOODPECKER_AGENT_SECRET` (RPC) and `WOODPECKER_SECRET` (cookie signing) are **separate variables** — do not conflate them.

You also need to set `WOODPECKER_ADMIN` to your Gitea username, otherwise Woodpecker will not allow you to register or access the admin panel:

```
WOODPECKER_ADMIN=yourusername
```

You will also need to create a **new OAuth application in Gitea** pointing at your Woodpecker URL, since the callback URL changes.

## Pipeline Files

**You must rename `.drone.yml` to `.woodpecker.yml`.** Despite what the documentation suggests, Woodpecker (at least on the `next` tag) does not fall back to `.drone.yml` — it will silently ignore it, and pushes will not trigger any pipelines.

The pipeline syntax is largely compatible. Steps, images, commands, secrets, and `when` conditions mostly work as-is. A few things to check:

**Move `trigger` to the pipeline level.** In Drone it was sometimes written under individual steps; in Woodpecker it must be a top-level key.


**Drone plugins still work.** They are just Docker images, so plugins like `drillster/drone-rsync` are fully compatible.

**`DRONE_*` environment variables become `CI_*`.** If your pipeline scripts reference injected variables like `DRONE_COMMIT`, these are renamed in Woodpecker (e.g. `CI_COMMIT_SHA`).

## What Does Not Migrate

Woodpecker does not import Drone's database. This means:

- **Build history is lost** — you start with a clean slate
- **Secrets must be re-entered** via the Woodpecker UI for each repository
- **Repos must be re-activated** in Woodpecker

If retaining build history matters, you can keep the old Drone instance running read-only alongside Woodpecker temporarily, since they can coexist on different hostnames.

## Summary

For a straightforward Drone + Gitea + Docker Compose setup, the migration is low effort. The main steps are:

1. Update `docker-compose.yml` with the new images and port numbers
2. Rename secrets in your env file and set `WOODPECKER_ADMIN`
3. Create a new OAuth app in Gitea
4. Rename `.drone.yml` to `.woodpecker.yml` in each repository
5. Move any pipeline-level `trigger` blocks to the top level
6. Re-activate repos and re-enter secrets in the Woodpecker UI
