Containers aren’t updated in place like a regular server. You pull a newer image and recreate the container from it. Your data survives because it lives in volumes or bind mounts, not inside the container. Here’s the right way to do it with plain docker run, and with Docker Compose.

Why not apt upgrade inside the container?

Changes made inside a running container are lost when it’s recreated, aren’t reproducible, and drift from the image. The image is the unit of deployment: update the image (or its tag), then replace the container.

Before updating

  • Check where data lives: docker inspect -f '{{json .Mounts}}' <container> | jq. Anything not in a volume or bind mount will be lost on recreate.
  • Read the release notes, especially for major versions (databases often need migration steps).
  • Back up volumes or database dumps. See Docker volume backups.
cd /opt/app
docker compose pull                # download newer images for the tags in compose.yaml
docker compose up -d               # recreate containers whose image changed
docker compose ps
docker image prune -f              # remove old, now-unused images

To move to a new version, change the tag in compose.yaml (image: nextcloud:30 → nextcloud:31), then pull and up -d. Keep the file in Git, so you know exactly what’s running. See Docker Compose in production.

Update a single service:

docker compose pull app && docker compose up -d app

Plain docker run

You need the original run options (ports, volumes, env, network, restart policy):

docker inspect nginx-web     # note the options, or keep them in a script
docker pull nginx:1.27
docker stop nginx-web && docker rename nginx-web nginx-web-old
docker run -d --name nginx-web --restart unless-stopped \
  -p 80:80 -v /srv/www:/usr/share/nginx/html:ro nginx:1.27
# test, then:
docker rm nginx-web-old

Renaming instead of removing gives you a quick rollback. Long docker run commands are a good reason to switch to Compose. See Docker Compose tutorial.

Rolling back

  • Compose: change the tag back to the previous version, and run docker compose up -d.
  • docker run: stop the new container, start the old one (docker start nginx-web-old).
  • Data migrations: if the new version migrated the database schema, rolling back the image may not be enough. Restore the backup taken before the update.

This is why pinned tags (not latest) matter: you always know the previous version. See Docker image tags best practices.

Minimising downtime

up -d stops the old container and starts the new one, which is usually a few seconds of downtime. For zero-downtime:

  • run two replicas behind a reverse proxy, and update them one at a time;
  • or use tools like docker-rollout, or Docker Swarm (docker service update with rolling updates). See Docker Compose scale and Docker Swarm.

Automating updates

  • Watchtower checks for new images, and recreates containers automatically. Good for home labs. Risky for production, since updates happen without review. See Watchtower.
  • Notification-only tools (Diun, What’s Up Docker) tell you when updates exist, so you can update deliberately.
  • CI/CD: build and tag images in CI, update the tag in Git, and deploy with docker compose up -d over SSH. See GitHub Actions deploy to VPS.

Quick reference

Task Command
Update all (Compose) docker compose pull && docker compose up -d
One service docker compose pull app && docker compose up -d app
New version Change the tag in compose.yaml, then pull + up
Clean old images docker image prune -f
Check mounts first docker inspect -f '{{json .Mounts}}' <c>

Related: updating self-hosted apps and Docker restart policy.

  • Docker
  • DevOps
  • Updates