Skip to main content
RunBook Academy

Proxmox VEX · LXC ContainersConfiguration

Bind mounts and mount points: sharing host storage with a container

Intermediate⏱ ~26 minpct

What you'll learn

  • Distinguish volume, bind and device mount points and say which one vzdump protects
  • Predict the ownership a bind-mounted directory will have inside an unprivileged container
  • Fix ownership with a host chown or a per-mount-point idmap, and choose between them
  • Recognise why a container with a bind mount migrates badly, and what shared=1 actually asserts

Prerequisites

Verified against Proxmox VE 9.2.4 · Proxmox Backup Server 4.2.5 · Ceph Squid / Tentacle · Debian 13 (Trixie) · Linux kernel 7.0 (PVE 9.2 default) · 2026-08-12

Not yet marked complete on this device.

A container with 8 GiB of rootfs and 40 TiB of media on the host is the normal shape of an LXC deployment. Getting the media into the container is one line of configuration. Getting it in correctly involves two things that are not visible in that line: who owns the files once they are inside, and whether they are in the backup.

Both are decided by choices you make when you write the mount point, and both fail quietly.

Three kinds of mount point, and the one difference that matters

mp0 through mp255 all use the same option syntax, but the value in the volume position selects between three quite different things.

KindWritten asManaged by PVE storage?In vzdump?
Volume mount pointmp0: local-zfs:32,mp=/dataYes - PVE allocates itYes, unless backup=0
Bind mount pointmp0: /srv/ct-media,mp=/mediaNo - a host pathNo
Device mount pointmp0: /dev/disk/by-id/…,mp=/dataNo - a host block deviceNo

The documentation is explicit that the contents of bind and device mount points are not backed up with vzdump, and that the backup option applies to volume mount points only. There is no flag that changes this.

Adding a bind mount

Configuration changebind a host directory into a stopped container
set -euo pipefail
CTID=200
HOSTDIR=/srv/ct-media

# The source must exist and must not contain symlinks anywhere in its path.
mkdir -p "$HOSTDIR"

pct set "$CTID" --mp0 "$HOSTDIR,mp=/media"

pct config "$CTID" | grep -E '^(rootfs|mp[0-9]+):'

Options worth knowing, all documented on mp[n]:

OptionEffect
mp=Path inside the container. Required.
ro=1Mount read-only. The cheapest real safety control on this page.
backup=1Volume mount points only. Silently irrelevant on a bind mount.
shared=1Marks a non-volume mount point as “available on all nodes”.
replicate=0Exclude a volume from storage replication jobs. Default is 1.
mountoptions=Extra mount options, semicolon-separated.
idmap=Per-mount UID/GID remapping. The subject of the next section.
keepattrs=1Inherit UID, GID and mode from the mount point directory if it already exists.

Why the files show up as nobody:nogroup

Here is the moment every operator meets once.

Read-only / Safethe same directory, from the host and from inside the container
# stat -c '%U:%G %u:%g %n' /srv/ct-media
pct exec 200 -- stat -c '%U:%G %u:%g %n' /media
root:root 0:0 /srv/ct-media
nobody:nogroup 65534:65534 /media

Illustrative output

Nothing is broken. This is the user namespace working exactly as designed.

An unprivileged container runs inside a user namespace whose mapping is, by default, container UID 0..65535 onto host UID 100000..165535. That mapping is declared for the root user in /etc/subuid and /etc/subgid:

Read-only / Safethe subordinate ID delegation that makes the default mapping legal
# cat /etc/subuid /etc/subgid
root:100000:65536
root:100000:65536

Illustrative output

So the arithmetic runs in both directions:

  • A file that a container process creates as container UID 0 is written to disk as host UID 100000.
  • A file on disk owned by host UID 0 has no container-side ID, because 0 is outside the mapped range 100000..165535. The kernel reports it as the overflow UID, 65534, which /etc/passwd renders as nobody.

nobody:nogroup is not an error message. It is the kernel saying “this ownership does not exist in your namespace.”

Two ways to fix ownership, and when to use each

Option 1: chown on the host to the mapped ID

The blunt, portable fix. Decide what the container-side owner should be, add 100000, and set that on the host.

Configuration changegive container root ownership of a bind-mounted tree
set -euo pipefail
HOSTDIR=/srv/ct-media
BASE=100000            # first host UID of the container's default map

