How to Install Drone CI on Ubuntu 24.04 VPS: Container-Native Continuous Delivery
Drone CI is a container-native continuous integration and delivery platform that turns every pipeline step into a Docker container. Every build runs in a clean, reproducible environment, which makes "works on my machine" problems almost impossible. This guide walks through deploying a production-grade Drone CI server on Ubuntu 24.04, from Docker install to Nginx TLS termination, OAuth with GitHub or Gitea, .drone.yml pipelines, secrets, image promotion, and scaling runners.
Note on branding: Drone was acquired by Harness in 2020 and the commercial product was rebranded. The open-sourcedrone/droneanddrone-runner-dockerimages on Docker Hub are still published under the Apache 2.0 license and remain fully maintained. Self-hosting Drone is free, unlimited, and the workflow has not changed.
Table of Contents
What is Drone CI?
Drone CI is an open-source, container-native continuous integration system originally created by Brad Rydzewski. It consists of two core components: a server that handles webhooks, UI, API, and database state, and one or more runners that execute pipeline steps in Docker containers. The server speaks to your Git provider (GitHub, Gitea, GitLab, Bitbucket) over OAuth, receives webhooks on push or pull request events, and dispatches work to runners over a gRPC-based RPC protocol.
Every step in a Drone pipeline runs inside a container you specify. Want Node 20? Pull node:20-alpine. Need Go 1.22? Pull golang:1.22. Need both in the same pipeline? List them as separate steps and they will run sequentially in their own clean environments. This container-native design eliminates host-level toolchain management: the runner host only needs Docker, nothing else.
Common workflows built with Drone include pull-request validation (lint, unit tests, integration tests on every PR), container image builds published to a registry with semver tags, deploy pipelines that SSH into servers or apply Kubernetes manifests, release automation that cuts a GitHub release and uploads artifacts when a tag is pushed, and scheduled jobs for nightly test suites or cache warming. The .drone.yml file lives at the repository root and is version-controlled alongside application code, so pipeline changes go through the same review process as any other code change.
Drone's plugin ecosystem covers hundreds of common tasks: plugins/docker for image builds, plugins/s3 for artifact uploads, plugins/slack for notifications, plugins/ssh for remote execution, appleboy/drone-scp for file transfers, and many more. Every plugin is itself a container image, so running one is the same as running any other step.
Why Self-Host Your CI/CD Pipeline?
Hosted CI services (GitHub Actions, CircleCI, Travis, Buildkite cloud) are convenient, but self-hosting Drone on your own VPS offers distinct advantages:
- Predictable flat-rate cost -- Hosted CI is priced per build minute or per concurrent job. Heavy pipelines can cost hundreds of dollars per month. A single VPS running Drone costs the same whether you run 10 builds or 10,000.
- No minute limits or queue throttling -- When GitHub Actions free minutes run out, builds stop. Your own runner queues work until the CPU is saturated and then keeps going.
- Source code stays on your infrastructure -- For proprietary codebases, regulated industries, or clients with strict data-residency requirements, keeping source, build artifacts, and secrets on a known server (not a shared cloud) simplifies compliance.
- Unlimited concurrent jobs -- Add more runners for more parallelism. No plan upgrades, no rate limits.
- Custom hardware -- Need a runner with a GPU, a specific CPU instruction set, or local NVMe? Provision a VPS that matches and point a runner at it.
- Full control of the supply chain -- You pin exact image versions, mirror registries internally, and decide what reaches the build environment.
- Works with self-hosted Git -- If you already run Gitea, Drone integrates natively and your entire dev loop lives on your own servers.
Cost Comparison: Self-Hosted vs. Hosted CI
| Scenario | GitHub Actions (paid) | CircleCI Performance | Self-Hosted Drone on VPS |
|---|---|---|---|
| Monthly cost (moderate use) | ~$40-120/mo (per-minute) | $30/mo + usage | EUR 19.99/mo (flat) |
| Concurrent jobs | 20 (paid tier) | 30 (Performance) | Unlimited (runner capacity) |
| Build minutes cap | 3,000-50,000 | 80,000 credits | None |
| Source leaves your server? | Yes | Yes | No |
| Custom runner hardware | Limited | Limited | Yes (any VPS size) |
| Typical cost at 500 builds/month | ~$60/mo | ~$50/mo | EUR 19.99/mo |
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access to your server
- A domain name pointing to your VPS (e.g.
drone.yourdomain.com) -- required for OAuth callback URLs and TLS - A GitHub, Gitea, or GitLab account with admin rights on at least one repository you want to build
- At least 4 GB of RAM (8-12 GB recommended for parallel builds)
- 20 GB+ of free disk space for Docker images and build caches
Recommended Plan: CloudCore Professional>
For a Drone server plus one or two concurrent runners, we recommend the CloudCore Professional plan:>
- 6 vCPU cores
- 12 GB RAM
- 100 GB NVMe SSD
- Unmetered bandwidth
- EUR 19.99/month>
This gives comfortable headroom for parallel Node, Go, or Docker-in-Docker builds while keeping the Drone server responsive.
Connect to your server via SSH:
ssh root@your-server-ipAlso make sure your domain's A record points to the server's IP. You can verify from the server:
dig +short drone.yourdomain.comIt should return your VPS IP.
Step 1: Update System Packages
Start by updating the package index and upgrading installed packages:
sudo apt update && sudo apt upgrade -yInstall a few utilities you will need later:
sudo apt install -y curl ca-certificates gnupg lsb-release ufwIf the kernel was updated, reboot before continuing:
sudo rebootStep 2: Install Docker and Docker Compose
Drone runs entirely in Docker. If you have not already installed Docker, follow the abridged steps below, or see the dedicated guide: How to Install Docker on Ubuntu.
Add Docker's official GPG key and repository:
sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Install Docker Engine and the Compose plugin:
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginVerify both:
docker --version
docker compose versionExpected output:
Docker version 27.0.3, build 7d4bcd8
Docker Compose version v2.29.1Allow your non-root user to run Docker (optional but recommended):
sudo usermod -aG docker $USER
newgrp dockerOpen the firewall for SSH, HTTP, and HTTPS:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enableStep 3: Register an OAuth Application
Drone authenticates users and pulls repositories through OAuth. Register an application with your Git provider before bringing up the stack.
Option A: GitHub OAuth App
Drone CI
- Homepage URL: https://drone.yourdomain.com
- Authorization callback URL: https://drone.yourdomain.com/login
Option B: Gitea OAuth Application
If you have Gitea running on the same network:
Drone CI
- Redirect URI: https://drone.yourdomain.com/login
Generate an RPC Shared Secret
The Drone server and its runners authenticate each other with a shared secret. Generate a strong one now:
openssl rand -hex 16Save the output -- you will use it for DRONE_RPC_SECRET.
Step 4: Create the Docker Compose Stack
Create a working directory and the Compose file.
sudo mkdir -p /opt/drone
sudo chown $USER:$USER /opt/drone
cd /opt/droneCreate an .env file for your secrets:
cat > /opt/drone/.env <<'EOF' DRONE_SERVER_HOST=drone.yourdomain.com DRONE_SERVER_PROTO=https--- GitHub OAuth (comment out if using Gitea) ---
DRONE_GITHUB_CLIENT_ID=your_github_client_id DRONE_GITHUB_CLIENT_SECRET=your_github_client_secret--- Gitea OAuth (uncomment to use Gitea instead of GitHub) ---
DRONE_GITEA_SERVER=https://git.yourdomain.com
DRONE_GITEA_CLIENT_ID=your_gitea_client_id
DRONE_GITEA_CLIENT_SECRET=your_gitea_client_secret
DRONE_RPC_SECRET=paste_the_openssl_rand_hex_output_here DRONE_USER_CREATE=username:your-github-or-gitea-username,admin:true EOF chmod 600 /opt/drone/.env
Replace the placeholder values with real credentials. The DRONE_USER_CREATE line pre-promotes your account to admin on first login.
Create the docker-compose.yml:
cat > /opt/drone/docker-compose.yml <<'EOF' version: "3.8"services: drone-server: image: drone/drone:2 container_name: drone-server restart: unless-stopped ports: - "127.0.0.1:8080:80" volumes: - drone-data:/data environment: - DRONE_SERVER_HOST=${DRONE_SERVER_HOST} - DRONE_SERVER_PROTO=${DRONE_SERVER_PROTO} - DRONE_RPC_SECRET=${DRONE_RPC_SECRET} - DRONE_USER_CREATE=${DRONE_USER_CREATE} # GitHub - DRONE_GITHUB_CLIENT_ID=${DRONE_GITHUB_CLIENT_ID} - DRONE_GITHUB_CLIENT_SECRET=${DRONE_GITHUB_CLIENT_SECRET} # Gitea (uncomment if using Gitea) # - DRONE_GITEA_SERVER=${DRONE_GITEA_SERVER} # - DRONE_GITEA_CLIENT_ID=${DRONE_GITEA_CLIENT_ID} # - DRONE_GITEA_CLIENT_SECRET=${DRONE_GITEA_CLIENT_SECRET} - DRONE_LOGS_PRETTY=true - DRONE_LOGS_COLOR=true
drone-runner: image: drone/drone-runner-docker:1 container_name: drone-runner restart: unless-stopped depends_on: - drone-server volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - DRONE_RPC_PROTO=http - DRONE_RPC_HOST=drone-server - DRONE_RPC_SECRET=${DRONE_RPC_SECRET} - DRONE_RUNNER_CAPACITY=2 - DRONE_RUNNER_NAME=runner-01
volumes: drone-data: EOF
Key points:
- The server binds only to
127.0.0.1:8080-- Nginx will terminate TLS on 443 and proxy to it. - The runner mounts the Docker socket so it can launch pipeline step containers on the host.
DRONE_RUNNER_CAPACITY=2lets the runner process two pipelines in parallel. Tune this to your RAM.- The runner talks to the server over plain HTTP on the Docker network -- public traffic is still TLS via Nginx.
cd /opt/drone
docker compose up -dCheck the logs:
docker compose logs -f drone-serverYou should see:
INFO[0000] starting the http server port=:80 proto=https ...
INFO[0000] main: server listening on :80Press Ctrl+C to stop tailing.
Step 5: Configure Nginx with TLS
Install Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginxCreate the Nginx site:
sudo tee /etc/nginx/sites-available/drone > /dev/null <<'EOF' server { listen 80; server_name drone.yourdomain.com;location /.well-known/acme-challenge/ { root /var/www/html; }
location / { return 301 https://$host$request_uri; } }
server { listen 443 ssl http2; server_name drone.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/drone.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/drone.yourdomain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5;
add_header X-Content-Type-Options nosniff; add_header X-Frame-Options SAMEORIGIN; add_header Strict-Transport-Security "max-age=31536000" always;
client_max_body_size 100m;
location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support for live log streaming proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s; proxy_send_timeout 3600s; } } EOF
Enable the site and obtain a certificate:
sudo ln -s /etc/nginx/sites-available/drone /etc/nginx/sites-enabled/ sudo rm -f /etc/nginx/sites-enabled/default sudo nginx -t sudo systemctl reload nginx
sudo certbot --nginx -d drone.yourdomain.com --non-interactive --agree-tos -m [email protected]
Certbot automatically rewrites the config to use the issued certificate and sets up a systemd timer for renewal.
Verify HTTPS works:
curl -I https://drone.yourdomain.comExpected:
HTTP/2 200
server: nginx
...Step 6: Launch Drone and Activate a Repository
Open https://drone.yourdomain.com in a browser. You will be redirected to GitHub (or Gitea) for OAuth approval. Authorize the app and Drone will log you in.
On first login, Drone syncs your accessible repositories. Click Sync in the top-right if the list is empty. Find a repository you want to build and toggle it Active.
Once activated, Drone registers a webhook on the repo pointing to https://drone.yourdomain.com/hook. From now on, every push or pull request sends an event, and Drone will look for a .drone.yml in the commit to decide what to run.
Step 7: Write Your First .drone.yml
A .drone.yml is a YAML file at the repo root that describes one or more pipelines. Here is a minimal pipeline that runs unit tests on every push for a Node project:
--- kind: pipeline type: docker name: defaultsteps: - name: install image: node:20-alpine commands: - npm ci
- name: lint image: node:20-alpine commands: - npm run lint
- name: test image: node:20-alpine commands: - npm test
trigger: branch: - main event: - push - pull_request
Commit this file, push, and watch the build run in the Drone UI. Each step launches a fresh node:20-alpine container, shares a working directory with the checked-out code, and runs the commands in order. A step failure stops the pipeline (unless failure: ignore is set).
Anatomy of a Pipeline
kind: pipeline-- the top-level object. Every YAML document is a pipeline.type: docker-- use the Docker runtime. Other types exist (exec,kubernetes,ssh) but Docker is the standard.name-- any identifier unique within the file.steps-- the ordered list of containers to run.trigger-- optional filter that restricts when the pipeline runs.
Multi-Pipeline Files
You can define several pipelines in one file separated by ---. Drone runs them in parallel by default, respecting depends_on:
--- kind: pipeline type: docker name: teststeps: - name: unit-tests image: node:20-alpine commands: - npm ci - npm test
kind: pipeline type: docker name: builddepends_on: - test
steps: - name: build image: node:20-alpine commands: - npm ci - npm run build
This runs test first, then build only if tests pass.
Step 8: Triggers, Conditions, and Image Promotion
Drone's trigger and when blocks let you fine-tune when pipelines and steps run.
Per-Pipeline Trigger
Run a pipeline only on tags for releases:
trigger:
event:
- tag
ref:
- refs/tags/v*Run on pull requests only:
trigger:
event:
- pull_requestSkip pipelines for specific branch names:
trigger:
branch:
exclude:
- wip/**Per-Step Conditions
Use when: on individual steps to make them conditional:
steps: - name: build image: node:20-alpine commands: - npm ci - npm run build- name: deploy-staging image: appleboy/drone-ssh settings: host: staging.yourdomain.com username: deploy key: from_secret: ssh_key script: - /opt/app/deploy.sh when: branch: main event: push
- name: deploy-production image: appleboy/drone-ssh settings: host: prod.yourdomain.com username: deploy key: from_secret: ssh_key script: - /opt/app/deploy.sh when: event: promote target: production
Image Promotion
The promote event is Drone's built-in way to move a previously built artifact from staging to production without rebuilding. Promotions are triggered from the CLI or UI and run only the steps with a matching target:
# Install the Drone CLI once
curl -L https://github.com/harness/drone-cli/releases/latest/download/drone_linux_amd64.tar.gz | tar zx
sudo install -t /usr/local/bin droneAuthenticate (grab your token from the UI: User -> Profile)
export DRONE_SERVER=https://drone.yourdomain.com
export DRONE_TOKEN=your_user_tokenPromote build 42 on the main branch to production
drone build promote yourorg/yourrepo 42 productionThis re-runs the pipeline with DRONE_BUILD_EVENT=promote and DRONE_DEPLOY_TO=production, so only the deploy-production step executes. The image or artifact produced by build 42 is deployed untouched -- exactly what you validated in staging.
Building and Pushing Docker Images
The plugins/docker image handles builds end-to-end. Here is a typical release pipeline:
--- kind: pipeline type: docker name: build-and-publishsteps: - name: build-image image: plugins/docker settings: repo: ghcr.io/yourorg/yourapp registry: ghcr.io username: from_secret: ghcr_username password: from_secret: ghcr_token tags: - latest - ${DRONE_COMMIT_SHA:0:8} - ${DRONE_TAG} dockerfile: Dockerfile when: event: - push - tag
trigger: branch: - main event: - push - tag
The plugin builds the Dockerfile, logs in to the registry with your secrets, and pushes the image with three tags: latest, the short commit SHA, and the git tag (only set when the event is a tag).
Step 9: Secrets Management
Never commit secrets to the repo. Drone stores them in the server database, encrypted per repo or per organization, and injects them at runtime.
Add a Repo Secret via UI
ghcr_token) and the value.push and tag, not pull_request -- important to prevent PRs from upstream forks leaking secrets).Reference the secret in .drone.yml:
password:
from_secret: ghcr_tokenAdd Secrets with the CLI
drone secret add \
--repository yourorg/yourrepo \
--name ssh_key \
--data "$(cat ~/.ssh/deploy_key)"Organization Secrets
For secrets shared across many repos (a registry token, a Slack webhook), create them at the org level:
drone orgsecret add yourorg SLACK_WEBHOOK "https://hooks.slack.com/services/..."Any repo under yourorg can reference SLACK_WEBHOOK with from_secret.
Pull-Request Safety
By default, secrets are not exposed to pipelines triggered by pull requests from forks. This prevents a malicious PR from exfiltrating your registry password via echo $SECRET. Never disable this protection unless you fully control all contributors.
Step 10: Scale Runners Horizontally
A single runner on the server VPS is enough for small teams. When build queues grow, add runners.
Add a Second Runner on the Same Host
Edit /opt/drone/docker-compose.yml and add another runner block:
drone-runner-2:
image: drone/drone-runner-docker:1
container_name: drone-runner-2
restart: unless-stopped
depends_on:
- drone-server
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DRONE_RPC_PROTO=http
- DRONE_RPC_HOST=drone-server
- DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
- DRONE_RUNNER_CAPACITY=2
- DRONE_RUNNER_NAME=runner-02cd /opt/drone
docker compose up -dAdd a Runner on a Separate VPS
On a second VPS (fresh Ubuntu 24.04 with Docker), create /opt/drone-runner/docker-compose.yml:
version: "3.8"
services: drone-runner: image: drone/drone-runner-docker:1 container_name: drone-runner restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - DRONE_RPC_PROTO=https - DRONE_RPC_HOST=drone.yourdomain.com - DRONE_RPC_SECRET=paste_the_same_secret_here - DRONE_RUNNER_CAPACITY=4 - DRONE_RUNNER_NAME=runner-big-01
docker compose up -dThe runner connects over HTTPS to the Drone server, registers itself, and starts picking up jobs. You can provision dedicated runner VPSes with more CPU and RAM for heavy jobs, or even a GPU VPS for ML test suites -- the server sees them all as a pool.
Targeting Specific Runners
Use labels to route pipelines:
On the runner:
environment:
- DRONE_RUNNER_LABELS=gpu:true,arch:amd64In .drone.yml:
node:
gpu: "true"Only runners with the matching label will accept the pipeline.
Performance and Hardening
Resource Limits per Step
Cap CPU and memory so a runaway step does not crash the host:
steps:
- name: build
image: golang:1.22
commands: [go build ./...]
resources:
limits:
cpu: 2000 # 2 CPUs
memory: 2GiBPrune Docker Regularly
Build caches and intermediate images accumulate fast. Add a weekly cron:
sudo tee /etc/cron.weekly/drone-docker-prune > /dev/null <<'EOF'
#!/bin/bash
docker system prune -af --filter "until=168h"
docker volume prune -f
EOF
sudo chmod +x /etc/cron.weekly/drone-docker-pruneBack Up Drone Data
The drone-data volume contains the SQLite database with users, secrets, and build history. Back it up regularly:
sudo tar czf /root/drone-backup-$(date +%F).tar.gz -C /var/lib/docker/volumes/drone_drone-data/_data .Ship the archive off-server with rsync or restic.
Upgrade Drone
Drone follows semver and publishes minor releases regularly. Upgrade by pulling and recreating:
cd /opt/drone
docker compose pull
docker compose up -dAlways check the release notes at github.com/harness/drone/releases before a major upgrade.
Restrict OAuth Scope
For GitHub, the OAuth app grants repo scope by default, which is broad. If your org uses GitHub Apps, install a Drone GitHub App with fine-grained repository selection instead of a personal OAuth app.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Webhooks from GitHub time out | Drone not reachable on HTTPS | Check curl -I https://drone.yourdomain.com. Confirm the A record, firewall rules (80/443), and Nginx is running. |
401 Unauthorized in runner logs | RPC secret mismatch | Ensure DRONE_RPC_SECRET is identical in both server and every runner. Restart both after editing. |
| Build stuck in "pending" forever | No runner connected, or label mismatch | In the UI: System -> Runners. If empty, check runner logs: docker compose logs drone-runner. |
OAuth login loops on /login | Callback URL mismatch | The URL registered in GitHub/Gitea must exactly match ${DRONE_SERVER_PROTO}://${DRONE_SERVER_HOST}/login. |
Error: no space left on device | Docker consumed all disk | Prune: docker system prune -af --volumes. Move /var/lib/docker to a bigger disk. |
Step fails with Get "https://registry-1.docker.io/v2/": net/http: TLS handshake timeout | Docker Hub rate limiting on the VPS IP | Authenticate with a Docker Hub account in the runner: add DRONE_RUNNER_ENVIRON with DOCKER_USERNAME and DOCKER_PASSWORD, or use a pull-through mirror. |
| Secrets empty inside PR from fork | Expected behavior | Secrets are withheld from fork PRs by design. Restrict the PR pipeline to non-secret steps only. |
| Live logs stop streaming in the UI | Nginx buffering | Ensure proxy_buffering off and WebSocket upgrade headers are set (as in Step 5). |
Useful Log Commands
Server logs:
docker compose -f /opt/drone/docker-compose.yml logs -f drone-serverRunner logs:
docker compose -f /opt/drone/docker-compose.yml logs -f drone-runnerFollow a specific build's containers while it runs:
docker ps --filter "label=io.drone=true"FAQ
Is Drone CI still maintained after the Harness acquisition?
Yes. Harness acquired Drone in 2020 and continues to publish the open-source drone/drone and drone-runner-docker images under the Apache 2.0 license. Self-hosted installs remain fully functional, receive security updates, and work identically to pre-acquisition builds. The commercial Harness platform is a separate SaaS product; nothing is forced on self-hosted users. The GitHub repositories live at github.com/harness/drone and see regular commits.
What is the difference between Drone CI and Woodpecker CI?
Woodpecker is a community fork of Drone that started shortly before the Harness acquisition over concerns about the project's future. The YAML syntax is nearly identical, and migrating a .drone.yml to .woodpecker.yml usually takes minutes. Drone has more mature enterprise features (fine-grained RBAC, more plugins, commercial support if you ever want it), while Woodpecker is community-governed and tends to ship new syntax faster. Both run well on a single VPS -- the choice often comes down to whether you prefer corporate stewardship (Drone) or pure community governance (Woodpecker).
How much RAM does Drone CI need?
The drone/drone server uses around 200-400 MB of RAM at rest. The real memory cost comes from the runner and the pipeline steps it launches. A single Node or Go build typically uses 500 MB to 2 GB per step. Multiply by your DRONE_RUNNER_CAPACITY. For light use (one runner, capacity 2), 4 GB RAM is enough. For parallel builds with Docker-in-Docker or heavier toolchains (Rust, C++), plan for 8-12 GB. The CloudCore Professional at 12 GB is the sweet spot.
Can I run Drone CI without a public domain?
Technically yes, but the OAuth handshake requires your Git provider to reach Drone's callback URL. If Drone is not internet-reachable, the easiest path is a self-hosted Gitea on the same private network -- both Gitea and Drone can live entirely behind a firewall or on a WireGuard mesh. Alternatively, use Cloudflare Tunnel to expose Drone without opening inbound ports. For most teams, a public domain with HTTPS is the path of least resistance.
Does each runner need its own VPS?
No. You can run the server and a runner on the same VPS for small teams -- the setup in this guide does exactly that. Scale horizontally only when build queues grow. Add extra drone-runner-docker containers on the same host (cheap, more parallelism on one box) or on separate VPSes (true isolation, bigger pool, specialized hardware). Each runner reports capacity independently, so you can mix a 2 vCPU small runner with a 16 vCPU heavy runner and Drone will distribute work appropriately.
How do I migrate from Jenkins or GitHub Actions?
Drone's YAML is much simpler than Jenkinsfile Groovy and similar in spirit to GitHub Actions workflows. Most migrations follow this pattern: list each Jenkins stage or Actions job as a Drone step, pick an appropriate public image (node:20-alpine, golang:1.22, python:3.12), translate shell commands as-is, and replace proprietary plugins with Drone plugins (plugins/docker, plugins/s3, plugins/slack). Matrix builds become multiple pipelines with depends_on. The biggest mindset change is that every step is a container, not a shell snippet on a persistent agent -- so install tooling by picking the right base image, not by running apt install in the pipeline.
Can Drone build itself (Docker-in-Docker)?
Yes, but with care. The plugins/docker image already knows how to talk to the host's Docker daemon via the mounted socket, which is the recommended approach. If you genuinely need a nested daemon (for Kubernetes-in-Docker testing, for example), use the docker:dind image as a service inside the pipeline. Mounting the host socket is faster and uses less RAM, but any step with socket access effectively has root on the host -- so only run trusted pipelines that way, and keep untrusted code in strictly non-privileged steps.
Next Steps
With Drone CI running, here are productive directions to take next:
- Install Gitea alongside Drone -- Run your own Git server and CI on the same network for a fully self-hosted dev loop. See How to Install Gitea on Ubuntu.
- Compare with Woodpecker CI -- If you want a community-governed fork of the same YAML, read How to Install Woodpecker CI on Ubuntu and decide which suits your team.
- Harden your Docker host -- Drone relies on Docker for every pipeline. Make sure the host is locked down: see How to Install Docker on Ubuntu for baseline hardening steps.
- Add a local Docker registry -- Run a private registry on the same VPS to avoid Docker Hub rate limits and keep internal images off the public internet.
- Wire up Slack or Discord notifications -- Drop the
plugins/slackorplugins/discordstep into pipelines to get build status in chat. - Read the official docs -- docs.drone.io covers every YAML directive, plugin, and runtime in depth.
Need a VPS sized right for CI/CD?>
The CloudCore Professional plan is the sweet spot for Drone: 6 vCPU, 12 GB RAM, and 100 GB NVMe for EUR 19.99/month. Deploy in under 60 seconds with Ubuntu 24.04 pre-installed and run unlimited builds on your own infrastructure.>
Launch Your CI/CD VPS Now