This error occurs when Docker containers cannot change file permissions using chmod, typically due to non-root user restrictions, volume mount limitations (NFS/CIFS), or base image constraints. The fix usually involves using COPY --chown in Dockerfiles, switching to root temporarily, or adjusting volume mount options.
The "chmod: changing permissions: Operation not permitted" error in Docker indicates that the container process lacks the necessary privileges to modify file permissions. This is different from a standard permission denied error - it means the operation itself is not allowed, regardless of the file's current ownership. This error commonly occurs in three scenarios: 1. **Non-root user in container**: Many Docker images (especially security-hardened ones) run as non-root users by default. When you COPY files into the container, they're owned by root with restricted permissions. The non-root user cannot chmod these files. 2. **Volume mount restrictions**: When mounting volumes from certain filesystems (NFS with root_squash, CIFS/Azure Files, or some network storage), the underlying filesystem doesn't support Linux permission changes. chmod operations are silently ignored or return EPERM. 3. **Read-only layers**: In some cases, files copied during the build process are in read-only layers, and attempting to chmod them at runtime fails. Understanding which scenario applies to your case is crucial for selecting the right fix.
The most reliable fix is to set ownership when copying files, rather than running chmod afterward:
# Instead of:
COPY script.sh /app/script.sh
RUN chmod +x /app/script.sh # This may fail
# Use --chown to set owner and permissions in one step:
COPY --chown=1000:1000 script.sh /app/script.sh
RUN chmod +x /app/script.sh # Now works because user 1000 owns the file
# Or if you know your non-root user name:
COPY --chown=appuser:appuser script.sh /app/script.shFor multiple files:
# Copy entire directory with correct ownership
COPY --chown=node:node ./src /app/src
# Or use a specific UID that matches your runtime user
COPY --chown=1001:1001 . /appNote: The --chown flag requires Docker 17.09 or later.
If you can't use --chown, switch to root before chmod, then switch back:
FROM node:18-alpine
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Copy files as root (default)
COPY entrypoint.sh /app/entrypoint.sh
# As root, set permissions
RUN chmod +x /app/entrypoint.sh && \
chown appuser:appgroup /app/entrypoint.sh
# Switch to non-root user for runtime
USER appuser
ENTRYPOINT ["/app/entrypoint.sh"]For images that already set a non-root user:
FROM bitnami/node:18
# Temporarily switch to root
USER root
COPY custom-script.sh /opt/scripts/
RUN chmod +x /opt/scripts/custom-script.sh
# Switch back to the original non-root user
USER 1001
CMD ["/opt/scripts/custom-script.sh"]Tip: Check your base image documentation to find the correct non-root user UID to switch back to.
Set the correct permissions on your host before building:
# Make script executable on host before build
chmod +x ./scripts/entrypoint.sh
chmod +x ./scripts/*.sh
# Then build - permissions are preserved
docker build -t myapp .In your Dockerfile, the COPY will preserve the executable bit:
# File already has +x on host, so it's executable in container
COPY entrypoint.sh /app/entrypoint.shFor Git repositories: Git tracks executable permissions, so commit with:
# Add executable permission to Git
git update-index --chmod=+x scripts/entrypoint.sh
git commit -m "Make entrypoint.sh executable"Note: This only works for new COPY operations, not for files in mounted volumes.
NFS and CIFS mounts often don't support Linux permission changes. Use these workarounds:
Option 1: Match container user to volume owner
# Find the UID/GID that owns files on the NFS share
ls -ln /mnt/nfs/share
# Output: drwxr-xr-x 2 1000 1000 4096 Jan 1 12:00 data
# Run container as that user
docker run --user 1000:1000 -v /mnt/nfs/share:/data myappOption 2: Use named volumes instead of bind mounts
# Named volumes handle permissions better
docker volume create app-data
docker run -v app-data:/data myappOption 3: For Azure Files / CIFS, set permissions at mount time
In docker-compose.yml:
services:
app:
image: myapp
volumes:
- type: volume
source: azure-files
target: /data
volume:
nocopy: true
volumes:
azure-files:
driver: local
driver_opts:
type: cifs
o: "username=USER,password=PASS,uid=1000,gid=1000,file_mode=0755,dir_mode=0755"
device: "//storage.file.core.windows.net/share"The file_mode and dir_mode options set permissions at mount time since chmod won't work.
Docker Desktop on macOS has specific file sharing requirements:
Step 1: Ensure directories are shared in Docker Desktop settings:
1. Open Docker Desktop
2. Go to Settings > Resources > File Sharing
3. Add the directories containing your scripts
4. Click Apply & Restart
Step 2: For persistent chmod issues, copy files instead of mounting:
# Instead of mounting at runtime:
# docker run -v ./scripts:/app/scripts myapp
# Copy scripts into the image at build time:
COPY --chmod=755 scripts/ /app/scripts/Step 3: Use Docker volumes for data that needs modification:
# Create a Docker volume (not a bind mount)
docker volume create app-scripts
# Copy files into the volume using a helper container
docker run --rm -v app-scripts:/target -v $(pwd)/scripts:/source alpine sh -c "cp -r /source/* /target/ && chmod +x /target/*.sh"
# Now use the volume in your app
docker run -v app-scripts:/app/scripts myappNote: The --chmod flag in COPY requires BuildKit. Enable it with:
DOCKER_BUILDKIT=1 docker build -t myapp .On RHEL, CentOS, and Fedora systems with SELinux, add the :z or :Z suffix to volume mounts:
# Shared volume (multiple containers can access)
docker run -v /host/scripts:/app/scripts:z myapp
# Private volume (only this container)
docker run -v /host/scripts:/app/scripts:Z myappThe :z/:Z options relabel the volume with appropriate SELinux contexts.
Check if SELinux is the cause:
# Check SELinux status
getenforce
# Look for SELinux denials in audit log
sudo ausearch -m avc -ts recent | grep docker
# Temporarily disable to test (not for production)
sudo setenforce 0
docker run -v /host/scripts:/app/scripts myapp
sudo setenforce 1Permanent fix without :z/:Z:
# Set the correct SELinux context on host directory
sudo chcon -Rt svirt_sandbox_file_t /host/scriptsDocker BuildKit supports setting permissions directly in COPY:
# syntax=docker/dockerfile:1
FROM alpine
# Set executable permission directly in COPY
COPY --chmod=755 entrypoint.sh /app/entrypoint.sh
COPY --chmod=644 config.json /app/config.json
# Multiple files with same permissions
COPY --chmod=755 scripts/*.sh /app/scripts/Enable BuildKit:
# Option 1: Environment variable
DOCKER_BUILDKIT=1 docker build -t myapp .
# Option 2: Docker daemon config (/etc/docker/daemon.json)
{
"features": {
"buildkit": true
}
}
# Option 3: Use docker buildx (BuildKit by default)
docker buildx build -t myapp .Combining --chown and --chmod:
# Set both owner and permissions
COPY --chown=node:node --chmod=755 entrypoint.sh /app/entrypoint.shNote: --chmod in COPY requires Docker 20.10+ with BuildKit enabled.
Understanding file ownership in multi-stage builds:
In multi-stage builds, file ownership can be tricky:
# Stage 1: Build as root
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Runtime as non-root
FROM node:18-alpine
RUN adduser -D appuser
WORKDIR /app
# Files from builder stage are owned by root
# Use --chown to transfer with correct ownership
COPY --from=builder --chown=appuser:appuser /app/dist ./dist
COPY --from=builder --chown=appuser:appuser /app/node_modules ./node_modules
USER appuser
CMD ["node", "dist/index.js"]Debugging permission issues:
# Check file ownership and permissions inside container
docker run --rm myapp ls -la /app/
# Check what user the container runs as
docker run --rm myapp id
# Check if filesystem supports permission changes
docker run --rm -v /path:/data myapp stat -f /dataRootless Docker considerations: In rootless Docker, UID mapping adds complexity. The container's root user maps to your host user, which can cause unexpected permission behaviors with bind mounts.
Kubernetes equivalent: In Kubernetes, use securityContext to manage user/group:
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000 # Sets group ownership of mounted volumesBest practices:
1. Always use COPY --chown for files that non-root users need to access
2. Prefer named volumes over bind mounts when permissions are problematic
3. Set file permissions on the host before building when possible
4. Use BuildKit's --chmod for cleaner Dockerfiles
5. Document the expected user/group IDs in your project's README
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