# How to Roll Back a Docker Deploy on a VPS (Beginner Guide)

> Beginner guide to rolling back a Docker deploy on a VPS: unique image tags, record the old version, deploy with --wait, restore the tag, verify, and the limits.

URL: https://www.matthewswong.com/en/blog/cara-rollback-deploy-docker-di-vps/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2026-10-17
- Updated: 2026-10-17
- Category: DevOps

> **Key takeaway:** To roll back a Docker deploy on a VPS: give every image a unique tag such as the commit SHA, record the tag that is running, and when a release breaks, set IMAGE in .env back to the previous tag and run docker compose up -d --wait. Verify with docker compose ps and curl. A rollback restores code, not data or database migrations.

You just deployed a new version and the home page shows an error. Visitors have already seen it, and in the terminal you type whatever comes to mind: git reset, rebuild, docker compose down. Most of the time is lost not because a rollback is hard, but because nobody knows exactly which version used to work, or that version has been overwritten by the latest tag.

This guide is the rollback steps only, not a guide to detecting problems: you set the system up before a release (unique tags, a record of versions), then use it when a release breaks. The examples follow the same pattern as the setup behind this site, rewritten with placeholders, and assume you already have a production Compose file like the one in the 

guide to deploying Docker Compose in production

. Every flag is checked against the official documentation listed at the end.

## Want the concepts first?

This post is the hands-on rollback walkthrough. Our free lesson explains the bigger picture, from health checks to backups, with an interactive diagram of a bad release being rolled back:

Monitoring, Rollback and Backups for a Docker Server

The lesson also has a short quiz and a rollback drill you can run on your laptop.

## What you need

Get these four things ready before the next release, not after something breaks. Every name below is a placeholder, so replace it with your own.

A Linux VPS with Docker Engine and the Compose plugin, and SSH access as a normal user (named deploy in the examples).

An app run through docker-compose.yml in /home/deploy/my-app, with the image tag read from an IMAGE variable in the .env file.

Images pushed to a registry (ghcr.io/example-user/my-app in the examples), so an old version can always be pulled again.

One health check in the Compose file. Without a health check, --wait has no yardstick for calling a release failed.

## Step 1: Give every image a unique tag, not just latest

A rollback means running the previous known-good version again. That is only quick when the version has a name that points at exactly one build. The latest tag moves with every push, so after a bad release latest points at the bad build. Use the commit SHA as the tag: it is never reused, so it always means the same build.

This site's pipeline pushes every image twice, as latest and as the full commit SHA, and the deploy step writes that SHA tag into the server's .env file as IMAGE. So the live version is always one exact build. The example above uses a short SHA to keep it typeable; in CI, let the pipeline tag the image.

In docker image ls and in the registry, every build has its own tag, and an old tag does not change when a new build arrives.

## Step 2: Record the running version before you deploy

In a panic, the tag that was running well is the first thing to slip from memory. Write it down before changing anything. One line in a text file is enough, and do not copy the whole .env because it contains secrets.

One line shaped like IMAGE=ghcr.io/example-user/my-app followed by a tag appears in the terminal and is saved in deploy-history.txt. That line is your rollback destination.

## Step 3: Check that the previous image is still on the server

A rollback is fastest when it starts from an image already on disk. See what is available. The default columns of docker image ls are REPOSITORY, TAG, IMAGE ID, CREATED and SIZE, and --format with a Go template gives a more compact view.

If the old image is gone, that is not necessarily a problem as long as it is still in the registry: docker compose pull fetches it again, possibly after a docker login. This site's cleanup is a deliberate compromise: images created in the last 72 hours are kept, the portfolio's own images are capped at the newest two (the running release and one for a rollback), and older releases remain in the registry. That cleanup never removes volumes.

A table with several tags of your app's image, including the tag from Step 2. If that tag is missing, note that you will need to pull during the rollback.

## Step 4: Deploy the new version with --wait

