IPv4 & IPv6 Leasing - Any RIR, Any LocationOrder Now
Hostperl

Docker Compose Deployment Rollback on Debian 12 VPS

By Raman Kumar

Share:

Updated on Sep 5, 2026

Docker Compose Deployment Rollback on Debian 12 VPS

What this tutorial solves

A failed container release should not turn into a long outage. This guide walks you through a Docker Compose deployment rollback on a Debian 12 VPS with a safe cutover, versioned release directories, health checks, and a clear recovery path. You will set up a non-root deploy user, publish a sample app behind Nginx, verify the new release, and roll back quickly if the health check fails.

This workflow fits teams hosting small business apps, client portals, internal tools, and API services on a Hostperl VPS. It also matches the way support teams handle launch windows: keep the old release intact, verify the new one, and switch traffic only after it passes.

If you want a broader deployment pattern later, see Docker deployment checklist for safer launches and Blue-green Docker deployments on RHEL 9 VPS for related production workflows.

Scenario and release design

We will deploy a simple web app on Debian 12 using Docker Compose, Nginx as the front door, and a versioned release layout under /opt/myapp. Each release lives in its own directory, so rollback means pointing the reverse proxy back to the previous container stack instead of rebuilding from scratch.

  • Host OS: Debian 12
  • App port: 8000 inside the container stack
  • Public entry: Nginx on port 80
  • Rollback method: stop the new release, restore the previous Compose stack, reload Nginx

This approach avoids a common post-launch failure: the app is fine, but the newest image has a bad env variable, a broken migration, or a startup regression.

Connect to the VPS and confirm Debian 12

On your local computer, connect first.

ssh root@203.0.113.10

203.0.113.10 is a reserved documentation example. Replace it with your VPS public IP from Hostperl or your provider.

On the VPS as root, check the operating system before you change anything.

cat /etc/os-release

You should see Debian 12 fields such as ID=debian and VERSION_CODENAME=bookworm. If you are not on Debian 12, stop here and adapt the package and firewall commands to your distribution.

Create a deploy user and keep root as a fallback

Do not disable root access yet. Create a sudo user first, add your SSH key, and test a second login. That gives you a fallback if you mistype a permission or firewall rule later.

On the VPS as root, create the account and grant sudo.

adduser deploy

Set a strong password when prompted. Then add the user to the sudo group.

usermod -aG sudo deploy

Prepare the SSH directory and key file. Replace the example public key with your own key from your local machine.

install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
printf '%s
' 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleReplaceThisWithYourOwnKey deploy@laptop' > /home/deploy/.ssh/authorized_keys
chown deploy:deploy /home/deploy/.ssh/authorized_keys
chmod 600 /home/deploy/.ssh/authorized_keys

The key file should be readable only by deploy. If you use ssh-copy-id from your workstation, the result is the same.

On your local computer, open a second terminal and test the new login while keeping the root session open.

ssh deploy@203.0.113.10

Then verify sudo access.

sudo -v

If that works, keep this terminal open. You will use it for the deployment.

Update packages and install Docker Compose support

On the VPS as root, refresh Debian and install the packages needed for Docker Compose management.

apt update
apt -y upgrade
apt -y install ca-certificates curl gnupg lsb-release

Next, install Docker from the official repository and add the Compose plugin. Debian 12 ships with an older Docker version in the default repo, so the upstream package source is the better production choice here.

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable" > /etc/apt/sources.list.d/docker.list
apt update
apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Check the version after installation.

docker --version
docker compose version

Add deploy to the Docker group so you do not need root for every release action.

usermod -aG docker deploy

Log out and back in as deploy for the new group membership to take effect.

Set up the release directory and sample app

On the VPS as the non-root sudo user, create a structured deployment tree. This keeps the active release, previous release, and shared config separate.

sudo mkdir -p /opt/myapp/releases/2026-09-01
sudo mkdir -p /opt/myapp/releases/2026-08-28
sudo chown -R deploy:deploy /opt/myapp

Move into the new release directory.

cd /opt/myapp/releases/2026-09-01
pwd

Create a minimal app container and Compose file. This example uses a small Python HTTP server so the rollback process stays focused on Compose, not application framework code.

cat > Dockerfile <<'EOF'
FROM python:3.12-slim
WORKDIR /app
COPY app.py /app/app.py
EXPOSE 8000
CMD ["python", "/app/app.py"]
EOF

cat > app.py <<'EOF'
from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        body = b'OK release 2026-09-01\n'
        self.send_response(200)
        self.send_header('Content-Type', 'text/plain')
        self.send_header('Content-Length', str(len(body)))
        self.end_headers()
        self.wfile.write(body)

HTTPServer(('0.0.0.0', 8000), Handler).serve_forever()
EOF

cat > compose.yml <<'EOF'
services:
  app:
    build: .
    container_name: myapp_2026_09_01
    restart: unless-stopped
    ports:
      - "8000:8000"
EOF

Build and start the release.

docker compose up -d --build

Check that the container is running and listening on port 8000.

docker compose ps
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'

Put Nginx in front of the app

Nginx gives you a stable public endpoint and keeps the container port private to localhost. It also makes rollback less disruptive because the proxy target changes quickly.

On the VPS as root, install Nginx.

apt -y install nginx

Create a site file for the reverse proxy.

