Skip to main content

How to Self-Host Drone CI: Container-Native CI/CD 2026

·OSSAlt Team
droneci-cddevopsself-hostingdockergitea2026

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

FeatureDrone CIWoodpecker CIGitea Actions
LicenseApache 2.0Apache 2.0Built-in
GitHub Stars~31K~4K
Pipeline formatDrone YAMLWoodpecker YAMLGitHub Actions YAML
GitHub Actions compatNoPartialYes
Multi-runnerYesYesYes
Docker runnerYesYesYes
Bare-metal runnerYes (exec)YesYes
SecretsEncryptedEncryptedEncrypted
ParallelismYesYesYes
Best withGitea/GitHubGitea/ForgejoGitea/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:

  1. Gitea: User Settings → Applications → OAuth2 Applications → Create OAuth2 Application
  2. Application Name: Drone CI
  3. Redirect URI: https://ci.yourdomain.com/login
  4. 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.

Comments