Git Tags, Drone CI, and Watchtower — A Simple Deployment Pipeline
My CI/CD setup has changed a lot over the years. I started with Jenkins like most people, struggled with it, and eventually landed on something much simpler. Here’s how my current pipeline works.
The Jenkins Days
I used Jenkins for a while. It works, but maintaining it felt like a job on its own. Plugin updates breaking things, Groovy pipelines that nobody wants to debug, and a UI that feels like it’s from 2008. For a small team or solo developer, Jenkins is overkill.
I needed something lighter.
Git Tags as the Trigger
Before talking about the CI tool itself — the most important change I made was switching to git tags as my deployment trigger instead of pushing to a branch.
The flow is simple:
- Work on a feature branch
- Merge to main when ready
- Tag a release when I want to deploy
git tag v1.2.0
git push origin v1.2.0
This gives me full control over what gets deployed and when. Merging to main doesn’t automatically trigger a build — only a tag does. It also makes rollbacks straightforward since every deployment maps to a version.
Drone CI
I replaced Jenkins with Drone CI. It’s lightweight, runs in a single Docker container, and uses a simple YAML config. No plugins to manage, no Groovy scripts.
Here’s a simplified .drone.yml:
kind: pipeline
type: docker
name: build
trigger:
event:
- tag
ref:
- refs/tags/v*
steps:
- name: build-and-push
image: plugins/docker
settings:
registry: registry.example.com
repo: registry.example.com/team/myapp
username:
from_secret: docker_username
password:
from_secret: docker_password
tags:
- latest
- ${DRONE_TAG}
- ${DRONE_COMMIT_SHA:0:8}
cache_from:
- registry.example.com/team/myapp:latest
build_args:
- APP_VERSION=${DRONE_TAG}
A few things I like about this setup:
refs/tags/v*— only triggers on tags starting withv, so random tags don’t accidentally deployplugins/docker— handles the Docker build and push in one step, no need fordocker:dindor mounting the socket- Three tags —
latestfor Watchtower, the version tag for traceability, and the short commit SHA for debugging cache_from— pulls the previouslatestimage as cache so builds are fastbuild_args— passes the version into the build so the app knows what version it’s running- Secrets — registry credentials are stored in Drone’s secret manager, never in the YAML
When I push a git tag, Drone picks it up, builds the Docker image, tags it three ways, and pushes it to the registry. That’s it.
Watchtower for Auto-Redeploy
Here’s where it gets nice. On the server, I run Watchtower — a container that watches for new Docker image versions and automatically restarts containers when a new image is available.
But I don’t want Watchtower to update every container. Some things I want to update manually. So I use the label-based approach — Watchtower only updates containers that have a specific label.
Run Watchtower with label filtering:
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --label-enable --interval 60
Then on containers I want auto-updated, I add the label:
myapp:
image: myregistry/myapp:latest
labels:
- "com.centurylinklabs.watchtower.enable=true"
Containers without that label are left alone. This way, databases, reverse proxies, and other infrastructure stay untouched — only the app containers get redeployed automatically.
The Full Flow
- I push a git tag (
v1.2.0) - Drone CI builds the Docker image and pushes it to the registry as both
v1.2.0andlatest - Watchtower detects the new
latestimage within 60 seconds - Watchtower pulls the new image and restarts only the labeled containers
- Done — zero manual SSH, near zero downtime
The whole pipeline is lightweight. Drone runs in a single container, Watchtower runs in a single container. No complex orchestration, no Kubernetes, no managed CI service bills.
Key Takeaways
- Git tags give you explicit control over what gets deployed
- Drone CI is a great Jenkins replacement for small teams — simple YAML, no plugin hell
- Watchtower with
--label-enablelets you auto-deploy only the containers you choose - You don’t need a complex setup to deploy with confidence
Related
Migrating from Nginx to Caddy — Why I Switched and Never Looked Back
How Caddy replaced Nginx as my reverse proxy — automatic HTTPS, wildcard domains, and a config file that actually makes sense.
Streaming IP Cameras to the Browser with RTSP, WebRTC, and DDNS
How I stream RTSP cameras to the browser using RTSPtoWeb and WebRTC — plus how DDNS makes it accessible from anywhere.
Writing Dockerfiles That Build Fast
Practical tips to speed up Docker builds for Node.js projects — layer caching, multi-stage builds, and keeping images small.