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.10203.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-releaseYou 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 deploySet a strong password when prompted. Then add the user to the sudo group.
usermod -aG sudo deployPrepare 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_keysThe 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.10Then verify sudo access.
sudo -vIf 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-releaseNext, 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-pluginCheck the version after installation.
docker --version
docker compose versionAdd deploy to the Docker group so you do not need root for every release action.
usermod -aG docker deployLog 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/myappMove into the new release directory.
cd /opt/myapp/releases/2026-09-01
pwdCreate 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"
EOFBuild and start the release.
docker compose up -d --buildCheck 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 nginxCreate 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/defaultReplace server.example.com with your real hostname. Before reload, test syntax.
nginx -tIf the test is clean, reload Nginx.
systemctl reload nginxVerify 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 verboseIf 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=50This 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 downMove to the last known good release directory and start it.
cd /opt/myapp/releases/2026-08-28
docker compose up -d --buildUpdate 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.confMake the port change if required, save the file, then run:
sudo nginx -t
sudo systemctl reload nginxFinally confirm the rollback.
curl -i http://127.0.0.1/
docker compose psMake 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 dockerReboot only after your maintenance window allows it.
rebootAfter 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 8000If nothing listens on 8000, rebuild the stack.
docker compose up -d --build2) 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 30Correct 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 20If 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.
