Loading Smiley Hoster
Skip to content

Deploy with Docker Compose on a VPS and prepare a seamless update

Smiley Hoster Team · · 2 min read
Two application versions run side by side while an arrow moves traffic to the ready version.

Docker's production guide describes recreating a service when its image changes. A single replacement container leaves a service gap; this design keeps the old application running while a separate Compose project is prepared and checked.

1. Define the deployment boundary

You need Docker Engine, the Compose plugin, an HTTPS Nginx site and a tested application image. Inside the container, the app must listen on 0.0.0.0:3000. Its /health route should succeed only when required dependencies are ready.

Use this recipe for a stateless HTTP frontend with shared external storage and sessions. Keep scheduled jobs and workers outside the duplicated deployment. Both versions must understand the database schema; take a backup before schema changes.

Create compose.yaml in a dedicated deployment directory:

Example
services: web: image: ${APP_IMAGE:?APP_IMAGE required} ports: - "127.0.0.1:${APP_PORT:?APP_PORT required}:3000" restart: unless-stopped mem_limit: 512m logging: driver: json-file options: max-size: "10m" max-file: "3"

Supply any additional settings your application needs. The memory cap is an illustrative starting value: measure your workload. Budget space for both copies, the operating system, Nginx and headroom.

Create blue.env containing APP_IMAGE= followed by your actual immutable image tag or digest, and APP_PORT=3001. In green.env, use the replacement image and APP_PORT=3002. These files feed Compose interpolation; they do not automatically become container environment variables.

2. Establish the live route, then the candidate

Run these commands from the deployment directory:

Example
docker compose --env-file blue.env -p blue pulldocker compose --env-file blue.env -p blue up -dcurl -fsS http://127.0.0.1:3001/health

Put this upstream in /etc/nginx/conf.d/app-upstream.conf:

Example
upstream app_backend { server 127.0.0.1:3001;}

Use it in your domain's HTTPS server block:

Example
location / { proxy_pass http://app_backend; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme;}

Now bring up green independently:

Example
sudo nginx -t && sudo systemctl reload nginxdocker compose --env-file green.env -p green pulldocker compose --env-file green.env -p green up -dcurl -fsS http://127.0.0.1:3002/healthdocker compose --env-file green.env -p green logs \ --tail=50 webdocker stats --no-stream

Exercise a real application operation through port 3002 as well. Process existence is weaker evidence than a successful request. Restart policies handle process exits; an application can remain stuck while its process stays alive.

3. Switch traffic with a rollback available

Change 3001 to 3002 in the upstream file, then validate and reload:

Example
sudo nginx -t && sudo systemctl reload nginxcurl -fsS https://example.com/health

Substitute your domain. Check from another network, confirm the served version and inspect errors and business operations. Leave blue running until requests using the previous configuration finish. Long-lived connections require an application-specific draining policy.

If acceptance fails, restore port 3001 and repeat the Nginx test and reload. A rollback works only while the old application remains compatible with the data. After validation and draining:

Example
docker compose --env-file blue.env -p blue stop

Frequent mistakes: binding both projects to one host port, using a moving latest tag, leaving too little RAM for overlap or applying an incompatible database migration. Do not use down -v as routine cleanup: volume deletion is outside this procedure.

This arrangement can avoid an application-update outage when its prerequisites hold. It does not protect against failure of the single VPS.

Sources: Docker production guide, Docker Compose services.

Slide 1 of 6: VPS guide. Update your app with no gap in service. Run the new version beside the old one, then switch.
Slide 2 of 6: Step 1 of 3. Define the boundary. Use this for a stateless HTTP frontend with shared storage and sessions. The app listens on 0.0.0.0:3000, and /health succeeds only when dependencies are ready. Keep workers and jobs outside.
Slide 3 of 6: Step 2 of 3. Bring up the candidate beside the live one. Two Compose projects, two ports: blue on 3001 behind Nginx, green on 3002. Check the health route of green, then run a real operation through it. A running process is weaker evidence than a successful request.
Slide 4 of 6: Step 3 of 3. Switch traffic with a rollback ready. Point the Nginx upstream at 3002, test the config and reload. Check from another network and confirm the served version. Leave blue running until the older requests finish, then stop it.
Slide 5 of 6: Frequent mistakes. Four things that break the switch. Binding both projects to the same host port. Using a moving latest tag instead of a fixed one. Too little RAM for two copies running at once. Applying an incompatible database migration
Slide 6 of 6: Full guide. Ready to ship without a gap?. The Compose files, the health checks and the rollback path are all in the Smiley Hoster Guide.
On social media

See it in our social posts

The key steps of this article, as a carousel. Follow us to catch the next ones.

Did this article answer your question?
Your feedback shapes what we write next.

Your site online today

Free migration* · 30-day refund

Get started* A site under 30 GB, cPanel, WordPress and VPS plans.