A normal deploy uses the same pattern as a rollback: change IMAGE, pull the image, run again. The difference is that we use --wait. Docker's documentation describes it as waiting for services to be running or healthy, and --wait-timeout sets a limit in seconds. Without it, up -d returns once the container is started, without waiting for the health check result.

In the lesson's drill, a release whose /health answered 500 made --wait exit 1 after about 19 seconds, with health checks 5 seconds apart. Docker marks a container unhealthy after retries consecutive failures. Note also that visitors were still served by that broken release, because unhealthy is only a label.

If the release is healthy, exit=0 is printed. If it is unhealthy or times out, the exit code is not zero, and that is your signal to go to Step 5.

--wait only makes the deploy notice the failure, it does not undo anything. This site's own deploy runs docker compose up -d without --wait, so a release that fails its health check still ends the workflow green. The rollback is still a step you have to take.

## Step 5: Roll back by restoring the old tag

This is the whole rollback: write the old tag from Step 2 back into IMAGE, check that Compose reads it, then run up again. Because the old tag points at one exact build, nothing has to be rebuilt. Compose recreates the container because the image differs from the running one.

docker compose config prints the interpolated configuration, so the image: line shows the tag that will actually be used. This small check prevents a rollback that silently changes nothing, for example because the sed pattern did not match. Do not use down -v for a rollback: the -v option removes the named volumes declared in the Compose file, and that means data.

docker compose config shows the old tag, then up -d --wait finishes with exit=0 once the old container is healthy. In the lesson's drill this took about 8 seconds.

## Step 6: Verify from the inside and from the outside

Do not stop at exit=0. The health check runs inside the container, so it cannot see DNS, TLS or the reverse proxy in front of it. Check from two sides: on the server itself, and through the public domain the way visitors arrive.

If the log shows errors, read it the way the 

guide to viewing Docker logs for beginners

 describes. A successful rollback means the old app boots with no new errors.

The container is running, and healthy once the start period has passed. Both curl calls answer with status 200 or a redirect you expect, and the log shows the app is ready.

## Step 7: Make the rollback permanent through git or the pipeline

Editing .env on the server is only a stopgap. This site's pipeline rewrites .env with the IMAGE of the newest build on every deploy, so the next deploy would bring that broken code back unless the code has been fixed. There are two proper ways: git revert the bad commit and push, or re-run the workflow from the last successful run.

GitHub's documentation says a re-run uses the same commit SHA as the original run and is allowed up to 30 days after the first run. git revert is slower but leaves a clear trail in the history. The full pipeline is covered in the 

guide to deploying Next.js to a VPS with Docker and GitHub Actions

.

The new workflow ends green with a healthy IMAGE, and docker compose ps on the server shows the tag you expect. After that, fix the bug calmly and release a new version.

## What a rollback cannot undo

A rollback only swaps code. Database migrations that have already run stay applied, and data written by the new version stays. If the new version dropped or renamed a column, the old version that comes back can fail on the changed schema. In that case the rollback succeeds and the app is still broken.

The prevention is in how you write migrations: change the schema in two phases so both the old and new versions can run, as in the 

guide to zero-downtime migrations and rolling deploys

. For data, treat a rollback as no substitute for a backup. Take a backup before any release that touches the database, and test the restore, as in the 

3-2-1 backup strategy for a VPS

.

Do not roll back by deleting volumes or running docker compose down -v. That does not bring the old code back, it only erases the app's and the database's data.

## Common errors and how to fix them

These are what most often go wrong the first time you roll back. Match your symptom to the first column, then read the cause and the fix.

Symptom

Cause

Fix

You ran the rollback but the app is still broken

IMAGE did not change (the sed pattern did not match, or you are in the wrong folder), or you used latest, which already points at the broken build.

Run grep IMAGE .env and docker compose config, make sure the old tag shows up, and use the SHA tag, not latest.

Compose fails to pull the image during the rollback

The tag is mistyped, you are not logged in to the registry, or the image has been deleted from the registry.

Compare the tag with deploy-history.txt, run docker login for the registry, then repeat docker compose pull.

The old image is not on disk