# container root (0) -> host 100000
chown -R "$BASE:$BASE" "$HOSTDIR"

# container uid 1000 (a service account inside) -> host 101000
# chown -R "$((BASE + 1000)):$((BASE + 1000))" "$HOSTDIR/app"

stat -c '%u:%g %n' "$HOSTDIR"

This works, and it is what most estates do. Its limitation is that the data now belongs to one container’s namespace. A host process, or a second container with a different offset, sees 100000 and has no idea what it is.

Option 2: a per-mount-point idmap

PVE supports a custom mapping on the mount point itself, overriding the container’s default for that mount only. The syntax is type:container:disk:range-size, semicolon-separated, where type is u for UIDs or g for GIDs.

Configuration changemap one on-disk owner to one in-container owner
set -euo pipefail
CTID=200

# On disk the tree is owned by 1000:1000 (a host service account).
# Inside the container it should appear as 1000:1000 as well.
pct set "$CTID" --mp0 '/srv/ct-media,mp=/media,idmap=u:1000:1000:1;g:1000:1000:1'

pct reboot "$CTID"
pct exec "$CTID" -- stat -c '%u:%g %n' /media

The documented special value passthrough identity-maps every ID on that mount, so the container sees exactly the on-disk ownership.

SituationUse
Directory exists only to serve this containerchown to 100000-relative IDs
Host and container both read and write the treeidmap= for the specific IDs involved
Two containers share the same treeidmap= in both, mapping to the same on-disk IDs
Container is untrustedro=1, and neither of the above
You are reaching for passthroughAsk whether a volume mount point solves it instead

Migration, and what shared=1 really claims

A volume mount point on shared storage migrates because PVE knows where the data is and that the target node can reach it. A bind mount is a host path, and PVE has no way to know whether /srv/ct-media on pve-02 is the same data as /srv/ct-media on pve-01, a different NFS export, or an empty directory.

shared=1 is documented as marking the mount point “as available on all nodes”. Read that as an assertion you are making, which PVE then trusts.

Verification, and undo

A mount point that “looks right” in pct config proves only that the config parsed. These four checks can each fail.

Read-only / Safeverify a bind mount is real, writable and correctly owned
set -euo pipefail
CTID=200
INNER=/media

# 1. The container agrees it is a mount, not just a directory.
pct exec "$CTID" -- findmnt -no SOURCE,FSTYPE,OPTIONS "$INNER"

# 2. It is not empty when it should not be.
pct exec "$CTID" -- sh -c "ls -A '$INNER' | head -5"

# 3. The service account inside can actually write.
pct exec "$CTID" -- runuser -u www-data -- \
sh -c "touch '$INNER/.writetest' && rm '$INNER/.writetest'" \
&& echo 'write OK'

# 4. Ownership resolves to a name, not to nobody.
pct exec "$CTID" -- stat -c '%U:%G %n' "$INNER"

Check 3 is the one that matters. ls succeeding tells you the mount exists; only a write as the account the application actually runs as tells you the mapping is right.

Configuration changeremove a mount point
set -euo pipefail
CTID=200

pct set "$CTID" --delete mp0
pct reboot "$CTID"
pct config "$CTID" | grep -E '^mp[0-9]+:' || echo 'no mount points remain'

Removing a volume mount point behaves differently: the volume becomes an unused disk on the container config rather than disappearing, and you delete it deliberately afterwards. That asymmetry is a feature - PVE will not silently destroy storage it allocated.

Knowledge check

Knowledge check · 5 questions

  1. Q1. A container with a bind mount at /media is backed up nightly by vzdump to PBS. The container is destroyed and restored from last night’s snapshot. What is the state of /media?

  2. Q2. You bind mount /srv/ct-media, owned root:root on the host, into an unprivileged container. Inside, ls shows the directory as nobody:nogroup. Why?

  3. Q3. Which are sound reasons to prefer a per-mount-point idmap= over chowning the host directory? Select all that apply.

  4. Q4. Setting shared=1 on a bind mount point causes Proxmox to verify that the path holds the same data on every node before allowing migration.

  5. Q5. A directory should appear as owned by UID 1000 inside an unprivileged container using the default mapping. Which host UID should own it on disk?

Passing score: 75%. Answers are checked in this browser.