Explore our specialized services, tailored solutions, and industry expertise to elevate your digital presence. From custom WordPress development to seamless integrations, we build high-performing websites that deliver impact.
A recreated container is clean by default: the writable layer, which contains anything stored only there, does not survive the process. Persisting WordPress data means the database and the wp-content directory survive a restart or a recreation instead of the container’s own writable layer.
WordPress data consists of two stores: the database, which stores posts, settings, and user accounts, and wp-content, which contains uploads, themes, and plugins. Neither store belongs to that writable layer. Docker volumes and bind mounts persist that data by attaching storage that is separate from the container itself.
A named volume or a bind mount attaches the database and wp-content to a running container, first with docker run and then inside Docker Compose, depending on which mechanism applies to each store. That same choice also configures permissions on the mounted paths and the backup strategy for what they store. A Docker volume is the Docker-managed form of that storage, and its own two types govern how reliably each store can be addressed afterward.
What Is a Docker Volume?
A Docker volume is a Docker-managed directory on the host that stores a container’s data outside the container’s own filesystem. The volume decouples that data from any single container’s lifecycle, so the data on it survives a restart or a full recreation, unlike the container’s own writable layer, which does not.
WordPress data, the database and wp-content together, belongs on that kind of storage because neither store can afford to disappear when a container is recreated. The volume stores the actual content; the container only mounts it at a path inside its own filesystem. A recreated container, after an image update for instance, mounts the same volume again, and the data it contains persists exactly as it was before.
Containers and images are the stage that comes before persistence. Docker for WordPress developers covers that earlier stage.
A Docker volume represents one of two types, distinguished by how each is addressed.
Named and Anonymous Docker Volumes
A named volume is a Docker volume addressed by an explicit, human-chosen name rather than an identifier Docker assigns on its own. An anonymous volume, by contrast, is addressed by an auto-generated id. That id is a long string created the moment the volume is declared, with no name attached to it for later reference.
The distinction matters for WordPress persistence. A database store on a named volume (db_data, for example) stays predictable across every restart and every recreation: the same name maps to the same store every time, which makes that store simple to address in a backup command or an inspection later on. An anonymous volume offers no such guarantee; its auto-generated id changes with context and gives nothing stable to address.
The WordPress database favors a named volume over an anonymous one for exactly that reason. The bind mount, which attaches a host path directly instead, represents a different mechanism entirely.
What Is a Docker Bind Mount?
A Docker bind mount maps an existing host directory directly into a container, so the host filesystem, not the Docker Engine, controls the contents at that path. The bind mount takes its source from a directory the developer already controls on the host; a Docker-managed volume, by contrast, decouples storage from any single host location and stores the data under Docker’s own control instead. Nothing about a bind mount is Docker-specific. It exposes a host path at a container path, and whatever already exists at the source shows up inside the container.
That direct exposure creates one caveat worth stating plainly. A bind mount hides a container’s existing files at the mount point whenever it mounts over a path the image already populated; the host directory’s contents replace whatever was there, for as long as the mount stays active. An empty host directory bind-mounted over a populated image path produces an empty directory inside the container, not the image’s original files.
Docker exposes two equivalent ways to bind-mount a directory at docker run:
# short syntax
docker run -v /host/wp-content:/var/www/html/wp-content wordpress:latest
# long syntax
docker run --mount type=bind,source=/host/wp-content,target=/var/www/html/wp-content wordpress:latest
Both lines bind-mount the same host directory to the same container path. --mount spells out type, source, and target as named fields; -v compresses the same three pieces into one colon-separated string. Host-path control versus Docker-managed control is the exact trade-off the WordPress database and wp-content each resolve, in opposite directions.
How to Persist the WordPress Database and wp-content
The docker bind mount vs volume decision for WordPress persistence settles which mechanism keeps the WordPress database and wp-content intact across container restarts. Neither store survives on its own: without one of these two mechanisms, the database files and the wp-content directory would sit only in the container’s ephemeral writable layer, and recreating the container would erase both.
The MariaDB or MySQL database that backs a WordPress installation persists best on a Docker named volume. A named volume decouples the database’s files from any single host directory and from the container’s writable layer alike, and Docker keeps the data intact across restarts, recreations, and even container removal, as long as the volume itself is not deleted. wp-content covers the uploads, themes, and plugins WordPress writes to at runtime, and it persists just as reliably on a bind mount, with the added benefit that a bind mount keeps those files directly reachable and editable from the host filesystem, something a named volume does not offer without an extra step.
Database
wp-content
Mechanism
Named volume
Bind mount
Persistence
Survives container restart and recreation
Survives container restart and recreation
Editability
Not directly editable from the host filesystem
Directly editable from the host filesystem
Framed the other way, the docker volume vs bind mount decision comes down to which store needs host-level editability and which does not. Each store takes its own mechanism: a named volume for the database, a bind mount for wp-content.
A Named Volume for the WordPress Database
A named volume called db_data is the Docker-managed store for the containerized MariaDB or MySQL database that backs WordPress. The database data directory, /var/lib/mysql inside the container, persists across restarts and survives container recreation because the db_data volume, not the container’s writable layer, stores its files. The volume exists independently of any single container instance and decouples the database from that container’s lifecycle entirely.
The volumes top-level key declares db_data as a named volume, and the db service mounts it to /var/lib/mysql, the exact path where MariaDB and MySQL both write their data files. The db_data volume is declared and mounted inside the compose file the WordPress Docker Compose setup builds, alongside the WordPress service it also defines.
Bind-mounting the database data directory instead of using a named volume is not recommended. MySQL and MariaDB expect specific file ownership and locking behavior on /var/lib/mysql, and a bind mount hands that behavior over to whatever permissions and filesystem features the host directory happens to carry, risking permission conflicts and data corruption that a named volume avoids by keeping the storage under Docker’s own control. wp-content asks for the opposite trade-off, and a bind mount is what serves it.
A Bind Mount for wp-content
A bind mount for wp-content maps a host directory, the one that contains uploads, themes, and plugins, onto the container path /var/www/html/wp-content. The bind mount attaches that host directory to the container path directly, with no copy step between the two; the file on the host and the file inside the container are the same object.
wp-content persists across a container restart and stays host-editable at the same time. A named volume, used for the database, decouples storage from any host path; wp-content instead stays attached to a real host directory reachable outside Docker.
- ./wp-content:/var/www/html/wp-content
Declaring that mapping is not the same as attaching it. Docker run applies it first; Docker Compose applies the same mapping afterward.
How to Mount a Docker Volume with docker run
Mounting a Docker volume with docker run attaches a named volume or a host path to a container path at container start, through flags on the run command rather than a separate configuration file. Docker run mounts a volume in one of two forms: the short -v flag or the longer –mount flag, and both apply the same host-to-container relationship.
The docker volume mount syntax for -v is host:container order: the left side is the volume or host path, the right side is the container path it maps to. Its –mount form represents the same relationship across three named parts instead of one: type, source, and target.
docker run -v db_data:/var/lib/mysql mariadb:latest
docker run --mount type=bind,source="$(pwd)/wp-content",target=/var/www/html/wp-content wordpress:latest
The first command mounts db_data (the named volume that stores the MariaDB/MySQL data directory), so the database persists across a container restart. Right after it, the second binds the host’s wp-content directory into the WordPress container, the same relationship declared earlier, now applied at runtime instead of inside a Compose file.
Docker Compose represents both commands inside a single declarative file instead of two run-time flags.
How to Mount a Docker Volume in Docker Compose
A Docker volume in Docker Compose is declared under the top-level volumes key and applied inside a service’s own volumes list. The top-level key creates the named volume Docker stores; the per-service list maps that volume, or a bind-mount host path, onto a container path.
Compose volume syntax represents the same short form as docker run: source:target, with a longer named-parameter form available too.
That declaration creates db_data once for the db service; the wordpress service instead binds a host path directly, and a bind mount is otherwise absent from the top-level volumes key. The named volume and the bind mount each apply that split a level further, one for the database, one for wp-content.
A Named Volume in Docker Compose
A named volume in Docker Compose is a name declared under the top-level volumes: key and attached to a service through that same name, rather than through a host path. For the WordPress database, the named volume holds the role of db_data, a label Compose resolves to a storage location that decouples the data from any directory addressed directly by the host. Docker creates db_data the first time the stack starts, then reuses it on every subsequent start; a restart or a recreation of the container does not touch the data inside.
The declaration sits in two places. The top-level volumes: block registers the name, and the database service’s volumes: list maps that name to the data directory the database engine expects.
Once mapped, db_data mounts at /var/lib/mysql inside the database container: the path MariaDB or MySQL writes to for every table, index, and log it keeps. Docker stores the volume’s contents behind that mount; the database service reads and writes through the path without tracking db_data’s physical location on the host. The Compose name is what makes that indirection repeatable: Docker resolves the top-level volumes: entry to the stored data, so a fresh container declared from the same file reattaches the identical db_data store without the operator ever naming a location by hand.
A bind mount handles the sibling case for wp-content, where Compose maps a host path instead of a Docker-declared name.
A Bind Mount in Docker Compose
A bind mount in Docker Compose is a host-path-to-container mapping declared directly inside a service’s volumes: list, with no top-level entry required, unlike a named volume, nothing gets registered separately before use. For wp-content, the WordPress service’s volumes: line points at the folder holding uploads, themes, and plugins, and connects it straight to a directory on the host machine.
This docker compose bind mount example for wp-content shows the same docker-compose bind mount pattern that applies to any host-editable path in the stack.
The short form binds ./wp-content, a folder next to the Compose file, to /var/www/html/wp-content inside the container, the path WordPress serves uploads, active themes, and plugins from. Because the mapping runs through an ordinary filesystem path rather than a Docker-declared name, the host directory stays editable outside the container: a plugin dropped into ./wp-content from the host appears inside the running WordPress site without any copy step.
The host path functions as the source of truth: WordPress reads and writes files stored, physically, on the host disk, and that content survives every restart of the container for the same reason a named volume survives one, just through a path the developer controls directly instead of one Docker abstracts away.
That host-editable arrangement is also what raises the ownership question that follows.
Docker Volume Permissions for wp-content
Docker volume permissions for wp-content, sometimes framed as docker compose volume permissions, govern which system user is allowed to write into the bind-mounted directory once the WordPress container is running. A bind mount carries the ownership already present on the host directory into the container unchanged, the exact mechanism docker bind mount permissions addresses, and no reassignment happens automatically the way it does with a named volume, where Docker initializes ownership correctly on first use. wp-content, mapped this way, keeps whatever uid and gid the host directory already had before the container touched it.
That inherited ownership is frequently wrong for the container’s purposes. The web-server process inside the WordPress container, typically running as www-data, needs write access to wp-content for uploads, plugin additions, and theme edits, but the host directory was often created by a different user, at a different numeric uid and gid. Docker does not translate one into the other; a mismatch between the host uid/gid and the container’s uid/gid leaves the web-server user locked out of a directory it can otherwise see.
The fix sits squarely inside docker bind mount permissions uid gid handling: match ownership on the host, not permission bits, to the numeric uid and gid the container’s web-server user runs as. The Apache and PHP-FPM images commonly used for WordPress run www-data at uid 33 and gid 33, so applying that ownership on the host resolves the mismatch directly.
chown -R 33:33 ./wp-content
Applying numeric ownership this way affects only the bind mount; a named volume does not need it, because Docker already initializes a named volume with ownership the container process can write to from the first run. The distinction matters for wp-content specifically, since it is the bind-mounted directory in this stack, the database’s db_data volume never faces the same mismatch.
A read-only mount for WordPress configuration takes a different approach to the same directory-level control.
How to Mount WordPress Configuration as a Read-Only Volume
A read-only volume mount attaches WordPress configuration (wp-config.php, or a configuration directory) to a container while denying that container permission to alter its contents. Docker applies the restriction through a :ro suffix on a bind mount, or through Compose’s read_only: true directive declared against the same target. The container reads the configuration at startup and cannot write to it afterward; the read-only volume protects WordPress configuration from container-side writes for the life of that container.
Configuration behaves differently from the WordPress database and wp-content. Those two stores stay read-write because plugins write files there, uploads change, and MariaDB writes transaction data continuously. WordPress configuration runs the opposite way: wp-config.php holds database credentials, table prefixes, and debug flags that stay fixed once the container starts. Mounting it read-only removes an entire class of runtime tampering, a compromised process inside the container cannot rewrite its own database credentials or quietly disable debug logging.
Compose expresses the restriction inside the volumes key of the WordPress service:
The trailing :ro is the short form of the read-only restriction: it marks the mount read-only inside the container’s mount namespace, while the host file keeps its ordinary permissions, so editing it from outside the container still works normally. The long form expresses the same restriction as a read_only: true field inside an individual mount entry, spelling out type, source, target, and read_only rather than appending a suffix:
That read_only: true field belongs under the one mount entry, not at the service level, where a service-level read_only: true would instead make the container’s entire filesystem read-only. Either form of the per-mount restriction protects the same target, WordPress configuration, and never the database volume or wp-content.
Persistence and read-only protection answer two different questions. Retaining volumes across container restarts keeps data from disappearing. Mounting configuration read-only keeps one specific file from changing at all, survival against permission. With persistence and protection both accounted for, the volumes holding the WordPress database and wp-content still need a way to leave the host entirely: backed up to a separate location and restored when a volume is lost or moved.
How to Back Up the WordPress Docker Volumes
Backing up WordPress data means archiving each store through the mechanism that actually holds it. The database sits on the db_data named volume, so a disposable helper container reaches it. wp-content sits on a bind mount, so its files are already on the host and a plain archive of that directory captures them. Each store produces a compressed .tar.gz archive that survives on the host after the command finishes.
db_data holds the WordPress database(MariaDB or MySQL data files, depending on the image in use) behind a Docker-managed volume that no host path addresses directly. A helper container mounts that volume at one path and a host directory at another, then archives the volume’s contents:
# database (named volume): archive through a disposable helper container
docker run --rm -v db_data:/data -v "$(pwd)":/backup alpine
tar czf /backup/db_data-backup.tar.gz -C /data .
# restore the database archive into a fresh named volume
docker run --rm -v db_data:/data -v "$(pwd)":/backup alpine
tar xzf /backup/db_data-backup.tar.gz -C /data
wp-content needs no helper container, because a bind mount keeps its files on the host already. The ./wp-content directory that the WordPress service mounts is an ordinary host folder, so tar archives and restores it in place:
# wp-content (bind mount): archive the host directory directly
tar czf wp-content-backup.tar.gz -C ./wp-content .
# restore wp-content by extracting back into the host directory
tar xzf wp-content-backup.tar.gz -C ./wp-content
The database commands each start a throwaway Alpine container, mount db_data at /data and the current host directory at /backup, archive or extract, then exit; the --rm flag removes the container immediately, leaving only the tarball or the restored files behind. One condition governs that database archive: because it copies the InnoDB data files straight from the filesystem, the database container should be stopped, or a consistent snapshot taken, before the archive runs, so the tar records a settled state rather than a half-written transaction.
A logical export of the database is the consistency-safe alternative when the container has to stay live. Restoring the database reverses the backup: a freshly recreated, empty db_data volume receives the same /data mount, and tar xzf extracts the archive back into it before the database container starts against that volume again. The wp-content commands skip the container entirely, since the source and destination are the same host directory the bind mount already exposes.
Archive size scales with what each store holds. A db_data archive for a modest WordPress database typically lands in the low tens of megabytes (MB). A wp-content archive carrying several years of uploaded media can reach several hundred megabytes (MB) or cross into gigabytes (GB). The two archives compress unevenly. A db_data archive shrinks well, because the raw MariaDB or MySQL data directory it captures holds repetitive, compressible structure. A wp-content archive barely shrinks, since a media-heavy uploads directory is mostly JPEG, PNG, WebP, and MP4 files that already carry their own compression and gain little further from gzip.
A backup workflow like this one treats the tar archive as the actual unit of recovery, not the helper container that creates the database copy: the container is disposable, while the archive is what a restoration depends on.
This same procedure is the backup authority referenced wherever a WordPress installation moves toward production, including when a deployment plan calls for deploy WordPress in Docker to production, a backup taken immediately before cutover gives a fallback if that move fails. Once both the database volume and the wp-content directory carry a tested backup, the remaining question is simpler: where each store actually lives on the host filesystem.
The Docker Volume Storage Location on the Host
The Docker volume storage location is the path on the host filesystem where the Docker engine keeps a named volume’s actual data. For a Linux host running the standard Docker engine, that path sits under /var/lib/docker/volumes, with one directory per named volume and a _data subdirectory inside it holding the volume’s contents. The db_data volume, for instance, stores its files at /var/lib/docker/volumes/db_data/_data, the same bytes the database container sees when it mounts that volume at /var/lib/mysql.
docker volume inspect reveals the exact Mountpoint without guesswork:
The output maps the volume name directly to its Mountpoint on the host, confirming the path before anyone goes looking for it manually.
That managed path applies to named volumes only. wp-content, mapped as a bind mount, never lands under /var/lib/docker/volumes at all: its files sit at the ./wp-content host directory the mount points to, reachable and editable there directly. Docker owns the /var/lib/docker/volumes tree, and root ownership plus internal bookkeeping make it the wrong surface for day-to-day editing, so the bind mount, pointed at an ordinary host directory, stays the intended route to wp-content files. Only db_data and any other named volume live behind the managed path; the bind-mounted directory has a plain host path instead.
Knowing where each store sits on disk still leaves an operational question open: whether a volume can be attached to a container that is already running, rather than only at container creation.
Can a Docker Volume Be Mounted to a Running Container?
A Docker volume cannot be mounted to a container that is already running. The mount specification is fixed at creation, assigned once when the container starts from its image, and stays fixed for as long as that container keeps running. A container does not accept new mounts while active; only recreation changes what is mounted.
The container is re-run with the volume added, or redeclared in the Compose specification with the volume mapped to its path, and a new container replaces the old one. Once the replacement starts, the volume reattaches at the assigned path, and whatever data already sits there, the WordPress database or the wp-content directory, becomes available inside the new container without being moved, copied, or restored by hand.
Recreation works this way because the container and its data are not the same object. The container runs the WordPress process; the Docker volume stores what that process writes, independent of any single container’s lifecycle. When a container is swapped for a fresh one but the volume mapping is kept, the stored state carries over intact: database rows, uploads, themes, and plugin files remain exactly as they were before the previous container stopped. Recreating a container is therefore not destructive for a WordPress installation whose database and wp-content directory persist on named volumes and bind mounts: the container is replaceable, the persisted data is not.