Cleanup such as docker image prune -a with no filter removed every image no container uses.

Pull it again with docker compose pull. Going forward, use an age filter such as until=72h and never prune volumes.

After the rollback, up -d --wait still exits with a non-zero code

The old version is unhealthy too: a migration changed the database schema, or the configuration in .env has changed.

Read docker compose logs --tail 50 web. If the schema is the cause, a rollback alone is not enough; restore from a backup or ship a fix forward.

The next deploy brings the broken code back

The pipeline rewrites IMAGE in .env on every deploy, and the broken commit is still on the branch.

Run git revert and push, or fix the bug before deploying again.

App data is gone after the rollback

The rollback was done with docker compose down -v, which removes the named volumes in the Compose file.

Restore from the latest backup. For a rollback, just change IMAGE and run up -d --wait, without down -v.

The container is healthy but visitors still see an error

The health check only looks from inside the container, so problems in DNS, TLS or the reverse proxy are invisible to it.

Run curl against the public domain, not only 127.0.0.1, and check the proxy configuration.

## Checklist before the next release

Every image is pushed with a commit SHA tag, and the server reads that tag from IMAGE in .env.

The running tag is recorded in deploy-history.txt before IMAGE is changed.

The previous version's image is still on disk, or certainly in the registry and you are logged in.

The deploy uses docker compose up -d --wait with --wait-timeout, and the exit code is watched.

After a rollback you verify with docker compose ps, a local curl, a curl to the public domain, and the log.

A release that touches the database has a tested backup, and its migration can run alongside the old version.

## Learn more

This guide answers one question: how to bring back a broken release. Our free lesson puts it in a bigger picture, including a bad-release drill you can run on your laptop and a list of what needs a backup on one server.

Open the lesson: Monitoring, Rollback and Backups

Practise this rollback once on your laptop before you need it on a server that is on fire. A rollback you have tried is far calmer than one you run for the first time while visitors are looking at an error.

## Sources

- Docker docs: docker compose up (--wait, --wait-timeout)
- Docker docs: docker compose down (the --volumes option)
- Docker docs: docker image ls (columns, --format, --filter)
- Docker docs: HEALTHCHECK (interval, retries, start period)
- GitHub docs: Re-running workflows and jobs

Expected result

## Source links

- https://docs.docker.com/reference/cli/docker/compose/up/
- https://docs.docker.com/reference/cli/docker/compose/down/
- https://docs.docker.com/reference/cli/docker/image/ls/
- https://docs.docker.com/reference/dockerfile/#healthcheck
- https://docs.github.com/en/actions/how-tos/manage-workflow-runs/re-run-workflows-and-jobs

## FAQ

### Why does my Docker rollback not change anything?

Most often IMAGE in .env did not actually change, because the sed pattern did not match or you ran the command in the wrong folder. Run docker compose config and look at the image: line before running up. If you point IMAGE at latest, it already refers to the broken build, so use a commit SHA tag.

### Does docker compose up --wait roll back a failed release?

No. --wait waits for services to be running or healthy and exits with a non-zero code on failure, but it never undoes the deploy. The broken release can keep serving visitors, so the rollback is a separate step you or a script must run.

### Can I roll back a Docker deploy if I tagged my image latest?

Not reliably, because latest moves with every push and after a bad release it points at the bad build. If an older image still exists on the server you can find it by IMAGE ID, but a unique tag such as the commit SHA is the safe habit. Start tagging every build now.

### Does a rollback also undo my database migration?

No. A rollback only swaps the code running in the container, and migrations that already ran stay applied. Write migrations in two phases so the old version still works, and take a tested backup before a release that touches the database.

### Is it safe to use docker compose down -v to roll back?

No. The -v option removes the named volumes declared in the Compose file, which usually hold your data. To roll back, change IMAGE to the previous tag and run docker compose up -d --wait, and leave the volumes alone.

---

- HTML version: https://www.matthewswong.com/en/blog/cara-rollback-deploy-docker-di-vps/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/cara-rollback-deploy-docker-di-vps/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
