How to Backup Docker Volumes on a VPS: Practical Persistence Guide

Learn how to back up Docker volumes on a VPS without stopping containers. Strategies for databases, configs, and persistent data in production.

Docker volumes are the point where the container abstraction meets the reality of the disk. A container is disposable — you rebuild it from the image in minutes. A volume is where the real state lives: databases, user uploads, persistent settings. Losing a volume means losing production data.

Despite this, volume backups are the most neglected part of the Docker stack. Most VPSes running containerized applications have no backup routine in place, only the false security that “everything is in a container”. When the disk fails or someone runs docker volume prune distractedly, the loss is final.

This tutorial walks through practical strategies for backing up Docker volumes on a Linux VPS, covering static data, databases, and cron-based automation. Estimated execution time: 25-40 minutes to configure the first backup and validate the restore.

Prerequisites

What you need

A VPS running Ubuntu 22.04 LTS or Debian 12 with Docker Engine 24+ installed, at least one Docker volume in use by a container, and sudo access. Free space equivalent to 1.5x the original volume size (for temporary compression) and, ideally, a remote destination for the backups (S3-compatible, another server, or external storage).

Confirm the Docker version and list your volumes before starting:

docker --version
docker volume ls
Docker Engine 24.x or higher
System Ubuntu 22.04+ / Debian 12+
Free space 1.5x volume size
Remote target S3, rsync or block storage

Identifying affected volumes and containers

Before backing up, map which containers use each volume. A blind backup that ignores dependencies causes inconsistency — you may capture the database file in the middle of a transaction if the container is writing.

01

List all named volumes and the containers that use them:

docker volume ls --format '{{.Name}}'

For each volume, identify the consuming containers:

docker ps -a --filter volume=volume_name --format '{{.Names}}: {{.Image}}'

Anonymous volumes (with long hashes) are usually disposable — they come from images that declare VOLUME in the Dockerfile without an explicit name. Focus on the named volumes.

02

Inspect the physical mount point of each volume:

docker volume inspect volume_name

The Mountpoint field shows where Docker stores the data — typically /var/lib/docker/volumes/<name>/_data. That path is what you need for a direct tar-based backup.

03

Check the real size of each volume:

sudo du -sh /var/lib/docker/volumes/volume_name/_data

Use that value to size the destination space and estimate backup time. Volumes above 20 GB deserve an incremental strategy.

Backing up static data with tar

Static data — user uploads, configs, assets — can be copied while the container is running. The risk of inconsistency is low because atomic writes (mv, rename) preserve the previous state until the operation completes.

The standard strategy uses a temporary Alpine container with the volume mounted in read-only mode, creating the tarball in a directory on the host.

04

Create a directory for the backups on the host:

sudo mkdir -p /var/backups/docker-volumes
sudo chmod 700 /var/backups/docker-volumes

Permission 700 ensures only root can read the files — backups often contain sensitive data.

05

Run the backup of a volume with a disposable Alpine container:

docker run --rm \
  -v volume_name:/source:ro \
  -v /var/backups/docker-volumes:/backup \
  alpine \
  tar czf /backup/volume-$(date +%Y%m%d-%H%M%S).tar.gz -C /source .

The :ro flag mounts the volume read-only — it prevents the temporary container from accidentally modifying data. The -C /source . avoids including the /source path in the tarball structure, making restoration easier.

Large tarballs consume CPU

gzip compression is single-threaded — for volumes above 10 GB, consider pigz (parallel gzip) or zstd, which scale with available cores. On a 2 vCPU VPS, plain gzip is acceptable; on 8+ vCPUs, pigz cuts the time in half.

Consistent database backups

Databases are the most delicate case. Copying the binary files (MySQL datadir, PostgreSQL base/) while the engine is running yields a corrupted backup — writes in flight, unflushed WAL, inconsistent indexes. The solution is to use the engine’s native dump tool.

06

For MySQL/MariaDB, run mysqldump directly from the container:

docker exec mysql_container_name \
  sh -c 'mysqldump --single-transaction --quick \
    -u root -p"$MYSQL_ROOT_PASSWORD" --all-databases' \
  | gzip > /var/backups/docker-volumes/mysql-$(date +%Y%m%d-%H%M%S).sql.gz

The --single-transaction flag creates a consistent snapshot on InnoDB without blocking writes — it works because the dump runs inside an isolated transaction. For MyISAM tables, add --lock-tables (but consider migrating to InnoDB).

07

For PostgreSQL, use pg_dumpall or pg_dump per database:

docker exec pg_container_name \
  pg_dumpall -U postgres \
  | gzip > /var/backups/docker-volumes/postgres-$(date +%Y%m%d-%H%M%S).sql.gz

pg_dumpall includes roles and cluster settings — required for a complete restore. For large individual databases, pg_dump -Fc (custom format) enables parallel restore with pg_restore -j.

08

For MongoDB, use mongodump to a temporary directory inside the container and tar afterward:

docker exec mongo_container_name \
  mongodump --archive --gzip \
  > /var/backups/docker-volumes/mongo-$(date +%Y%m%d-%H%M%S).archive.gz

The archive format concatenates all databases into a single stream — easier to transfer than the classic mongodump directory.

Password in environment variable

The examples above assume the password is in $MYSQL_ROOT_PASSWORD inside the container. Never pass the password as -p$password directly in the command — it shows up in ps aux and shell logs. Always use an environment variable or a mounted .my.cnf file.

Automation with cron

Manual backups don’t scale — you forget, or only do it when you remember (usually when you need to restore). Cron solves this. The recipe below creates a script wrapping the routine and schedules daily execution.

