The EACCES mkdir permission denied error occurs when a process inside a Docker container cannot create a directory due to insufficient filesystem permissions. This typically happens when running as a non-root user without proper ownership of the target directory or its parent path.
The EACCES (Error: Access) permission denied error when attempting to `mkdir` inside a Docker container indicates that the process lacks write permissions to the parent directory where you're trying to create a new folder. In Docker containers, this error commonly appears when: - Your application runs as a non-root user (via the USER directive) but the parent directory is owned by root - You mount a volume from the host where the ownership doesn't match the container user's UID/GID - The Dockerfile creates directories in the wrong order relative to the USER directive - WORKDIR is declared after USER, but the directory doesn't exist yet The error is most frequently encountered in Node.js applications trying to create directories for data storage, caches, or uploaded files, but it affects any containerized application that needs to create directories at runtime.
First, determine which user your process runs as and who owns the target directory:
# Check the running user
docker exec <container_name> whoami
# Check user ID and group ID
docker exec <container_name> id
# Check ownership of the parent directory
docker exec <container_name> ls -la /app/
# Check ownership recursively
docker exec <container_name> ls -laR /app/If the user is node (UID 1000) but the directory is owned by root:root, that's the cause of your permission error.
The most reliable approach is to create all directories while still running as root, then chown them to the runtime user:
FROM node:20-alpine
WORKDIR /app
# Copy and install dependencies as root
COPY package*.json ./
RUN npm ci
# Copy application code
COPY . .
# Create the data directory while still root
RUN mkdir -p /app/data /app/uploads /app/cache
# Create non-root user and set ownership of everything
RUN addgroup -g 1001 -S appgroup \
&& adduser -S appuser -u 1001 -G appgroup \
&& chown -R appuser:appgroup /app
# Now switch to non-root user
USER appuser
CMD ["node", "server.js"]This ensures all directories exist and are writable before the user switch.
When copying files into the container, use the --chown flag to set ownership immediately:
FROM node:20-alpine
# Create user first
RUN addgroup -g 1001 -S appgroup \
&& adduser -S appuser -u 1001 -G appgroup
WORKDIR /app
# Create directories with correct ownership
RUN mkdir -p /app/data && chown -R appuser:appgroup /app
USER appuser
# Copy files with correct ownership
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci
COPY --chown=appuser:appgroup . .
CMD ["node", "server.js"]The --chown flag ensures files are owned by the runtime user from the start.
When using bind mounts, ensure the host directory permissions match the container user:
# docker-compose.yml
services:
app:
build: .
volumes:
- ./data:/app/data
user: "${UID:-1000}:${GID:-1000}"Before running, create the host directory with correct permissions:
# Find your user ID and group ID
id -u # UID (e.g., 1000)
id -g # GID (e.g., 1000)
# Create the directory on the host
mkdir -p ./data
# The directory will inherit your user's ownership
ls -la ./dataSet environment variables to pass your IDs:
export UID=$(id -u)
export GID=$(id -g)
docker-compose upNamed volumes automatically handle permissions better than bind mounts:
# docker-compose.yml
services:
app:
build: .
volumes:
- app-data:/app/data
volumes:
app-data:Docker manages named volume permissions, and they persist across container restarts. The container user can write to them without host-side permission setup.
If you need to initialize the volume with specific ownership, use an entrypoint script:
#!/bin/sh
# entrypoint.sh
# Ensure data directory is writable (runs as root initially)
chown -R appuser:appgroup /app/data 2>/dev/null || true
# Switch to non-root user and run the main command
exec su-exec appuser "$@"For dynamic directory creation, use an entrypoint script that runs as root then drops privileges:
FROM node:20-alpine
RUN apk add --no-cache su-exec
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN addgroup -g 1001 -S appgroup \
&& adduser -S appuser -u 1001 -G appgroup \
&& chown -R appuser:appgroup /app
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["node", "server.js"]#!/bin/sh
# entrypoint.sh
# Create runtime directories with correct permissions
mkdir -p /app/data /app/uploads /app/cache
chown -R appuser:appgroup /app/data /app/uploads /app/cache
# Drop to non-root user and execute CMD
exec su-exec appuser "$@"This allows runtime directory creation while maintaining security.
After applying the fix, verify the permissions are correct:
# Rebuild the image
docker build -t myapp .
# Run and check permissions
docker run --rm myapp ls -la /app/
# Test directory creation
docker run --rm myapp sh -c "mkdir -p /app/data/test && ls -la /app/data/"
# Check it works with your actual application
docker run --rm myapp node -e "require('fs').mkdirSync('/app/data/test', {recursive: true}); console.log('Success!')"The directory creation should now succeed without permission errors.
Understanding Linux Permissions in Containers
Docker containers share the host's kernel, meaning filesystem permissions are enforced by the same Linux permission model. The container doesn't have its own user database - it just maps numeric UIDs/GIDs to names from /etc/passwd inside the container.
A file owned by UID 1000 in the container is owned by UID 1000 on the host, regardless of what username maps to that UID in either context.
The Principle of Least Privilege
Running containers as root is convenient but insecure. A compromised application running as root inside a container can potentially escape to the host or access sensitive resources. Always:
1. Create a dedicated non-root user for your application
2. Only grant write permissions to directories that truly need it
3. Keep system directories (/usr, /etc, /bin) read-only
Rootless Docker Considerations
In rootless Docker mode, UIDs are remapped. Container UID 1000 might map to host UID 101000 (base UID + container UID). Check your mapping with:
cat /etc/subuid
cat /etc/subgidMulti-stage Builds and Permissions
When using multi-stage builds, remember that COPY --from uses the source stage's ownership. You may need to chown after copying:
FROM builder AS build
# ... build steps ...
FROM node:20-alpine
RUN adduser -S appuser
COPY --from=build /app/dist /app/dist
RUN chown -R appuser /app
USER appuserSecurity Warning
Never use chmod 777 as a fix. While it eliminates permission errors, it allows any user (including potential attackers) to read, write, and execute files. Always use proper ownership (chown) to the specific user that needs access.
image operating system "linux" cannot be used on this platform
How to fix 'image operating system linux cannot be used on this platform' in Docker
manifest unknown: manifest unknown
How to fix 'manifest unknown' in Docker
cannot open '/etc/passwd': Permission denied
How to fix 'cannot open: Permission denied' in Docker
Error response from daemon: failed to create the ipvlan port
How to fix 'failed to create the ipvlan port' in Docker
toomanyrequests: Rate exceeded for anonymous users
How to fix 'Rate exceeded for anonymous users' in Docker Hub