Docker Compose Rollbacks for Safer App Releases on Ubuntu

What you are building
This tutorial shows you how to run safer application releases on Ubuntu Server 24.04 with Docker Compose, using a release folder, a reverse-proxy-ready app layout, health checks, and a rollback path you can use under pressure. The goal is not a toy container demo. You will finish with a production-style deployment pattern that lets you cut over, verify, and revert cleanly on a Hostperl VPS if a release misbehaves.
If your app already sits behind Nginx, you can pair this approach with the patterns in Nginx Reverse Proxy for Node.js Apps on Hostperl VPS and keep the application layer isolated from the public edge. For release safety, the structure here also complements Docker Compose Health Checks for Safer App Releases, but this tutorial goes deeper into full rollback sequencing on Ubuntu.
You will use a stable deployment path under /opt/myapp, keep the previous release available, and test the new one before swapping traffic. That matters for real customer sites, especially stores and client portals where a bad deployment means support tickets, not just inconvenience.
Prerequisites and rollout plan
This procedure is written for Ubuntu Server 24.04 on a fresh VPS. It assumes your application can run in Docker containers and that you already have a domain or internal hostname ready for a reverse proxy or direct port test. If you are using Hostperl VPS hosting, this is a sensible fit for staging, production APIs, and small business apps that need predictable rollback behavior.
- Ubuntu Server 24.04
- Docker Engine and Docker Compose plugin
- A non-root sudo user named
deploy - An application image or Dockerfile already prepared
- Port 8000 for the example application
The safe sequence is straightforward: log in as root, create the admin user, test the new SSH login, install Docker, deploy a first release, validate it locally, then switch traffic only after the new release passes checks. Do not disable root SSH until the new user session works.
Connect to the VPS and identify the OS
On your local computer, open your first SSH session:
ssh root@203.0.113.10203.0.113.10 is a reserved documentation address. Replace it with the public IP assigned to your real server.
Now confirm the operating system before you touch packages or firewall rules.
cat /etc/os-releaseOn Ubuntu 24.04, you should see NAME="Ubuntu" and VERSION_ID="24.04". If you do not, stop and follow the correct branch for your distribution.
Create the deploy user and keep root open
On the VPS as root, create a non-root administrator, add it to sudo, and prepare SSH access. Keep the original root session open until the new login is verified in a second terminal.
adduser deploySet a strong password when prompted. Then add the account to the sudo group.
usermod -aG sudo deployCreate the SSH directory and permissions for key-based login.
install -d -m 700 -o deploy -g deploy /home/deploy/.sshCopy your public key into the file. If you already have a key on your local machine, run this from there in a second terminal.
ssh-copy-id deploy@203.0.113.10If you prefer to copy it manually, append your public key to /home/deploy/.ssh/authorized_keys, then lock down permissions.
chmod 600 /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.sshOn your local computer, open a second terminal and test the new login.
ssh deploy@203.0.113.10After login, confirm sudo works.
sudo -v
whoami
pwdYou should see deploy for whoami and a successful sudo prompt. Only after this works should you consider restricting root SSH later.
Install Docker and Compose on Ubuntu 24.04
On the VPS as root, update the system and install Docker from Ubuntu’s packages. This keeps the tutorial clean on a fresh VPS and avoids mixing instructions from other distributions.
apt update
apt -y upgrade
apt -y install docker.io docker-compose-pluginCheck the installed versions so you know the daemon and Compose plugin are available.
docker --version
docker compose versionEnable and start Docker.
systemctl enable --now dockerAdd the deploy user to the Docker group so you can manage containers without root every time.
usermod -aG docker deployLog out and back in as deploy for that group change to take effect.
Build a release directory layout
On the VPS as the non-root sudo user, create a layout that keeps releases and shared data separate. That is what makes rollback quick.
sudo -iu deploy
mkdir -p /opt/myapp/releases/v1
mkdir -p /opt/myapp/releases/v2
mkdir -p /opt/myapp/shared
cd /opt/myapp
pwdYou should now be in /opt/myapp. The idea is to place each compose file in a release directory, then point traffic at the current one through a stable path or proxy target.
Create the first Compose release
For this example, the app listens on port 8000 inside the container and publishes the same port on the host. Adjust image names and environment variables for your real application, but keep the release structure intact.
On the VPS as the non-root sudo user, create the first release file.
cd /opt/myapp/releases/v1
nano docker-compose.ymlPaste this complete file content, then save and exit with Ctrl+O, Enter, Ctrl+X.
services:
web:
image: nginx:1.27-alpine
container_name: myapp-web-v1
ports:
- "8000:80"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost/ || exit 1"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10sThis uses Nginx only as a predictable example container. Replace it with your real image when you deploy your application. The health check matters more than the demo image. Compose will mark the container unhealthy if the app stops answering.
Start the release and test it locally
On the VPS as the non-root sudo user, bring the stack up and inspect status.
cd /opt/myapp/releases/v1
docker compose up -d
docker compose psLook for running and a healthy state after a few seconds. Then test the endpoint from the server itself.
curl -I http://127.0.0.1:8000A successful response should show HTTP/1.1 200 OK or another expected 2xx/3xx status for your app. If the port does not answer, do not move on.
Check the container logs if something looks wrong.
docker compose logs --no-color --tail=100For a production app, this is usually where you catch a missing environment variable, bad image tag, or startup crash.
Switch traffic safely with a stable path
In practice, many teams front Docker with Nginx or another proxy, then point the proxy to a stable port or upstream. If your app has a private port change between releases, keep the proxy target stable and only update the release directory after validation. That avoids exposing half-baked builds to customers.
If you are using Nginx, follow the reverse-proxy layout from Nginx Reverse Proxy for Node.js Apps on Hostperl VPS. Your release can stay private on localhost while Nginx handles public TLS and port 80/443.
Prepare the second release and compare behavior
Now simulate a new release in /opt/myapp/releases/v2. This could be a new image tag, new environment file, or a rebuilt application container.
On the VPS as the non-root sudo user, clone the first file so you can adjust it.
cd /opt/myapp/releases
cp -a v1 v2
cd v2
nano docker-compose.ymlChange the container name and, if needed, the image tag. This example keeps the port mapping the same so the test is easy to follow.
services:
web:
image: nginx:1.27-alpine
container_name: myapp-web-v2
ports:
- "8000:80"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost/ || exit 1"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10sBefore starting it, check the file syntax by reading it back and comparing the result. For Compose files, the main syntax check is practical: bring the container up in the new release path and observe whether it starts cleanly.
docker compose up -d
docker compose ps
curl -I http://127.0.0.1:8000If the new release is on the same public port, stop the old one first or use a proxy cutover. The safe pattern is to validate v2 on an alternate internal port or separate hostname, then move traffic once the checks pass.
Docker Compose rollbacks for safer app releases
Rollback should be boring. Stop the bad release, start the last known good one, and confirm the service state before you tell customers the issue is fixed.
On the VPS as the non-root sudo user, stop the new release and return to v1.
cd /opt/myapp/releases/v2
docker compose down
cd /opt/myapp/releases/v1
docker compose up -d
docker compose psConfirm the old release is responding again.
curl -I http://127.0.0.1:8000If you have a proxy layer, reload it only after the rollback is healthy. Never reload the proxy first and then discover the backend is still broken.
Make startup persistent after reboot
Docker’s restart policy handles container restarts after the daemon comes back, but you still need to confirm the service survives a reboot. Do a controlled test during a maintenance window.
On the VPS as root or the non-root sudo user with sudo, check Docker and the active release after reboot.
sudo systemctl is-enabled docker
sudo systemctl status docker --no-pagerThen confirm the container came back.
docker ps
curl -I http://127.0.0.1:8000If the container is missing after reboot, check the Docker daemon logs and release directory permissions.
sudo journalctl -u docker -b --no-pager | tail -n 80
ls -ld /opt/myapp /opt/myapp/releases /opt/myapp/releases/v1Firewall and access checks on Ubuntu
If this server faces the internet directly, open only the ports you need. On Ubuntu, UFW is the simplest option for a VPS.
sudo ufw allow OpenSSH
sudo ufw allow 8000/tcp
sudo ufw enable
sudo ufw status verboseYou should see OpenSSH and port 8000 allowed. If you put Nginx in front, expose 80 and 443 instead of the app port.
Troubleshooting the most likely failures
Container starts, then exits immediately
Diagnostic:
docker compose ps
docker compose logs --tail=100Expected clue: a missing environment variable, a bad image tag, or an application crash loop. Fix the compose file, then run docker compose up -d again.
Port 8000 is already in use
Diagnostic:
sudo ss -ltnp | grep ':8000'Expected clue: an older container or another service is binding the port. Stop the conflicting process with docker compose down in the old release directory, or choose a different internal port for the next release.
Docker daemon does not start after reboot
Diagnostic:
sudo journalctl -u docker -b --no-pager | tail -n 100Expected clue: storage corruption, a broken package, or permission errors. Correct the underlying issue, then run sudo systemctl enable --now docker again.
Health check stays unhealthy
Diagnostic:
docker inspect myapp-web-v1 --format '{{json .State.Health}}'Expected clue: the app does not answer on localhost, or the health command is wrong for the image. Adjust the health check so it matches a real endpoint, then redeploy.
Why this pattern works for real hosting customers
Rolling back by release directory gives you a practical safety net. Agencies use it for client launches, small businesses use it for storefront updates, and support teams use it when a release needs to be reversed before business hours start.
On Hostperl VPS hosting, this pattern fits teams that want control without building a full orchestration stack on day one. If you later outgrow a single node, you can move the same application into a larger environment or a dedicated server without changing how you think about release validation. For heavier workloads, Hostperl VPS hosting is a practical place to start, and dedicated server hosting is the next step when your app needs more consistent CPU, memory, or storage headroom.
If you are deploying customer-facing applications, Hostperl can help you keep releases controlled and reversible. Start on a Hostperl VPS for app staging and smaller production loads, or move to dedicated server hosting when your release traffic and storage needs grow.
Our support team is used to migration windows, rollback pressure, and launch-day surprises, so you are not left guessing when a container refuses to start.
FAQ
Should I expose Docker directly to the internet?
No. Publish only the ports your app needs, and keep databases or internal services private. If you use a reverse proxy, expose 80 and 443 on the proxy, not the app container.
Do I need blue-green infrastructure for every release?
No. A single-host release directory structure is enough for many small teams. Blue-green becomes useful when you need simultaneous validation or tighter cutovers.
What is the safest rollback trigger?
Rollback as soon as the new release fails a health check, returns errors on a smoke test, or starts generating support-facing errors. Do not wait for a larger outage.
Can I use this on Debian or RHEL-based systems?
Yes, the release structure and rollback method are portable. The package and firewall commands differ, so adjust them for the target distribution before you deploy.
Final verification checklist
Before you call the release complete, confirm these from the server and from a client connection:
docker compose psshows the service runningcurl -I http://127.0.0.1:8000returns the expected responsedocker compose logs --tail=100shows no repeated crashessudo systemctl is-enabled dockerconfirms startup persistence- A browser or external client reaches the public endpoint through your proxy or exposed port
That last smoke test is the one that matters to customers. If it fails, the release is not done yet.
