Permissions, Bind Mounts and the Reclaim Helper¶
LDM occasionally starts a short-lived Alpine container that does nothing but
change the ownership and mode of one of your project's directories. If you have
seen those appear and disappear during ldm run, or hit a "Permission denied"
on a project directory, this page explains what they are, why they exist, and
which directories they touch.
The problem they solve¶
Most of a project's directories are bind mounts — a folder on your machine, presented directly inside the container. On native Linux a bind mount performs no UID translation: a file owned by uid 1001 on the host is owned by uid 1001 inside the container, whatever user that container runs as.
The Liferay image's Tomcat/Equinox process runs as uid 1000, and so does
the elasticsearch:8.x image behind liferay-search-global. Your host account
is almost never uid 1000 — it is typically 1001 on Linux CI and 501 on macOS.
So the two sides of the mount are different users, and neither is in the other's group.
Why the mode is 777¶
The helper sets ownership to the host user and then applies a mode. If that
mode were 750, uid 1000 would land in the "other" class and have no access at
all to a tree it must both read and write. 770 and 775 do not help either,
because the container's uid is not in the host user's group — the "other" class
is the only one both parties land in.
This is not hypothetical. LDM-#599 changed the default from 777 to 750 and
broke native Linux; LDM-#645 ("restore native linux cross-container mapping")
re-widened the affected directories one release later.
On macOS and Windows the point is largely moot, because Docker Desktop's virtiofs and gRPC-FUSE mounts present files as the host user regardless of mode — which is exactly why that regression was not noticed before it shipped.
What this means for you: the config trees LDM reclaims are world-readable on your host. On a shared machine, treat a project directory as readable by any local account.
What the helper actually does¶
docker run --rm \
--label com.liferay.ldm.managed=true \
--label com.liferay.ldm.helper=chown \
-v <directory>:/workspace alpine \
sh -c "chown -R <host-uid>:<host-gid> /workspace; chmod -R 777 /workspace; stat -c '%a' /workspace"
Three things worth noting:
- It is recursive. Everything beneath the directory is re-owned and re-moded, not just the directory itself.
- It runs as root inside the container, which is the whole point: it can
correct a directory your own account does not own. Nothing running as your
user can — not
rsync -p, notchmodover SSH. - It ends with
stat, because achmodon exFAT, FAT32 or NTFS exits 0 and silently changes nothing. Verifying the mode is the difference between "the command ran" and "the mode changed" (LDM-#1946).
You can spot strays with:
Which directories, on which paths¶
There is no single reclaim. Different commands reclaim different sets, and they are not the same set.
| path key | pre-boot (handlers/base.py) |
run pipeline (pipelines/run.py) |
repair (ldm rescue) |
|---|---|---|---|
data |
yes | — | — |
state |
yes | — | — |
logs |
yes | yes | yes |
deploy |
yes | yes | yes |
files |
yes | yes | yes |
configs |
yes | — | — |
log4j |
yes | — | — |
portal_log4j |
yes | — | — |
backups |
yes | — | — |
cx |
yes | — | — |
routes |
yes | yes | yes |
osgi |
— | yes | yes |
osgi is the parent of configs, state, modules and marketplace.
Reclaiming it recursively covers those children, which is why the run pipeline
names the parent where the pre-boot check names the children.
Two gates decide whether the run pipeline reclaims at all:
- against a remote target, it always runs, through that node's Docker context;
- locally, only on Linux, or when you pass
--fix-permissions. On macOS without that flag it does not run, because Docker Desktop's mounts make it unnecessary.
On a remote node, the reclaim also runs before the sync¶
The run pipeline's reclaim happens after the project is synced to the node,
and a container keeps writing once it is up. Anything it creates after that,
such as routes/default/<project>, belongs to uid 1000. The SSH user cannot
change or delete it, so on the next run the sync failed. rsync then skips its
--delete step for the whole transfer, which leaves files you deleted locally
on the node, where they are mounted back in (LDM-#2266).
So against a remote target LDM now also reclaims, through the node's Docker daemon:
- before every sync (
ldm run,ldm start, and moving a project to another target): it reclaims the run pipeline's own list of trees, but only those that already exist on the node, and gives them to the node's SSH user rather than to your local uid; - before
ldm rm --deleteremoves the node's copy: it reclaims the whole project directory, which is about to be deleted anyway.
If the removal still fails, the warning now says whether the node answered. "May be unreachable" is reserved for a node that really did not answer.
The routes tree is a special case¶
routes/default/<project> is written by Liferay, at runtime, after the
container is healthy — not by LDM before it starts. Liferay creates those
directories under its own umask, and its
RoutesPortalK8sConfigMapModifier._deleteRoutes deletes and recreates the
per-project child on each publish.
That has two consequences:
- A reclaim that runs before boot cannot set the mode of a directory Liferay has not created yet.
- Setting ownership once on an existing directory does not hold, because the child is replaced.
LDM sets UMASK=0022 in the Liferay container (LDM-#1944) so that what Liferay
creates is readable — that is the primary lever, and the reclaim is the backstop.
If you have a project from before LDM v2.26.0, its routes/default may
still carry the old 0027-derived mode of 750. Nothing recreates that
directory, so the umask fix cannot reach it. Symptoms are a remote sync failing
with Permission denied on routes/default, or a client extension unable to
read its own config. ldm rescue <project> repairs it (LDM-#2144).
Related¶
docs/how-to/multi_node_orchestration.md— how a project is synced to a compute node, and what happens when that sync fails.docs/TROUBLESHOOTING.md— the Permission & Mount Issues section, for symptoms rather than mechanism.
Last Updated: 2026-10-20 | Last Reviewed: 2026-10-20