Dash0 Raises $110M Series B at $1B Valuation

  • 11 min read

How to Mount a Host Directory in a Docker Container

Mounting a host directory into a container is how you get files to cross the boundary between your machine and a container's isolated filesystem: source code during development, config files the app reads at startup, or a directory where the container writes data you want to keep after it exits. Docker calls this a bind mount, and the idea is straightforward. The host path and the container path point at the same files on disk, so a write on either side is instantly visible on the other.

Where it gets fiddly is the syntax, which differs between docker run and Compose, plus a couple of path and permission traps that catch almost everyone the first time around.

Mount a directory with docker run

The quickest way to bind mount a directory is the -v (or --volume) flag. It takes a host path and a container path separated by a colon:

bash
1
docker run -v /home/alice/project:/app node:lts

This mounts /home/alice/project from the host at /app inside the container. Edit a file on the host and the container sees the change immediately, which is exactly what you want when you're iterating on code with a file watcher running inside the container. Mounts are fixed when the container is created, so they go on docker run (or docker create); you can't attach a new mount to an already-running container without recreating it.

The host path must be absolute. This is the single most common mistake with -v: if you pass something that looks like a relative path or a bare name, Docker doesn't bind mount it. Instead it creates a named volume with that name (more on the difference below). To mount the current directory, expand it explicitly:

bash
1
docker run -v "$(pwd)":/app node:lts

To confirm a mount is actually in place, inspect the container's mounts:

bash
1
docker inspect --format '{{json .Mounts}}' <container> | jq
json
12345678910
[
{
"Type": "bind",
"Source": "/home/alice/project",
"Destination": "/app",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
]

Type tells you it's a bind mount rather than a managed volume, Source and Destination show the host and container paths, and RW: true confirms the mount is writable.

Use --mount for clearer, safer syntax

Docker recommends --mount over -v for new work. It's more verbose, but it states exactly what you're doing with explicit keys, and it fails loudly when the source is wrong instead of silently doing something unexpected:

bash
123
docker run \
--mount type=bind,src=/home/alice/project,dst=/app \
node:lts

The type=bind key tells Docker this is a host directory rather than a managed volume. src (also spelled source) is the host path, and dst (also target or destination) is the path inside the container. The two commands above are equivalent, with one important behavioral difference: if /home/alice/project does not exist on the host, --mount returns an error and refuses to start the container, while -v silently creates an empty directory at that path. The --mount behavior is almost always what you want, because a typo in the source path with -v gives you an empty mount and a confusing debugging session.

When the source is missing, the error names the path it couldn't find:

text
1
docker: Error response from daemon: invalid mount config for type "bind": bind source path does not exist: /home/alice/project

The full set of options for both flags is documented in the Docker bind mounts reference.

Mount a directory read-only

If the container only needs to read the files, mount the directory read-only so a misbehaving or compromised process can't modify your host files. With -v, append :ro:

bash
1
docker run -v /etc/myapp/config:/app/config:ro node:lts

With --mount, add the readonly option (ro works too):

bash
123
docker run \
--mount type=bind,src=/etc/myapp/config,dst=/app/config,readonly \
node:lts

Read-only is the right default for configuration files and any reference data the container shouldn't touch. Reserve writable mounts for directories the application genuinely needs to write to.

Named volumes vs host directories

A bind mount points at a specific path you manage on the host. A named volume points at storage Docker creates and manages for you under /var/lib/docker/volumes/. The syntax difference with -v is whether the source starts with a path separator:

bash
12345
# Bind mount: source is a host path
docker run -v /home/alice/data:/app/data postgres:17
# Named volume: source is a name, no leading slash
docker run -v pgdata:/var/lib/postgresql/data postgres:17

The named volume pgdata is created on first use and reused on subsequent runs, and unlike the container's own writable layer it survives a docker rm unless you delete it explicitly. Reach for a named volume when you want persistent storage but don't care where it physically lives, which is the typical case for database data directories. Reach for a bind mount when the location on the host matters, such as mounting your working directory for live code reloads or pointing at a config file that other tools on the host also read. With --mount, switch types by changing type=bind to type=volume.

Mounting directories in Docker Compose

In Compose, mounts go under each service's volumes key. The short syntax mirrors -v, using HOST:CONTAINER[:OPTIONS]:

yaml
123456
services:
app:
image: node:lts
volumes:
- ./project:/app
- ./config:/app/config:ro

Compose resolves relative paths like ./project against the location of the Compose file, so the relative-path trap from docker run doesn't apply here. The long syntax is the equivalent of --mount and reads more clearly for anything beyond a basic mount:

yaml
1234567891011
services:
app:
image: node:lts
volumes:
- type: bind
source: ./project
target: /app
- type: bind
source: ./config
target: /app/config
read_only: true

Named volumes work the same way, with a top-level volumes key to declare them:

yaml
12345678
services:
db:
image: postgres:17
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:

Common pitfalls

A bind mount obscures whatever already exists at the target path inside the container. If the image ships files at /app and you bind mount a host directory there, the container sees your host directory and the image's original /app contents are hidden for the life of the mount. This is the same as plugging a USB drive into a mount point on Linux: the underlying directory is still there, it's just covered. The fix is to mount at a path that doesn't collide, or to accept that your host directory fully replaces that location.

The permission errors are the ones that cost the most time. The process inside the container runs as some user ID, often root (UID 0) or an app-specific UID baked into the image, and that UID may not match the ownership of the files on your host. The result is a permission denied that looks like a Docker problem but is really a plain Unix ownership mismatch. Because this trips up nearly everyone working with shared directories, the UID and group ID (GID) mechanics are worth getting straight. Our guide to managing permissions for Docker shared volumes walks through the --user flag, file ownership alignment, and the SELinux :z and :Z label options that bind mounts need on RHEL-family hosts.

One more thing to watch on Docker Desktop for Mac and Windows: bind mounts cross a virtualization boundary, so large or write-heavy mounts can be noticeably slower than on native Linux. For directories with thousands of files, such as node_modules, a named volume or Docker Desktop's synchronized file shares will perform better than a plain bind mount.

Final thoughts

That covers the options for sharing a host directory with a container. Use -v for quick one-liners and --mount when you want the configuration to be explicit and fail safely. Add :ro or readonly whenever the container only needs to read, reach for named volumes when you want persistence without managing the path yourself, and use the Compose volumes directive to capture any of it in a file. When a mount misbehaves, the cause is almost always an absolute-vs-relative path slip or a UID ownership mismatch rather than the syntax itself.

Once your containers are running with the right data mounted, the next question is what's actually happening inside them. Watching container resource usage catches a mount that's quietly filling a host disk, and richer telemetry catches the subtler cases, like a process that can't write to its data directory or config that loads but is wrong. Dash0's infrastructure monitoring tracks these alongside real-time logs and distributed traces, so you can connect a misconfigured mount to its downstream effect from a single place. Start a free trial to monitor your containers, hosts, and clusters in one view. No credit card required.