Continuous integration is the backbone of fast-moving engineering teams, yet many organizations still rely on cloud-hosted CI services that charge per-seat or per-minute premiums and keep build logs on third-party infrastructure. Drone CI is a lightweight, container-native continuous integration platform that turns your own servers into a scalable build farm. It reads pipeline definitions from .drone.yml files inside repositories, executes steps inside ephemeral Docker containers, and streams logs back to a clean web dashboard. Because Drone separates the server control plane from the runner execution plane, you can add more build capacity without reconfiguring the core application.
This guide deploys Drone Server and a Drone Docker Runner on Ubuntu with Docker Compose, Caddy for automatic HTTPS, PostgreSQL for build metadata and user sessions, and Redis for webhook event queuing and build log buffering. By the end, you will have a production-ready CI stack that can authenticate users through GitHub, trigger builds on every push, cache Docker layers between runs, and scale horizontally by adding runner hosts to the same Docker network.
Architecture and flow overview
Caddy sits at the edge and terminates TLS with automatically managed Letβs Encrypt certificates. It reverse-proxies HTTPS traffic to the Drone Server container on its internal HTTP port. Drone Server persists user accounts, repository settings, build metadata, and encrypted secrets in PostgreSQL. Redis acts as a message bus for webhook events and a temporary buffer for build logs while runners stream output back to the server. The Drone Docker Runner connects to the server over gRPC on an internal port, receives build jobs, and spawns sibling containers on the host Docker socket to execute pipeline steps.
All services except Caddy remain inside an isolated Docker bridge network. The database and Redis are unreachable from the public internet because they do not publish host ports. The runner requires access to the host Docker socket so it can launch build containers alongside the Compose stack, but the runner itself communicates with the server only over the internal network. Persistent volumes keep PostgreSQL data, Caddy certificates, and Drone server state across container restarts and image upgrades.
Prerequisites
- Ubuntu 22.04 or 24.04 LTS server with at least 2 vCPU, 4 GB RAM, and 40 GB SSD. For teams running more than fifty concurrent builds, scale to 4 vCPU and 8 GB RAM, and provision a separate volume for Docker layer caching.
- Docker Engine 24.x and the Docker Compose plugin installed. Verify with
docker compose version. - A DNS A record pointing
drone.example.comto your server public IP. Do not start Caddy before DNS resolves, or certificate rate limits may block your domain temporarily. - OAuth application credentials from your Git provider. This guide uses GitHub as the identity provider; you will need a Client ID and Client Secret with the callback set to
https://drone.example.com/login. - UFW enabled with default-deny incoming, plus SSH allowed from your management IP range and ports 80 and 443 open for Caddy.
Step-by-step deployment
1. Create directory structure and environment file
Create a dedicated directory for the Drone stack, set strict permissions, and prepare an environment file for secrets that never enters version control. Use strong passwords and store the real values in your password manager or secrets vault.
sudo mkdir -p /opt/drone/{data,postgres,caddy-data,caddy-config}
sudo useradd -r -s /usr/sbin/nologin -d /opt/drone drone || true
sudo chown -R drone:drone /opt/drone/data
sudo chmod 750 /opt/drone
sudo chmod 700 /opt/drone/postgres
cd /opt/drone
cat > .env <<'EOF'
DRONE_SERVER_HOST=drone.example.com
DRONE_SERVER_PROTO=https
DRONE_RPC_SECRET=$(openssl rand -hex 24)
DRONE_GITHUB_CLIENT_ID=your-github-client-id
DRONE_GITHUB_CLIENT_SECRET=your-github-client-secret
DRONE_DATABASE_DATASOURCE=postgres://drone:$(openssl rand -hex 16)@postgres:5432/drone?sslmode=disable
POSTGRES_USER=drone
POSTGRES_PASSWORD=$(openssl rand -hex 16)
POSTGRES_DB=drone
EOF
chmod 600 .env
2. Write the Docker Compose file
Pin major image versions, define restart policies, keep the database and Redis on the private network, and mount the host Docker socket into the runner so it can spawn pipeline containers. The server depends on PostgreSQL and Redis being healthy before it starts accepting web traffic.
cat > docker-compose.yml <<'EOF'
services:
caddy:
image: caddy:2.8
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
depends_on:
- server
server:
image: drone/drone:2
restart: unless-stopped
env_file: .env
environment:
DRONE_SERVER_HOST: ${DRONE_SERVER_HOST}
DRONE_SERVER_PROTO: ${DRONE_SERVER_PROTO}
DRONE_RPC_SECRET: ${DRONE_RPC_SECRET}
DRONE_GITHUB_CLIENT_ID: ${DRONE_GITHUB_CLIENT_ID}
DRONE_GITHUB_CLIENT_SECRET: ${DRONE_GITHUB_CLIENT_SECRET}
DRONE_DATABASE_DRIVER: postgres
DRONE_DATABASE_DATASOURCE: ${DRONE_DATABASE_DATASOURCE}
DRONE_REDIS_ENDPOINT: redis:6379
volumes:
- ./data:/data
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
runner:
image: drone/drone-runner-docker:1
restart: unless-stopped
env_file: .env
environment:
DRONE_RPC_PROTO: http
DRONE_RPC_HOST: server:9000
DRONE_RPC_SECRET: ${DRONE_RPC_SECRET}
DRONE_RUNNER_CAPACITY: 4
DRONE_RUNNER_NAME: docker-runner-01
volumes:
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- server
postgres:
image: postgres:16-alpine
restart: unless-stopped
env_file: .env
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- ./postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- ./redis:/data
EOF
3. Create the Caddy reverse proxy configuration
Caddy handles TLS termination and proxies traffic to the Drone server container. The configuration also adds standard security headers and enables compression for the web dashboard and log streams.
cat > Caddyfile <<'EOF'
{
email [email protected]
}
{$DRONE_SERVER_HOST} {
encode zstd gzip
reverse_proxy server:80
header {
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
Referrer-Policy strict-origin-when-cross-origin
}
}
EOF
4. Launch the stack
Pull images, bring up the services in detached mode, and watch the server container logs for database migration completion. The first boot may take a minute while PostgreSQL initializes and Drone creates its schema.
docker compose pull
docker compose up -d
docker compose ps
docker compose logs -f --tail=80 server
5. Register the runner
The Docker Runner should connect automatically once the server is healthy, but you can verify registration from the server dashboard or by inspecting runner logs. If you need additional capacity, copy the runner service block to another host, update DRONE_RUNNER_NAME, and point it at the same DRONE_RPC_SECRET.
docker compose logs --tail=40 runner
docker compose exec server sh -c "drone-server --help" 2>/dev/null || true
Configuration and secrets handling best practices
After the stack is running, open the HTTPS URL, log in with your GitHub account, and activate at least one repository from the Drone dashboard. Create a simple .drone.yml in the root of that repository to validate the pipeline. The example below runs a smoke test inside an Alpine container and prints the commit hash.
kind: pipeline
type: docker
name: default
steps:
- name: build
image: alpine:latest
commands:
- echo "Commit $DRONE_COMMIT_SHA"
- apk add --no-cache curl
- curl -I https://drone.example.com
Never store OAuth client secrets, RPC secrets, or database passwords inside public repositories. Keep the live .env file on the server with mode 600, back it up through an encrypted secret store, and rotate credentials when an operator leaves. Limit admin access to the Drone dashboard, require MFA on the Git provider organization, and audit activated repositories regularly. For production workloads, pin pipeline images to immutable digests rather than floating tags, and mount a private registry mirror inside the runner host to reduce external pull latency and supply-chain exposure.
Use a small maintenance script for database backups. Test restore quarterly; untested backups are only optimistic log files.
cat > backup.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
stamp=$(date -u +%Y%m%dT%H%M%SZ)
mkdir -p /opt/drone/backups/$stamp
source .env
# Note: use the Postgres container to run pg_dump so you do not need the client locally
docker compose exec -T postgres pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
> /opt/drone/backups/$stamp/drone.sql
tar -C /opt/drone -czf /opt/drone/backups/$stamp/drone-files.tgz data Caddyfile docker-compose.yml .env
docker run --rm -v /opt/drone/redis:/data redis:7-alpine redis-cli --rdb /dev/stdout \
> /opt/drone/backups/$stamp/redis.rdb || true
find /opt/drone/backups -mindepth 1 -maxdepth 1 -type d -mtime +14 -exec rm -rf {} +
EOF
chmod +x backup.sh
Verification checklist
- Open
https://drone.example.comand confirm the certificate is valid. - Log in through GitHub and authorize the OAuth application.
- Activate a test repository and push a commit containing the sample
.drone.yml. - Confirm the build appears in the dashboard, passes all steps, and shows streamed logs.
- Inspect the runner host with
docker psand verify ephemeral build containers are created and cleaned up. - Run the backup script and verify the SQL dump and archive files are not empty.
curl -I https://drone.example.com
docker compose ps
docker compose logs --tail=120 runner
docker compose exec -T postgres pg_dump -U drone -d drone > /tmp/verify.sql
ls -lah /opt/drone/backups/*/
Common issues and fixes
Caddy cannot obtain a certificate. Verify the DNS A record resolves to the server and that ports 80 and 443 are reachable from the internet. Cloud firewalls, stale AAAA records, or CDN proxies in front of Caddy can block the HTTP-01 challenge.
OAuth login fails with a callback mismatch error. Check that the GitHub OAuth application callback URL exactly matches https://drone.example.com/login, including the protocol and trailing path. A missing trailing slash or an http prefix will reject the handshake.
Builds queue but never start. Inspect the runner logs for RPC connection errors. Ensure the runner can reach the server container on port 9000 over the Docker network, and verify that DRONE_RPC_SECRET matches on both sides.
Pipeline containers fail immediately with a Docker socket error. Confirm the runner service mounts /var/run/docker.sock:/var/run/docker.sock and that the host Docker daemon is running. On hardened systems, AppArmor or SELinux profiles may restrict socket access.
Database connections are exhausted during heavy build loads. Increase PostgreSQL max_connections through a custom configuration file mounted into the Postgres container, or enable connection pooling with PgBouncer if you run more than a hundred concurrent pipelines.
Webhook deliveries from the Git provider time out. If Drone sits behind a corporate proxy or CDN, whitelist the Git provider IP ranges and ensure the proxy does not buffer long-running SSE log streams.
FAQ
Can Drone CI run on ARM64 servers?
Yes. Drone Server, the Docker Runner, and most official pipeline images publish multi-arch manifests for amd64 and arm64. Use the same Compose file on ARM hosts; the container runtime will select the correct platform layer automatically.
How many runners can attach to a single server?
There is no hard limit, but each runner should run on a separate host for resource isolation. A single runner container on the same host as the server is sufficient for small teams. Scale horizontally by adding more hosts with the runner service and the same DRONE_RPC_SECRET.
Does Drone support Kubernetes runners?
Yes. Drone provides a dedicated Kubernetes runner that schedules pipeline pods inside a cluster. This guide focuses on the Docker runner because it is the fastest path to production on a single Ubuntu host, but migrating to Kubernetes later only requires swapping the runner deployment.
Where are build logs stored long term?
Drone stores build metadata and recent logs in PostgreSQL. For long-term retention or compliance archiving, configure the server to write logs to an S3-compatible object store such as MinIO or AWS S3. Older logs can then be offloaded from the database to reduce table bloat.
Can I use SQLite instead of PostgreSQL?
Drone supports SQLite for evaluation, but it is not recommended for production. SQLite handles concurrent writes poorly, and a single long-running build can lock the dashboard for other users. Use PostgreSQL from the start to avoid painful migrations later.
How do I rotate the RPC secret without losing queued builds?
Update the secret in .env, run docker compose up -d to recreate the server and runner containers, and verify runner reconnection in the logs. Builds queued before the rotation may fail if the runner disconnects mid-job, so perform rotation during a maintenance window.
What monitoring should I add for a production Drone deployment?
Monitor HTTPS endpoint availability, PostgreSQL connection usage, Redis memory growth, runner container restarts, disk space for Docker layers, build queue depth, and the age of the latest successful backup. Alert on any build that stays in a running state longer than your typical pipeline timeout.
Internal links
- Production Guide: Deploy n8n with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
- Production Guide: Deploy Ghost with Docker Compose + Caddy + MySQL on Ubuntu
- Production Guide: Deploy Matomo with Docker Compose + Caddy + MariaDB on Ubuntu
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.