Two containers can access the same host directory and still disagree about where a file lives.
That became one of the most important lessons in building a Docker-based media stack.
The download client reported one internal path. The organizer received the same data under another. From the host, both bind mounts looked valid. Inside the applications, the completed file appeared to exist in two different places.
The symptoms looked like application failures:
- completed downloads that would not import;
- unnecessary copy-and-delete operations;
- hardlinks that never appeared;
- remote path mappings that multiplied confusion;
- permission troubleshooting aimed at the wrong directory;
- a media server that could read the final library while the automation layer could not create it correctly.
The underlying problem was simpler: the containers did not share one path contract.
Containers communicate with container paths
Applications inside containers do not exchange the host-side half of a bind mount.
Suppose a download client sees this:
/downloads/complete/movies/example.mkv
It reports that path through its API. The importing application must be able to resolve the same reported path to the same file.
A host can map the same storage into two containers under different names:
services:
downloader:
volumes:
- /srv/media-data/downloads:/downloads
organizer:
volumes:
- /srv/media-data/downloads:/data/downloads
- /srv/media-data/library:/data/library
Both mounts are valid individually. The downloader can read /downloads. The organizer can read /data/downloads.
The problem appears when the downloader reports:
/downloads/complete/movies/example.mkv
The organizer has no /downloads path. It knows the file as:
/data/downloads/complete/movies/example.mkv
The host knows those locations refer to the same storage. The applications do not.
One shared data root creates one contract
The cleaner design is to mount a common parent at the same container path in every service that participates in downloading or importing.
services:
downloader:
volumes:
- /srv/media-data:/data
organizer:
volumes:
- /srv/media-data:/data
The participating containers now agree on one hierarchy:
/data/downloads
/data/downloads/complete
/data/library/movies
/data/library/shows
/data/library/music
A completed file reported as:
/data/downloads/complete/movies/example.mkv
has the same meaning inside both applications.
The host path in this example is generic. It could be a local filesystem, a dedicated disk mounted beneath that directory, or a carefully designed network-backed mount. The important part is that the applications share the same container-visible root.
Separate Docker mounts can hide one filesystem
Path consistency is not only about whether an application can locate a file. Mount boundaries also affect how the filesystem relationship appears inside a container.
Consider this layout:
volumes:
- /srv/media-data/downloads:/downloads
- /srv/media-data/library:/library
The two host directories might live on the same physical filesystem. Docker still presents them as separate mounts inside the container.
That can prevent the application from treating the source and destination as one filesystem relationship. Imports that could have used an atomic move or hardlink may become full copies followed by deletion.
Mounting their common parent once preserves the relationship:
volumes:
- /srv/media-data:/data
Then both locations remain beneath one visible mount:
/data/downloads
/data/library
Why hardlinks depend on the layout
A hardlink lets two directory entries point to the same underlying file data.
In a torrent workflow, that means a completed file can remain in the download directory for seeding while an organized filename appears in the media library without consuming a second file’s worth of storage.
Hardlinks require several conditions:
- the source and destination must be on the same filesystem;
- the container must see that relationship through a compatible mount layout;
- the importing process must have permission to create the link;
- the filesystem must support hardlinks;
- the application must be configured to use them where appropriate.
Matching paths do not magically create hardlinks. They remove one of the common reasons hardlinks are impossible.
I verify the result with filesystem metadata instead of assuming the dashboard is correct:
stat /srv/media-data/downloads/complete/movies/example.mkv
stat /srv/media-data/library/movies/Example/example.mkv
Matching device and inode values indicate that both paths refer to the same underlying file. A link count greater than one confirms multiple directory entries.
Permissions are a separate layer
A consistent namespace solves path agreement. It does not solve access control.
The participating containers still need compatible identities and permissions:
- numeric user and group IDs that match the intended ownership model;
- directory ownership that allows the required reads and writes;
- a sensible umask or file-creation policy;
- execute permission on parent directories;
- read-only mounts for services that should never modify the library.
A broad permission change can make one import succeed while hiding the real identity mismatch. I avoid treating chmod 777 as a troubleshooting strategy.
The useful test runs from inside the container, not only from the host:
docker compose exec organizer id
docker compose exec organizer ls -ld /data /data/downloads /data/library
docker compose exec organizer touch /data/library/.write-test
docker compose exec organizer rm /data/library/.write-test
Use the actual service name and paths from the deployment. A host administrator being able to list a directory proves very little about the process identity inside the container.
Remote path mappings are not the first fix
Media automation applications often provide remote path mappings. Those mappings are useful when a download client genuinely runs on another host or reports a path that cannot be made identical.
They are not the best first response to inconsistent mounts on one Docker host.
On a single host, adding a translation such as:
/downloads -> /data/downloads
can make one immediate error disappear while preserving the confusing design underneath it.
Before adding a remote path mapping, I ask:
- Does the download client actually run on a different host?
- Can the services share one container path instead?
- Are source and destination intended to support hardlinks or atomic moves?
- Is the reported path visible from the importing container exactly as reported?
When the applications share a Docker host and storage layout, consistent mounts are usually clearer than translation rules.
A small end-to-end test catches the problem early
Before adding a large library, I run one controlled workflow:
- The download client writes a small authorized test item.
- I record the exact completed path reported by the client.
- I confirm the organizer can list that exact path inside its container.
- The organizer imports and renames the item without manual intervention.
- Ownership and permissions remain correct after import.
- The source remains available when the workflow requires seeding.
- I verify whether the import used a hardlink, atomic move, or full copy.
- The media server can read the organized result through its own intended mount.
That one test crosses the full storage contract.
It catches mistakes while cleanup is easy and before thousands of files make the symptoms expensive.
Troubleshooting the boundary in order
When an import fails, I now check the path boundary before changing application settings.
1. Read the path reported by the download client
Do not paraphrase it. Record the exact container path.
2. Check that exact path in the importing container
docker compose exec organizer ls -la /data/downloads/complete
If the reported path starts with /downloads but the organizer only has /data/downloads, the mismatch is already visible.
3. Compare effective mounts
docker inspect downloader
docker inspect organizer
Review the source and destination of each relevant mount. The Compose file you intended to deploy may differ from the container configuration currently running.
4. Verify the filesystem boundary
findmnt --target /srv/media-data/downloads
findmnt --target /srv/media-data/library
Two similar-looking directories may belong to different filesystems. Hardlinks cannot cross that boundary.
5. Test permissions as the application identity
Check ownership, group membership, and write access inside the importing container.
6. Review categories and root folders
A perfect mount cannot compensate for the wrong download category or an incorrect library root.
7. Add translation only when the architecture requires it
Remote path mappings should document a real remote difference, not cover up an avoidable same-host inconsistency.
What I would do differently
I would design the shared path namespace before writing the individual Compose services.
The host directories were not the main interface between the applications. The container-visible namespace was.
Once every participating service agreed about /data, a surprising amount of “application troubleshooting” disappeared. Imports became easier to explain, filesystem behavior became testable, and permission problems were no longer mixed together with path translation problems.
The broader lesson applies beyond media automation: whenever containers exchange file locations through an API, queue, database, or configuration value, the path must have the same meaning on both sides.
Related articles
- The Definitive Guide to the *arr Stack on Ubuntu with Docker
- The Definitive Guide to Jellyfin on Ubuntu with Docker
- The Definitive Guide to Building a Homelab from Scratch
Security note
All paths and service names in this article are generic examples. Exact host mounts, dataset names, accounts, storage locations, download categories, and service topology remain in private documentation.
AI transparency
AI assisted with structure and copy editing. The path-mapping, import, permission, hardlink, and validation lessons come from operating and troubleshooting my Docker-based media stack.