cat > /etc/nginx/sites-available/myapp.conf <<'EOF'
server {
    listen 80;
    server_name server.example.com;

    access_log /var/log/nginx/myapp_access.log;
    error_log /var/log/nginx/myapp_error.log;

    location / {
        proxy_pass http://127.0.0.1:8000;
        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;
    }
}
EOF
ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/myapp.conf
rm -f /etc/nginx/sites-enabled/default

Replace server.example.com with your real hostname. Before reload, test syntax.

nginx -t

If the test is clean, reload Nginx.

systemctl reload nginx

Verify the service and the public response from the server itself.

systemctl status nginx --no-pager
curl -i http://127.0.0.1/
curl -i http://127.0.0.1:8000/

Open the firewall safely

On the VPS as root, allow SSH and web traffic before you close any existing path. Debian 12 commonly uses nftables underneath, and UFW is a straightforward control layer for this tutorial.

apt -y install ufw
ufw allow OpenSSH
ufw allow 80/tcp
ufw enable
ufw status verbose

If you already use a stricter firewall stack, mirror these allowances there first and test from a second terminal before removing any older rule.

Test the release and capture logs

On your local computer, test the public endpoint.

curl -i http://203.0.113.10/

You should see 200 OK and the release text. If you use a domain, test the hostname as well.

curl -i http://server.example.com/

On the VPS as root, inspect logs if anything looks off.

journalctl -u nginx --no-pager -n 50
docker compose logs --tail=50

This log split is useful during support calls. Nginx errors usually show proxy or syntax problems. Container logs show app startup failures, bad environment variables, or missing files.

For a deeper look at log handling during container rollouts, see Nginx log rotation and app log debugging. The command patterns are similar even though this tutorial stays on Debian.

Roll back to the previous release

Now simulate a bad deployment by changing the app response, or assume your new image failed health checks. The rollback steps are the same either way.

On the VPS as the non-root sudo user, stop the current release.

cd /opt/myapp/releases/2026-09-01
docker compose down

Move to the last known good release directory and start it.

cd /opt/myapp/releases/2026-08-28
docker compose up -d --build

Update the Nginx proxy target only if the rollback release uses a different local port. If both releases publish to port 8000, no proxy edit is needed. If you must change it, edit the site file, test syntax, and reload.

sudo nano /etc/nginx/sites-available/myapp.conf

Make the port change if required, save the file, then run:

sudo nginx -t
sudo systemctl reload nginx

Finally confirm the rollback.

curl -i http://127.0.0.1/
docker compose ps

Make the stack survive a reboot

Docker restart policies handle the container side. System services handle the proxy side. Verify both after a reboot test window.

On the VPS as root, enable Nginx at boot and check Docker.

systemctl enable nginx
test -f /lib/systemd/system/docker.service && systemctl is-enabled docker

Reboot only after your maintenance window allows it.

reboot

After reconnecting, verify the services again.

systemctl status nginx --no-pager
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
curl -i http://127.0.0.1/

Troubleshooting the most likely failures

1) The site returns 502 Bad Gateway. Check whether the container is listening.

docker compose ps
ss -ltnp | grep 8000

If nothing listens on 8000, rebuild the stack.

docker compose up -d --build

2) Nginx fails to reload. Validate the config and look for a typo in the server name or proxy block.

nginx -t
journalctl -u nginx --no-pager -n 30

Correct the file in /etc/nginx/sites-available/myapp.conf, then test again.

3) The firewall blocks the site from your laptop. Confirm rules and listening ports.

ufw status verbose
ss -ltnp | grep ':80'

If port 80 is missing, reopen it with ufw allow 80/tcp and reload the firewall.

4) The rollback release still shows the bad version. Check which directory is active and which container is running.

cd /opt/myapp/releases/2026-08-28
docker compose ps
docker logs myapp_2026_09_01 --tail 20

If the wrong release is still up, run docker compose down in the newer directory before bringing the older one online.

Operational notes for Hostperl customers

A rollback plan matters most during client launches, billing cycles, and promotions. If you run application stacks for customers or agencies, keep the previous Compose file, image tag, and environment file for at least one full release cycle. That makes support faster and reduces the chance of a late-night rebuild.

For larger or busier deployments, a managed VPS hosting plan or a higher-spec instance is often the practical next step. If your release cadence is frequent, pair this with regular restore drills and container log review.

If you want a VPS that fits container workloads without guesswork, Hostperl can help you size CPU, RAM, and storage for the release pattern you actually run. For app hosting with room to grow, start with Hostperl VPS hosting and keep your deployment path simple.

For customer-facing projects that need more headroom, consider a stronger dedicated server hosting option and keep rollback windows short.

FAQ

Can I use this rollback method with images from a registry?
Yes. Keep the previous image tag available, update the Compose file to the known-good tag, and restart the stack. Do not delete the last working image until the new release passes health checks.

Should I store secrets in the Compose file?
No. Use a separate environment file with restrictive permissions, or a secret manager if your stack already supports one. Limit readable access to the deploy user and root.

Why use versioned release directories?
They preserve the exact files that produced a working deployment. When a launch fails, you can return to the prior directory without rebuilding the whole stack.

What should I check first after a failed release?
Check container status, Nginx syntax, and logs. Those three checks usually expose the cause faster than restarting everything.