Master blue-green deployment on a single VPS using Docker Compose and Traefik. Achieve zero-downtime releases without the complexity of Kubernetes clusters.

When you're running a side project or a small production app on a single $5 VPS, you eventually hit the "maintenance window" problem. Pushing a new container version usually means stopping the old one, leading to a few seconds of 502 Bad Gateway errors. While I’ve covered deploying a side project on a single cheap VPS reliably before, simple restarts aren't enough for applications that need high availability.
I recently moved my primary stack to a blue-green model using Traefik and Docker Compose. It’s significantly lighter than a full Kubernetes setup, yet it provides the same peace of mind during deployments.
A blue-green deployment works by running two identical production environments. At any time, only one (the "Blue" environment) is live. When you deploy, you spin up the new version in the "Green" environment, verify it’s healthy, and then tell your reverse proxy to route all traffic to Green. If something goes sideways, you point the proxy back to Blue instantly.
I’ve found this approach is roughly 1.5x faster to roll back than traditional redeploys. You aren't "deploying" a fix during an outage; you're just flipping a switch.

To pull this off, you need a stable Traefik instance running as a separate service, outside the lifecycle of your application containers. I keep mine in a dedicated proxy directory with a simple docker-compose.yml.
YAMLversion: '3.8' services: traefik: image: traefik:v2.10 command: --providers.docker=true --entrypoints.web.address=:80 ports: - "80:80" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro
The magic happens in how you label your application containers. Instead of hardcoding a single service, you define two separate docker-compose projects—app-blue and app-green—that share the same entry point.
You don't need fancy service mesh tools. Traefik’s dynamic configuration is enough to handle the switch. In your app's docker-compose.yml, define the labels dynamically based on the current active color.
YAMLservices: web: image: my-app:v2.0.0 labels: - "traefik.enable=true" - "traefik.http.routers.myapp.rule=Host(`myapp.com`)" - "traefik.http.routers.myapp.service=myapp-${COLOR}" - "traefik.http.services.myapp-${COLOR}.loadbalancer.server.port=8080"
When you deploy, you set an environment variable COLOR=green. Traefik sees the new service, registers it, and starts sending traffic to it. Because Traefik handles the routing table in memory, the switch is atomic.
I use a simple shell script to coordinate the switch. It follows a three-step dance:
curl to verify the new container responds with a 200 OK.I used to manually update environment variables, which led to about two separate human errors in a single month. Now, I use a small CI script that queries the current active container using docker inspect before deciding which color to deploy.
If you are already using CI/CD, you can easily integrate this into a zero-downtime deploy with GitHub Actions. The logic remains the same: treat your infrastructure as a state machine where the "active" state is just a label in your proxy.
The biggest "gotcha" isn't the traffic switching—it's the database. If your new version (Green) requires a database schema change that breaks the old version (Blue), your rollback path is destroyed.
I’ve learned the hard way that schema migrations must be backward compatible. Always add columns first, migrate data in a second step, and remove old columns only after the new version has been stable for a few days. If you're doing complex migrations, you might want to look into how Kubernetes Canary Deployments handle progressive traffic, though that's usually overkill for a single VPS.
Does this require double the RAM? Yes. You are running two copies of your application simultaneously. For small apps, this is negligible (usually under 200MB), but if you have memory-intensive background workers, you'll need to account for the overhead.
How do I handle persistent storage? If your app writes to local disk, don't use the same volume for both Blue and Green. Use versioned volumes or, better yet, offload state to an external managed database or S3-compatible storage.
Is this really better than a simple docker-compose up -d --build?
A standard up -d recreates containers one by one. If you have multiple replicas, it’s fine. If you have one, you have downtime. This blue-green pattern is specifically for when you cannot afford that gap.
I’m still experimenting with automating the "cleanup" phase where the old environment is automatically killed after a successful switch. For now, I leave it running for an hour—just in case I need to roll back at 2 AM. It’s not elegant, but it works, and I’d rather have a slightly cluttered server than a broken production environment.
Docker socket activation with Systemd and Nginx lets you hot-swap containers without dropping connections. Learn this robust deployment engineering strategy.