Docker & DevOps

Roll back a bad deploy using Docker image tags

Tag every release, pin the version in your Compose file, and switch back to the previous image when a deploy breaks production. Includes verification and rollback of the database step.

IntermediateLinux server6 min read

A rollback with Docker image tags means redeploying the exact image that worked before, not rebuilding from older source. If you tag every release and pin that tag in your Compose file, recovery is one edit and one docker compose up -d away.

This guide assumes you already build and deploy images to a registry. If you do not, start with Build and push Docker images with GitHub Actions first.

Before you start

You need:

  • A Linux VPS with Docker and the Compose plugin installed. See Install Docker and Docker Compose on a Linux VPS if you are starting fresh.
  • SSH access to the server, and a user in the docker group (or sudo).
  • A registry you can push to — Docker Hub, GHCR, or a private registry.
  • A docker-compose.yml that references an image by tag, not by latest.
  • The current running version noted somewhere, so you know what to go back to.

Tip: Never deploy latest to production. A floating tag gives you no target to roll back to, because the tag itself has already moved.

Tag releases so you always have a target

A useful release has two tags: a human version and the Git commit SHA. The version is easy to read; the SHA is unique and never reused.

Build and tag locally, then push both tags:

Bash
export VERSION=1.4.2
export SHA=$(git rev-parse --short HEAD)

docker build -t registry.example.com/myapp:${VERSION} -t registry.example.com/myapp:${SHA} .
docker push registry.example.com/myapp:${VERSION}
docker push registry.example.com/myapp:${SHA}

If your CI does the build, make it push the same pair. The GitHub Actions build guide shows how to derive the tag from the Git ref.

Check what is actually on the host before you rely on it:

Bash
docker image ls registry.example.com/myapp

You should see the new version and the SHA side by side. If the previous version is missing from the registry, you cannot roll back to it — keep old tags around for at least a few releases.

Pin the tag in docker-compose.yml

Open your Compose file and replace any floating tag with the version you just pushed.

YAML
services:
  app:
    image: registry.example.com/myapp:1.4.2
    restart: unless-stopped
    env_file:
      - .env
    ports:
      - "127.0.0.1:8080:8080"

Commit that change. The Compose file is now the record of what is deployed, which is what makes the rollback auditable later.

Note: If you use override files for environments, keep the image tag in the base file so production and staging cannot drift. See Docker Compose override files.

Deploy the pinned version:

Bash
docker compose pull app
docker compose up -d app
docker compose ps

pull fetches the exact tag, up -d recreates only the container whose image changed, and ps shows the new container as running.

Roll back to the previous tag

When the new version misbehaves, the fix is to point the Compose file at the tag that worked.

Warning: Recreating a container stops the running process. If your app writes to a local volume, make sure writes are flushed and, for anything with a database, take a backup first — see Back up your Linux VPS with rsync, restic and cron.

  1. Note the bad version so you can investigate later:
Bash
docker compose images app
  1. Edit docker-compose.yml and set the image back to the last good tag:
YAML
    image: registry.example.com/myapp:1.4.1
  1. Pull and recreate:
Bash
docker compose pull app
docker compose up -d app
  1. Confirm the container is running the old image:
Bash
docker inspect --format '{{.Config.Image}}' $(docker compose ps -q app)

The output should read registry.example.com/myapp:1.4.1.

If the image is already on the host, pull is a no-op and the switch takes a second. If it is not, Docker fetches it from the registry first.

Tip: For a faster emergency switch, keep the previous image locally with docker image tag registry.example.com/myapp:1.4.1 myapp:previous and reference myapp:previous in a temporary override file.

Handle database migrations before you roll back

Code rolls back cleanly. Schema changes often do not. If the bad release ran a migration that dropped or renamed a column, the old image may fail against the new schema.

Check what the release changed:

Bash
docker compose logs --tail=200 app | grep -i migrat

If the migration is not reversible, you have two options: restore the database from the backup taken before the deploy, or roll forward with a hotfix. Restoring is the safer path when the data loss is small and recent. For MariaDB, mariabackup full and incremental restores covers the procedure.

Warning: Restoring a database overwrites current data. Confirm the backup timestamp and take a fresh dump of the current state before you restore anything.

Check it works

Verify from the server and from outside it.

Bash
docker compose ps
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/health
docker compose logs --tail=50 app

A 200 from the health endpoint and no new errors in the logs means the rollback took. Then load the app through your reverse proxy or public URL and run the specific action that broke — a login, a checkout, an upload — rather than trusting the health check alone.

Finally, confirm the version the app reports matches the tag you pinned:

Bash
docker compose exec app printenv APP_VERSION

If your image does not expose a version variable, add one at build time. It saves guesswork during incidents.

Troubleshooting

  • manifest unknown on pull — the tag does not exist in the registry. Check the spelling, and confirm the CI job that was supposed to push it actually succeeded. Tags are case-sensitive.
  • Container restarts in a loop after rollback — the old image is running against a newer schema or a changed .env. Compare the environment variables the release added with your current .env, and see Manage secrets and environment variables in Docker Compose.
  • up -d says the container is up to date — Compose matched the container to the image ID and skipped the recreate. Force it with docker compose up -d --force-recreate app.
  • Old tag was deleted from the registry — you cannot pull it. Rebuild from the tagged commit in Git, or restore the image from a host that still has it with docker save and docker load.
  • Rollback worked but the site is still broken — the problem is outside the container: proxy config, DNS, or a dependency such as Redis or the database. Check container networking in bridge vs host mode.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.