Remote Work Tools

Drone CI is a container-native CI system where every pipeline step runs in a Docker container. There’s no plugin system to fight with, no shared state between steps by default, and pipeline configs are just YAML that any developer can understand. For remote teams that self-host, Drone’s simplicity reduces the operational burden compared to Jenkins.

Remote teams get a specific benefit from Drone: the pipeline definition lives in the repository as .drone.yml, so every team member — regardless of time zone — has full visibility into what CI does and can propose changes through a standard pull request. There’s no admin-only config hidden in a Jenkins web UI that only one person understands.


Architecture

Drone has two components:

Runners communicate with the server over HTTP. The server stores state in SQLite or PostgreSQL.


Deploy with Docker Compose

For GitHub integration:

# docker-compose.yml
version: "3.8"

services:
  drone-server:
    image: drone/drone:2
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/lib/drone:/data
    restart: always
    environment:
      - DRONE_GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
      - DRONE_GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
      - DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
      - DRONE_SERVER_HOST=drone.yourcompany.com
      - DRONE_SERVER_PROTO=https
      - DRONE_TLS_AUTOCERT=true
      - DRONE_USER_CREATE=username:your-github-username,admin:true
      - DRONE_DATABASE_DRIVER=postgres
      - DRONE_DATABASE_DATASOURCE=postgres://drone:${DB_PASSWORD}@postgres:5432/drone?sslmode=disable
    depends_on:
      - postgres

  drone-runner:
    image: drone/drone-runner-docker:1
    restart: always
    depends_on:
      - drone-server
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - DRONE_RPC_PROTO=https
      - DRONE_RPC_HOST=drone.yourcompany.com
      - DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
      - DRONE_RUNNER_CAPACITY=4
      - DRONE_RUNNER_NAME=runner-01
      - DRONE_RUNNER_LABELS=platform:linux,arch:amd64

  postgres:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=drone
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=drone
    volumes:
      - drone-postgres-data:/var/lib/postgresql/data

volumes:
  drone-postgres-data:

Create .env:

GITHUB_CLIENT_ID=your_oauth_app_id
GITHUB_CLIENT_SECRET=your_oauth_app_secret
DRONE_RPC_SECRET=$(openssl rand -hex 32)
DB_PASSWORD=$(openssl rand -hex 24)

For Gitea instead of GitHub:

# Replace GitHub env vars with:
- DRONE_GITEA_SERVER=https://git.yourcompany.com
- DRONE_GITEA_CLIENT_ID=${GITEA_CLIENT_ID}
- DRONE_GITEA_CLIENT_SECRET=${GITEA_CLIENT_SECRET}

Write Your First Pipeline

Create .drone.yml in your repository root:

# .drone.yml
kind: pipeline
type: docker
name: default

