How to Self-Host Drone CI: Container-Native CI/CD 2026
TL;DR
Drone CI (Apache 2.0, ~31K GitHub stars, Go) is a container-native CI/CD platform designed for self-hosting. Every build step runs inside a Docker container — no more Jenkins plugins or brittle agent configurations. Pipelines are defined in .drone.yml (YAML), runners execute Docker containers on your hardware, and the server is a single lightweight Go binary. Works with Gitea, GitHub, GitLab, Bitbucket, and Gitea/Forgejo out of the box via OAuth2.
Key Takeaways
- Drone CI: Apache 2.0, ~31K stars, Go — Docker-native CI/CD, one YAML per repo
- Container-first: Every step runs in its own Docker container — fully reproducible builds
- Gitea native: First-class Gitea/Forgejo integration via OAuth2
- Secrets management: Encrypted secrets per-repo or org-wide
- Multi-runner: Scale by adding more Docker runners on any machine
- Exec runner: For builds that need bare-metal access (not in Docker)
Drone vs Woodpecker vs Gitea Actions
| Feature | Drone CI | Woodpecker CI | Gitea Actions |
|---|---|---|---|
| License | Apache 2.0 | Apache 2.0 | Built-in |
| GitHub Stars | ~31K | ~4K | — |
| Pipeline format | Drone YAML | Woodpecker YAML | GitHub Actions YAML |
| GitHub Actions compat | No | Partial | Yes |
| Multi-runner | Yes | Yes | Yes |
| Docker runner | Yes | Yes | Yes |
| Bare-metal runner | Yes (exec) | Yes | Yes |
| Secrets | Encrypted | Encrypted | Encrypted |
| Parallelism | Yes | Yes | Yes |
| Best with | Gitea/GitHub | Gitea/Forgejo | Gitea/Forgejo |
Part 1: Docker Setup
# docker-compose.yml
services:
drone:
image: drone/drone:latest
container_name: drone
restart: unless-stopped
ports:
- "8080:80"
volumes:
- drone_data:/data
environment:
# Gitea OAuth2 integration:
DRONE_GITEA_SERVER: "https://git.yourdomain.com"
DRONE_GITEA_CLIENT_ID: "${GITEA_CLIENT_ID}"
DRONE_GITEA_CLIENT_SECRET: "${GITEA_CLIENT_SECRET}"
# Drone server config:
DRONE_RPC_SECRET: "${DRONE_RPC_SECRET}"
DRONE_SERVER_HOST: "ci.yourdomain.com"
DRONE_SERVER_PROTO: "https"
DRONE_TLS_AUTOCERT: "false"
# Admin user (your Gitea username):
DRONE_USER_CREATE: "username:admin,admin:true"
# Database:
DRONE_DATABASE_DATASOURCE: "/data/database.sqlite"
DRONE_DATABASE_DRIVER: "sqlite3"
volumes:
drone_data:
# .env
DRONE_RPC_SECRET=$(openssl rand -hex 16)
# GITEA_CLIENT_ID and GITEA_CLIENT_SECRET from Gitea OAuth2 app setup below
docker compose up -d
Part 2: HTTPS with Caddy
ci.yourdomain.com {
reverse_proxy localhost:8080
}
Part 3: Gitea OAuth2 Setup
Create an OAuth2 application in Gitea for Drone:
- Gitea: User Settings → Applications → OAuth2 Applications → Create OAuth2 Application
- Application Name:
Drone CI - Redirect URI:
https://ci.yourdomain.com/login - Copy Client ID and Client Secret to
.env
Part 4: Add a Docker Runner
The server only coordinates — runners do the actual build work:
# Add to docker-compose.yml:
services:
drone-runner:
image: drone/drone-runner-docker:latest
container_name: drone_runner
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
DRONE_RPC_PROTO: "https"
DRONE_RPC_HOST: "ci.yourdomain.com"
DRONE_RPC_SECRET: "${DRONE_RPC_SECRET}"
DRONE_RUNNER_CAPACITY: 2 # Concurrent builds
DRONE_RUNNER_NAME: "docker-runner-1"
DRONE_LOGS_TRACE: "false"
# Scale runners on additional machines:
docker run -d \
-e DRONE_RPC_PROTO=https \
-e DRONE_RPC_HOST=ci.yourdomain.com \
-e DRONE_RPC_SECRET=your-rpc-secret \
-e DRONE_RUNNER_CAPACITY=4 \
-e DRONE_RUNNER_NAME=worker-2 \
-v /var/run/docker.sock:/var/run/docker.sock \
drone/drone-runner-docker:latest
Part 5: Pipeline YAML
Create .drone.yml in your repository root:
Basic pipeline
# .drone.yml
kind: pipeline
type: docker
name: default
steps:
- name: test
image: node:20-alpine
commands:
- npm ci
- npm test
- name: build
image: node:20-alpine
commands:
- npm run build
when:
branch:
- main
Multi-stage with Docker build
kind: pipeline
type: docker
name: default
steps:
- name: test
image: python:3.12-slim
commands:
- pip install -r requirements.txt
- pytest
- name: build-image
image: plugins/docker
settings:
registry: git.yourdomain.com
repo: git.yourdomain.com/myorg/myapp
tags:
- latest
- ${DRONE_COMMIT_SHA:0:8}
username:
from_secret: docker_username
password:
from_secret: docker_password
- name: deploy
image: alpine
commands:
- apk add --no-cache openssh
- ssh deploy@prod.yourdomain.com "docker pull git.yourdomain.com/myorg/myapp:latest && docker compose up -d"
environment:
SSH_KEY:
from_secret: deploy_ssh_key
when:
branch:
- main
Parallel steps
kind: pipeline
type: docker
name: default
steps:
- name: backend-test
image: python:3.12
commands:
- cd backend && pip install -r requirements.txt && pytest
- name: frontend-test
image: node:20
commands:
- cd frontend && npm ci && npm test
# These two steps run in parallel (different resources, no depends_on)
Trigger conditions
trigger:
branch:
- main
- release/*
event:
- push
- pull_request
- tag
Part 6: Secrets Management
# Install Drone CLI:
curl -L https://github.com/harness/drone-cli/releases/latest/download/drone_linux_amd64.tar.gz | tar zx
sudo mv drone /usr/local/bin/
# Configure CLI:
export DRONE_SERVER=https://ci.yourdomain.com
export DRONE_TOKEN=your-personal-token # From Drone UI → Account → Token
# Add secret to a repo:
drone secret add --repository myorg/myrepo \
--name docker_password \
--data "your-registry-password"
# List secrets:
drone secret ls --repository myorg/myrepo
# Add org-wide secret:
drone orgsecret add myorg docker_password "your-registry-password"
Use in pipeline:
settings:
password:
from_secret: docker_password
Part 7: Exec Runner (Bare-Metal)
For builds that need the host's Docker daemon, GPU, or specific hardware:
# Install exec runner on the build host:
curl -L https://github.com/drone-runners/drone-runner-exec/releases/latest/download/drone_runner_exec_linux_amd64.tar.gz | tar zx
sudo mv drone-runner-exec /usr/local/bin/
# Configure:
sudo mkdir -p /etc/drone-runner-exec
cat > /etc/drone-runner-exec/config << EOF
DRONE_RPC_PROTO=https
DRONE_RPC_HOST=ci.yourdomain.com
DRONE_RPC_SECRET=your-rpc-secret
DRONE_RUNNER_NAME=exec-runner
DRONE_RUNNER_CAPACITY=1
EOF
# Run as systemd service:
sudo drone-runner-exec service install
sudo drone-runner-exec service start
# Pipeline targeting exec runner:
kind: pipeline
type: exec
name: gpu-build
platform:
os: linux
arch: amd64
steps:
- name: train
commands:
- python train.py --gpu
Maintenance
# Update Drone:
docker compose pull
docker compose up -d
# Backup:
tar -czf drone-backup-$(date +%Y%m%d).tar.gz \
$(docker volume inspect drone_drone_data --format '{{.Mountpoint}}')
# Logs:
docker compose logs -f drone
docker compose logs -f drone-runner
# Check runner status:
curl -s https://ci.yourdomain.com/api/runners \
-H "Authorization: Bearer your-admin-token" | jq
See all open source CI/CD and DevOps tools at OSSAlt.com/categories/devops.