16.2.1.4 Runtime Permission Denied
A focused guide to Runtime Permission Denied, connecting core concepts with practical Docker and container operations.
Runtime permission denied errors occur when a container's process attempts an operation, executing a file, reading or writing a path, binding to a port, that the operating system's permission model rejects, and because containers frequently run as non-root users and interact with bind-mounted host paths with their own independent ownership, this category of error is both common and often rooted in a mismatch between the container's user identity and the filesystem or system resource it is trying to access.
Executable permission missing on a script
A script copied into an image without its executable bit set produces a clear, specific permission error when the container attempts to run it directly:
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
exec /entrypoint.sh: permission denied
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
Explicitly setting the executable bit during the build, rather than relying on whatever permission the file happened to have on the host filesystem before being copied, resolves this directly and predictably regardless of the host's own file permission state.
Non-root user lacking access to a file or directory
When a container runs as a non-root user, either because the image explicitly sets one with USER or because of a security policy enforcing it, that user needs explicit read, write, or execute permission on whatever paths the application actually needs to access:
RUN useradd --create-home appuser
USER appuser
WORKDIR /app
COPY --chown=root:root . .
CMD ["node", "server.js"]
Error: EACCES: permission denied, open '/app/config.json'
If the files were copied while owned by root and the application now runs as appuser, that user may lack read access depending on the specific permission bits set; using COPY --chown= to assign appropriate ownership at copy time, rather than relying on default ownership and adjusting permissions separately afterward, is the more direct and reliable fix:
COPY --chown=appuser:appuser . .
Bind mount ownership mismatches
A bind-mounted host directory retains the ownership and permissions it has on the host filesystem, which frequently does not match the UID the container's process runs as, since UIDs are not automatically translated or remapped between the host and container namespaces by default:
docker run -v /srv/app/data:/app/data --user 1000:1000 my-api
Error: EACCES: permission denied, open '/app/data/cache.db'
If the host directory /srv/app/data is owned by a different UID than 1000 on the host, the container's process, running as UID 1000, will be denied access regardless of what permissions the image itself configures internally, since bind mount permission enforcement happens at the actual host filesystem level, which the container's own internal user configuration does not change.
chown -R 1000:1000 /srv/app/data
Adjusting host-side ownership to match the UID the container actually runs as resolves this, though it requires knowing and aligning on that specific UID across both the image's configuration and the host's directory ownership.
Volume mount permissions for named volumes
Named, Docker-managed volumes are typically initialized with permissions matching whatever the image's filesystem at the mount point would have been, but a volume reused across image updates that changed the expected user or permission scheme can retain stale ownership from an earlier configuration:
docker volume inspect pgdata --format '{{ .Mountpoint }}'
docker run --rm -v pgdata:/data alpine chown -R 999:999 /data
Running a one-off container to explicitly correct ownership on an existing named volume is a reasonable fix when a volume's stale permissions no longer match what a newer image version expects.
Privileged ports and capability restrictions
A non-root process attempting to bind to a port below 1024 is denied by the kernel's own privilege model, independent of anything Docker itself enforces, since binding to privileged ports has traditionally required root privilege or an explicitly granted capability:
USER appuser
EXPOSE 80
CMD ["node", "server.js"]
Error: listen EACCES: permission denied 0.0.0.0:80
The two practical fixes are having the application listen on a non-privileged port (commonly 8080 or similar) and mapping that to the desired external port through Docker's own port publishing, or explicitly granting the specific capability needed without requiring full root privilege:
docker run -p 80:8080 my-api
docker run --cap-add=NET_BIND_SERVICE my-api
Read-only filesystem restrictions
A container run with --read-only blocks any write attempt to the container's filesystem outside of explicitly mounted, writable volumes or tmpfs mounts, which produces a permission error for any application that attempts to write somewhere not specifically exempted:
docker run --read-only my-api
Error: EROFS: read-only file system, open '/app/logs/app.log'
docker run --read-only --tmpfs /tmp --tmpfs /app/logs my-api
Identifying every path an application genuinely needs to write to, and explicitly providing a writable mount (a volume or tmpfs) for each one, rather than disabling read-only mode entirely, preserves most of the security benefit of a read-only filesystem while still accommodating the application's actual, legitimate write needs.
Diagnosing with an interactive session
For a permission error whose exact cause is not immediately clear, starting an interactive session as the same user the container actually runs as allows direct testing of file access and permission behavior:
docker run -it --rm --user 1000:1000 my-api sh
ls -la /app/config.json
touch /app/data/test-write
Directly attempting the specific operation that is failing, from within a session running as the exact same user, confirms the precise permission gap more reliably than reasoning about expected behavior abstractly.
Common mistakes
- Not setting the executable bit explicitly on a copied script, relying on host filesystem permissions that may not carry over correctly into the image.
- Switching to a non-root user without ensuring file ownership for application files and directories was assigned correctly at the same time.
- Bind-mounting a host directory without aligning its ownership to the UID the container actually runs as, since UID translation does not happen automatically between host and container.
- Attempting to bind a privileged port as a non-root user without either changing the listening port or explicitly granting the required capability.
- Enabling a read-only filesystem without first identifying and explicitly providing writable mounts for every path the application genuinely needs to write to.
Runtime permission denied errors are resolved by identifying exactly which user the failing operation is running as, and exactly which permission boundary, file ownership, capability restriction, read-only filesystem, is actually blocking it, since the fix differs meaningfully depending on which of these several distinct permission mechanisms is the one actually responsible for a given failure.