steps:
  - name: test
    image: golang:1.22-alpine
    commands:
      - go test ./...
      - go vet ./...

  - name: build
    image: golang:1.22-alpine
    commands:
      - CGO_ENABLED=0 go build -o ./bin/app ./cmd/app
    when:
      branch: main

  - name: docker-build-push
    image: plugins/docker
    settings:
      repo: yourcompany/app
      auto_tag: true
      username:
        from_secret: docker_username
      password:
        from_secret: docker_password
    when:
      branch: main
      event: push

  - name: notify
    image: plugins/slack
    settings:
      webhook:
        from_secret: slack_webhook
      channel: "#deployments"
      template: >
        {{#success build.status}}
          Build {{build.number}} succeeded on {{build.branch}}
        {{else}}
          Build {{build.number}} failed on {{build.branch}}
        {{/success}}
    when:
      status: [success, failure]

For Node.js:

kind: pipeline
type: docker
name: node-app

steps:
  - name: install
    image: node:20-alpine
    commands:
      - npm ci
      - npm run build

  - name: test
    image: node:20-alpine
    commands:
      - npm run test:ci
    environment:
      NODE_ENV: test

  - name: e2e
    image: node:20-alpine
    commands:
      - npm run test:e2e
    when:
      branch: [main, staging]

Manage Secrets

Add secrets via the Drone CLI:

# Install CLI
curl -L https://github.com/harness/drone-cli/releases/latest/download/drone_linux_amd64.tar.gz | tar zx
install -t /usr/local/bin drone

# Authenticate
export DRONE_SERVER=https://drone.yourcompany.com
export DRONE_TOKEN=your_drone_token  # Get from your user settings in UI

# Add a repo-level secret
drone secret add \
  --repository your-org/your-repo \
  --name docker_password \
  --data "your_registry_password"

# Add an organization-level secret
drone orgsecret add your-org slack_webhook "https://hooks.slack.com/..."

Reference secrets in .drone.yml:

steps:
  - name: deploy
    image: alpine
    environment:
      DEPLOY_KEY:
        from_secret: deploy_key
    commands:
      - echo "$DEPLOY_KEY" | ssh-add -
      - ssh deploy@prod.yourcompany.com "cd /app && git pull && pm2 restart app"

For remote teams managing many repositories, organization-level secrets reduce the overhead of keeping secrets synchronized. Add slack_webhook and docker_password once at the org level, and every repository in the organization can reference them without per-repo configuration.


Pipeline Branch and Event Conditions

Drone’s when clause controls which builds run for which events. For remote teams with multiple environments, conditional steps map directly to your branching strategy:

steps:
  - name: deploy-staging
    image: alpine
    environment:
      SSH_KEY: { from_secret: staging_deploy_key }
    commands:
      - echo "$SSH_KEY" > /tmp/key && chmod 600 /tmp/key
      - ssh -i /tmp/key deploy@staging.yourcompany.com "cd /app && ./scripts/deploy.sh"
    when:
      branch: staging
      event: push

  - name: deploy-production
    image: alpine
    environment:
      SSH_KEY: { from_secret: prod_deploy_key }
    commands:
      - echo "$SSH_KEY" > /tmp/key && chmod 600 /tmp/key
      - ssh -i /tmp/key deploy@prod.yourcompany.com "cd /app && ./scripts/deploy.sh"
    when:
      branch: main
      event: push

  - name: run-smoke-tests
    image: node:20-alpine
    commands:
      - npm run test:smoke -- --env=$TARGET_ENV
    environment:
      TARGET_ENV: production
    when:
      branch: main
      event: push

This pattern — staging deploy on staging branch push, production deploy on main branch push — means engineers in any time zone can merge to staging to verify their change before promoting to production without any manual coordination.


Multi-Architecture Builds

Build for AMD64 and ARM64 in parallel using Drone’s multi-pipeline support:

# .drone.yml
---
kind: pipeline
type: docker
name: linux-amd64

platform:
  os: linux
  arch: amd64

steps:
  - name: build
    image: golang:1.22-alpine
    commands:
      - GOOS=linux GOARCH=amd64 go build -o ./bin/app-amd64 ./cmd/app

  - name: push
    image: plugins/docker
    settings:
      repo: yourcompany/app
      tags: linux-amd64-${DRONE_COMMIT_SHA:0:8}
      username: { from_secret: docker_username }
      password: { from_secret: docker_password }
    when:
      branch: main

---
kind: pipeline
type: docker
name: linux-arm64

platform:
  os: linux
  arch: arm64

steps:
  - name: build
    image: golang:1.22-alpine
    commands:
      - GOOS=linux GOARCH=arm64 go build -o ./bin/app-arm64 ./cmd/app

  - name: push
    image: plugins/docker
    settings:
      repo: yourcompany/app
      tags: linux-arm64-${DRONE_COMMIT_SHA:0:8}
      username: { from_secret: docker_username }
      password: { from_secret: docker_password }
    when:
      branch: main

---
kind: pipeline
type: docker
name: manifest

depends_on:
  - linux-amd64
  - linux-arm64

steps:
  - name: create-manifest
    image: plugins/manifest
    settings:
      username: { from_secret: docker_username }
      password: { from_secret: docker_password }
      target: yourcompany/app:latest
      template: yourcompany/app:linux-ARCH-${DRONE_COMMIT_SHA:0:8}
      platforms:
        - linux/amd64
        - linux/arm64
    when:
      branch: main

Caching Dependencies

Cache node_modules or Go module cache between builds to speed up pipelines:

steps:
  - name: restore-cache
    image: drillster/drone-volume-cache
    settings:
      restore: true
      mount:
        - ./node_modules
    volumes:
      - name: cache
        path: /cache

  - name: install
    image: node:20-alpine
    commands:
      - npm ci

  - name: rebuild-cache
    image: drillster/drone-volume-cache
    settings:
      rebuild: true
      mount:
        - ./node_modules
    volumes:
      - name: cache
        path: /cache

volumes:
  - name: cache
    host:
      path: /tmp/drone-cache

For Go module caching, mount the module cache directory:

steps:
  - name: test
    image: golang:1.22-alpine
    volumes:
      - name: go-cache
        path: /go
    commands:
      - go test ./...

volumes:
  - name: go-cache
    host:
      path: /tmp/drone-go-cache

Pipeline caching matters more for remote teams because CI feedback time directly affects async review cycles. A 12-minute pipeline that can be reduced to 4 minutes with caching means the author can address review feedback and get another CI run in before their reviewer goes offline.


Scaling Runners for Distributed Teams

The default DRONE_RUNNER_CAPACITY=4 means the runner executes 4 pipeline jobs in parallel. For a remote team with engineers across multiple time zones, builds queue at shift overlap times. Add more runners to handle the load:

# docker-compose.yml — add additional runners as separate services
  drone-runner-02:
    image: drone/drone-runner-docker:1
    restart: always
    depends_on:
      - drone-server
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - DRONE_RPC_PROTO=https
      - DRONE_RPC_HOST=drone.yourcompany.com
      - DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
      - DRONE_RUNNER_CAPACITY=4
      - DRONE_RUNNER_NAME=runner-02
      - DRONE_RUNNER_LABELS=platform:linux,arch:amd64

Alternatively, run runners on separate hosts to distribute the Docker build load. Each runner only needs network access to the Drone server on port 443 — runners do not need to communicate with each other.

For teams with multiple environments (Linux x86, Linux ARM, macOS), label runners by capability and target steps to specific runners:

# .drone.yml — target a specific runner label
steps:
  - name: test-on-arm
    image: golang:1.22-alpine
    commands:
      - go test ./...
    node:
      arch: arm64

Integrating Drone with Container Registries

After a successful build, push images to your registry immediately. For GHCR (GitHub Container Registry):

steps:
  - name: push-to-ghcr
    image: plugins/docker
    settings:
      registry: ghcr.io
      repo: ghcr.io/your-org/your-app
      username: your-github-username
      password:
        from_secret: ghcr_token
      tags:
        - latest
        - ${DRONE_COMMIT_SHA:0:8}
        - ${DRONE_BRANCH//\//-}
    when:
      branch: [main, staging]
      event: push

For AWS ECR, use the dedicated plugin:

steps:
  - name: push-to-ecr
    image: plugins/ecr
    settings:
      registry: 123456789.dkr.ecr.us-east-1.amazonaws.com
      repo: your-app
      region: us-east-1
      tags:
        - latest
        - ${DRONE_COMMIT_SHA:0:8}
      access_key:
        from_secret: aws_access_key
      secret_key:
        from_secret: aws_secret_key
    when:
      branch: main
      event: push

Tagging with both latest and the short commit SHA is the recommended practice for remote teams. The latest tag is what production instances poll for updates via Watchtower or similar tooling; the commit SHA tag is what you reference in post-incident reviews to identify exactly which code was running at the time of a failure.


Useful CLI Commands

# List builds for a repo
drone build ls your-org/your-repo

# View build details
drone build info your-org/your-repo 42

# Trigger a build on main
drone build create your-org/your-repo --branch main

# View logs for a specific step
drone log view your-org/your-repo 42 1 1

# List secrets for a repo
drone secret ls --repository your-org/your-repo