I ran Jenkins for a couple of years. It works, but configuring it for a new project involves plugins, Groovy syntax, XML configuration, and a learning curve that doesn't feel proportional to what I'm trying to do.
Drone CI is the opposite. The pipeline is a YAML file in your repo, every step runs in a Docker container (so you just reference an image — no plugin installation), and the whole thing integrates with GitHub or Gitea via OAuth in about an hour.
I switched several personal projects to Drone after getting tired of the Jenkins maintenance overhead. For a self-hosted setup where you want something that works cleanly without being heavyweight, it's the right tool.
I switched one of my projects to Drone after finding it noticeably faster to configure and less resource-hungry than Jenkins. The whole setup from install to first pipeline run took about an hour.
I run Drone CI on Tencent Cloud Lighthouse. The 2 GB RAM / 2 vCPU plan handles Drone server and a runner comfortably. For heavy parallel builds, 4 GB RAM gives more headroom. The snapshot feature is valuable for CI/CD infrastructure — a snapshot before a Drone version upgrade means you can restore quickly if the upgrade breaks existing pipelines. Running Drone on the same server as Gitea (if you're using it) keeps the OAuth roundtrip latency low and the configuration simple.
- Key Takeaways
Drone is designed for simplicity and container-native workflows:
.drone.yml lives with your code, versioned in GitCompared to GitHub Actions: Drone runs on your own server, so no minute limits. Compared to Jenkins: significantly simpler configuration and much lower resource usage.
| Requirement | Details |
|---|---|
| Server | Ubuntu 22.04, 2 GB RAM |
| Docker | Installed |
| Git provider | GitHub, Gitea, GitLab, or Bitbucket |
| Domain | For Drone server HTTPS |
| OAuth app | Created in your Git provider (see Part 1) |
Drone authenticates users via OAuth from your Git provider.
Drone CIhttps://drone.yourdomain.comhttps://drone.yourdomain.com/loginNote the Client ID and generate a Client Secret — you'll need both.
Drone CIhttps://drone.yourdomain.com/loginNote the Client ID and Client Secret.
Drone server and runners use a shared secret for communication:
openssl rand -hex 16
# Save this output — you'll use it for both server and runner config
Create /opt/drone/docker-compose.yml:
mkdir -p /opt/drone
nano /opt/drone/docker-compose.yml
For GitHub:
version: '3.8'
services:
drone-server:
image: drone/drone:2
restart: always
ports:
- "127.0.0.1:8080:80"
volumes:
- /opt/drone/data:/data
environment:
- DRONE_GITHUB_CLIENT_ID=YOUR_GITHUB_CLIENT_ID
- DRONE_GITHUB_CLIENT_SECRET=YOUR_GITHUB_CLIENT_SECRET
- DRONE_RPC_SECRET=YOUR_SHARED_SECRET
- DRONE_SERVER_HOST=drone.yourdomain.com
- DRONE_SERVER_PROTO=https
- DRONE_USER_CREATE=username:your-github-username,admin:true
- DRONE_LOGS_DEBUG=false
For Gitea, replace the GitHub environment variables with:
- DRONE_GITEA_SERVER=https://your-gitea-domain.com
- DRONE_GITEA_CLIENT_ID=YOUR_GITEA_CLIENT_ID
- DRONE_GITEA_CLIENT_SECRET=YOUR_GITEA_CLIENT_SECRET
cd /opt/drone
docker compose up -d
docker compose logs -f drone-server
sudo apt install -y nginx certbot python3-certbot-nginx
sudo nano /etc/nginx/sites-available/drone
server {
listen 80;
server_name drone.yourdomain.com;
location / {
proxy_pass http://localhost: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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
sudo ln -s /etc/nginx/sites-available/drone /etc/nginx/sites-enabled/
sudo certbot --nginx -d drone.yourdomain.com
Navigate to https://drone.yourdomain.com. Click Login — it redirects to GitHub/Gitea for OAuth authorization.
The runner executes pipeline steps on Docker containers. Install it on the same server:
Add to your docker-compose.yml:
drone-runner:
image: drone/drone-runner-docker:1
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DRONE_RPC_PROTO=https
- DRONE_RPC_HOST=drone.yourdomain.com
- DRONE_RPC_SECRET=YOUR_SHARED_SECRET
- DRONE_RUNNER_CAPACITY=2
- DRONE_RUNNER_NAME=my-vps-runner
depends_on:
- drone-server
cd /opt/drone
docker compose up -d
docker compose logs -f drone-runner
You should see: successfully pinged the remote server.
Add .drone.yml to your repository:
kind: pipeline
type: docker
name: default
steps:
- name: install
image: node:20-alpine
commands:
- npm ci
- name: test
image: node:20-alpine
commands:
- npm test
depends_on:
- install
- name: build
image: node:20-alpine
commands:
- npm run build
depends_on:
- test
trigger:
branch:
- main
- develop
event:
- push
- pull_request
https://drone.yourdomain.comNow push a commit — the pipeline runs automatically.
Don't put API keys or passwords in .drone.yml. Use Drone's built-in secrets:
In the Drone UI:
deploy_keyReference in your pipeline:
steps:
- name: deploy
image: alpine:latest
environment:
DEPLOY_KEY:
from_secret: deploy_key
commands:
- echo "$DEPLOY_KEY" > /tmp/key
- chmod 600 /tmp/key
- ssh -i /tmp/key ubuntu@YOUR_SERVER "cd /opt/myapp && git pull"
Organization-level secrets (shared across repositories) are available in the Drone organization settings.
Using the appleboy/ssh-action style directly in Drone:
- name: deploy
image: appleboy/drone-ssh
settings:
host: YOUR_PRODUCTION_IP
username: ubuntu
key:
from_secret: ssh_private_key
script:
- cd /opt/myapp
- git pull origin main
- npm ci --production
- pm2 reload myapp
when:
branch:
- main
event:
- push
- name: build-and-push
image: plugins/docker
settings:
repo: registry.yourdomain.com/myapp
tags:
- latest
- ${DRONE_COMMIT_SHA:0:8}
registry: registry.yourdomain.com
username:
from_secret: registry_username
password:
from_secret: registry_password
After getting Drone running, my pipelines were stuck in "pending" state indefinitely. They'd show in the UI but never start executing.
The cause: the Drone runner couldn't communicate with the Drone server. The runner uses the public HTTPS address (my domain), not localhost. My firewall was blocking outbound connections on port 443 from the Docker network.
The fix:
First, check if the runner is connected:
docker compose logs drone-runner | grep -i "ping\|error\|connect"
I saw: failed to connect to server.
The issue was that Docker containers by default use the docker0 bridge network, and the runner container was routing traffic through the host's external interface. My UFW rules weren't blocking it, but the Docker network wasn't resolving the domain correctly inside the container.
Solution: Tell the runner to use the container network name instead of the external domain for internal communication:
drone-runner:
environment:
- DRONE_RPC_PROTO=http # Use HTTP internally
- DRONE_RPC_HOST=drone-server # Use container service name
But only if both services are on the same Docker Compose network. If the runner is on a different host or network, keep the HTTPS external URL and ensure DNS resolves correctly inside containers.
| Issue | Likely Cause | Fix |
|---|---|---|
| Pipeline stuck "pending" | Runner not connected | Check runner logs for connection errors |
| OAuth login fails | Redirect URI mismatch | Verify callback URL matches exactly in Git provider |
Build fails: Cannot connect to Docker |
Runner volume missing | Ensure /var/run/docker.sock is mounted |
| Secret not available in step | Wrong name reference | Check exact secret name, case-sensitive |
| Drone UI not loading | Nginx misconfigured | Check proxy headers include Upgrade for WebSocket |
| Runner capacity exceeded | All slots busy | Increase DRONE_RUNNER_CAPACITY or add another runner |
| Git clone fails | No SSH key or HTTPS token | Check repository access in Drone settings |
✅ What you built:
Drone's .drone.yml format is clean and readable. Adding CI/CD to a new project is a matter of dropping a config file in the repo and toggling it active in the Drone UI — under 5 minutes once the server is running.
What's the difference between CI and CD?
CI (Continuous Integration) automatically runs tests when code is pushed. CD (Continuous Deployment/Delivery) automatically deploys tested code to servers. Together they eliminate manual build-test-deploy cycles.
How do I store secrets (API keys, passwords) in Drone CI?
Use the CI/CD platform's secrets management — never put secrets in your repository. Reference them as environment variables in your pipeline config. Lighthouse server credentials go in deployment secrets.
What happens if a deployment fails partway through?
Implement a rollback strategy: tag git commits before deployment, keep the previous release available, and have a procedure to revert. Lighthouse snapshots provide a full-server rollback point.
How do I deploy to multiple environments (staging, production) from the same pipeline?
Use branch-based or tag-based triggers. Pushes to develop deploy to staging; tagged releases or merges to main deploy to production. Configure different deployment targets per environment.
👉 Get started with Tencent Cloud Lighthouse
👉 View current pricing and launch promotions
👉 Explore all active deals and offers