09

Create a script at /usr/local/bin/docker-backup.sh:

#!/bin/bash
set -euo pipefail

BACKUP_DIR=/var/backups/docker-volumes
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
RETENTION_DAYS=14

# Backup MySQL
docker exec mysql_container \
  sh -c 'mysqldump --single-transaction --quick \
    -u root -p"$MYSQL_ROOT_PASSWORD" --all-databases' \
  | gzip > "$BACKUP_DIR/mysql-$TIMESTAMP.sql.gz"

# Backup static volume
docker run --rm \
  -v uploads_volume:/source:ro \
  -v "$BACKUP_DIR":/backup \
  alpine \
  tar czf "/backup/uploads-$TIMESTAMP.tar.gz" -C /source .

# Clean up old backups
find "$BACKUP_DIR" -name "*.gz" -mtime +$RETENTION_DAYS -delete

The set -euo pipefail aborts on any error — a partial backup is worse than no backup because it gives a false sense of security.

10

Make the script executable and schedule it in root’s cron:

sudo chmod 700 /usr/local/bin/docker-backup.sh
sudo crontab -e

Add the line to run every day at 3 AM:

0 3 * * * /usr/local/bin/docker-backup.sh >> /var/log/docker-backup.log 2>&1

Redirecting stderr and stdout to the log enables diagnosis when something fails — silent cron is the worst-case scenario.

Off-site is mandatory

A backup on the same disk as the server protects against human error (docker volume rm), not against hardware failure. Add an upload step to S3-compatible storage (mc, rclone) or rsync to another server at the end of the script. Without off-site, your recovery plan has a single point of failure.

Verification and restore

A backup that has never been tested isn’t a backup — it’s a file. Validate the restore periodically, ideally on a staging VPS.

To restore a static-data volume:

# Create empty volume
docker volume create uploads_restored

# Restore content
docker run --rm \
  -v uploads_restored:/target \
  -v /var/backups/docker-volumes:/backup \
  alpine \
  sh -c 'cd /target && tar xzf /backup/uploads-20260529-030000.tar.gz'

To restore MySQL:

gunzip -c /var/backups/docker-volumes/mysql-20260529-030000.sql.gz \
  | docker exec -i mysql_container mysql -u root -p"$MYSQL_ROOT_PASSWORD"

After restoring, validate integrity — for MySQL, run CHECK TABLE on the main tables; for application data, start a container pointing to the restored volume and verify functionality.

Troubleshooting

”Cannot create container: volume in use”

Appears when you try to delete or rename a volume mounted on an active container. Stop the container first with docker stop before manipulating the volume. Backing up with :ro mounts the volume in parallel without conflict.

Corrupted tarball during restore

Symptom: tar: Unexpected EOF in archive. The common cause is the disk filling up during backup — the file was truncated. Always monitor free space (df -h) before running the backup, and add a verification step to the script: tar tzf file.tar.gz > /dev/null && echo OK.

Slow backup even on SSD

Check whether the backup is being written to the same disk as the original volume — I/O contention cuts throughput in half. Backing up to a separate disk (additional block storage or network) speeds things up significantly. On a Hostini VPS, you can add a separate block volume via the panel to dedicate to the backup area.

Next steps

With backups configured and validated, consider the next levels of maturity: implementing incremental backups with restic or borg for large volumes (cuts time and space by ~80%), configuring alerts in the script to notify on failure via webhook or email, and separating backups by window (daily 14 days, weekly 8 weeks, monthly 12 months) to cover different recovery scenarios.

If you’re putting this into production, a Hostini VPS comes with NVMe SSD and an optional dedicated block volume for backups, which eliminates I/O contention between live data and backup copies. For critical workloads, also consider synchronous replication to another region — backups solve data loss, replication solves downtime.

Frequently asked questions

Can I back up a Docker volume while the container is running?

Yes, for static data (uploads, configs) tarring the volume directly works with the container active. For databases (MySQL, PostgreSQL, MongoDB), you need to use the native dump tool (mysqldump, pg_dump) or briefly stop the container — copying binary files held open by the database engine produces a corrupted backup.

What's the difference between backing up a volume and backing up a container?

A container is ephemeral — the image plus the Dockerfile rebuild it. A volume is the persistent state that needs to be preserved. Backing up a container rarely makes sense; backing up the volume is what matters to recover real data after a failure.

Where should I store backups to keep them safe?

Never on the same disk as the server — if the disk fails, you lose everything together. Use remote storage (S3-compatible, rsync to another server, or external block storage). The 3-2-1 rule applies here: 3 copies, 2 different media, 1 off-site.

How long does a full 50 GB backup take?

It depends on compression and the disk. With tar + gzip on NVMe SSD, expect 8-15 minutes for 50 GB; with plain tar (no compression) it drops to 3-5 minutes but the file is larger. For large volumes, consider incremental backups with restic or borg.

How do I restore a volume from the tarball?

Stop the container, create the empty volume with docker volume create, mount it on a temporary container (alpine) with the tarball accessible, run tar xzf inside the mount point, and start the original container. The exact command is in the verification section.

Do I need to stop the entire Docker daemon to back up?

No. Stop only the containers that use the specific volumes. The daemon must keep running so you can run docker commands during the backup. Stopping the entire daemon affects unrelated containers unnecessarily.

Topics:
Next steps Ryzen cloud with NVMe storage and always-on DDoS protection.Go live on a Hostini VPS →
Was this tutorial helpful?
Chat on WhatsApp