As a web professional who has spent years building complex WordPress sites with Elementor, I rely on Docker for my local development. It’s the best way to create a clean, consistent, and isolated environment that matches my production server. But I’ve hit this “permission denied” wall more times than I can count, especially when working with web servers like Nginx or Apache that need to write to mounted wp-content or node_modules directories.

The good news is that this isn’t some deep, dark magic. The error is almost always caused by a simple mismatch in user permissions between your computer (the “host”) and the Docker container. Once you understand what’s happening, the fix is straightforward.

In this guide, I’ll walk you through exactly why this error occurs and provide several step-by-step solutions, from the quick-and-dirty fixes to the clean, production-safe methods.

Key Takeaways: The Quick Answers

Pressed for time? Here are the most common solutions to the Docker “permission denied” error. The best long-term fix is Solution 2 or Solution 3.

  • The Problem: The user inside the container (e.g., www-data, UID 33) doesn’t have permission to write to the folder on your host machine (owned by your-user, UID 1000).
  • Solution 1 (The “Quick Fix”): Give the container user ownership of the host directory. Find the container’s user ID (e.g., docker exec <container> id) and run sudo chown -R <UID>:<GID> ./your-directory on your host. This is simple but can make it harder for you to edit files on your host.
  • Solution 2 (The “Correct Way”): Rebuild the Docker image to match your host’s user ID. Use a Dockerfile to change the container user’s UID and GID to match your host’s (usually 1000:1000). This is the cleanest, most secure, and most portable solution.
  • Solution 3 (The “Compose Way”): Use the user directive in your docker-compose.yml file. Adding user: “${UID}:${GID}” (and defining these in an .env file) tells Docker to run the container as your host user. This is fantastic for local development.
  • Solution 4 (The “Security Risk”): Running chmod 777 ./your-directory on your host. This makes the folder writable by everyone and anyone. It’s a significant security vulnerability, especially for web-facing files. Avoid this.

Why Does Docker Say “Permission Denied”? Understanding the Root Cause

This error feels confusing because Docker creates a layer of isolation. It’s easy to forget that the files and folders you “share” with a container still live on your host machine and obey your host machine’s (Linux) permission rules.

The entire problem boils down to a conflict between two different users.

It’s All About Users: Host vs. Container

At its core, Linux (and macOS, which is built on a similar foundation) doesn’t care about usernames. It cares about User IDs (UIDs) and Group IDs (GIDs).

Your Host User: When you work on your computer, you are logged in as a user. On most modern Linux and macOS systems, the first user created has a UID of 1000 and a GID of 1000. You can check this by opening a terminal and typing id.
$ id

uid=1000(myuser) gid=1000(myuser) groups=1000(myuser),4(adm),27(sudo)…

  1. Any file or folder you create in your home directory is owned by uid=1000.
  2. The Container User: A Docker container runs its own isolated Linux operating system. This system has its own set of users. A web server image (like nginx or php-fpm) is often configured for security to run its main process as a special, non-privileged user.
    • The official php (Apache) image runs as the www-data user, which has a UID of 33.
    • The official nginx image runs as the nginx user, which has a UID of 82.
    • Many other official images run as a user named node or app, which might have a UID of 1001 or something else entirely.
    • The root user inside the container (UID 0) is NOT the same as the root user outside the container.

The Problem with Bind Mounts

The “permission denied” error almost always happens when you use a bind mount. This is when you tell Docker to map a directory from your host machine directly into the container’s filesystem.

For example, in a docker-compose.yml file:

volumes:

  – ./my-app-code:/var/www/html

Here’s the conflict:

  1. You create ./my-app-code on your host. Its owner is uid=1000.
  2. You start a container (e.g., php-fpm) that runs its process as www-data (uid=33).
  3. Docker maps ./my-app-code to /var/www/html inside the container.
  4. Your application (running as uid=33) tries to create a file (like a cache file or a CSS file generated by Elementor) inside /var/www/html.
  5. The Linux kernel on your host checks the permissions. It sees that a user with uid=33 is trying to write to a directory owned by uid=1000.
  6. The kernel correctly says: “Permission denied.”

Before You Fix It: How to Diagnose the Exact Problem

Don’t just guess. You can find the exact UIDs and GIDs in conflict in about 30 seconds.

Step 1: Find Your Host User’s UID and GID

This is the easy one. Open your terminal and run:

id

You’ll see your uid and gid. On most desktop systems, this will be 1000. We’ll use 1000 as the example for the rest of this guide.

Step 2: Find the Container User’s UID and GID

First, get your container’s name or ID from docker ps. Then, run an exec command to ask the container who it’s running as.

# Replace <container_name> with your container’s name

docker exec -it <container_name> id

You will likely see something different, like:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

There’s your mismatch: Your host is 1000. The container is 33.

Step 3: Find the User Running the Process

Sometimes the id command shows root (if that’s the default user), but the process itself drops privileges. You can check this with ps.

docker exec -it <container_name> ps aux

Look at the USER column. This will show you who is actually running your Nginx, Apache, or Node process. That’s the user you need to worry about.

For a great overview of how to use docker exec for debugging, this video is a helpful resource: https://www.youtube.com/watch?v=sK7KajMZcmA 

Solution 1: The “Quick Fix” (And Why You Shouldn’t Use It)

The fastest, most common, and most dangerous “fix” you’ll find on forums is to just open up your host directory’s permissions completely.

# — DANGEROUS: DO NOT DO THIS —

sudo chmod 777 ./my-app-code

# ———————————–

This command gives everyone (owner, group, and other) the ability to read, write, and execute files in that directory. Yes, it fixes the permission error because the container’s uid=33 user is now allowed to write to the directory.

But this is a massive security risk. You would never set your wp-config.php file to 777 on a live server. Any script, any user, any process on your system can now modify those files. If you’re running a web server, this is an open invitation for a breach.

Avoid this “solution” and learn to fix the problem correctly.

Solution 2: The “Correct” Way: Matching UIDs in Your Dockerfile

This is the cleanest, most professional, and most portable solution. The strategy is to build a custom Docker image where the container’s user has the same UID and GID as your host user.

What is a Dockerfile?

A Dockerfile is just a text file with instructions for building your own Docker image. You’ll almost always FROM an official image (like php:8.2-fpm) and then add your own modifications.

The Strategy: Recreate the Container User

Our goal is to make the container’s www-data user (or nginx, node, etc.) have a UID of 1000 and a GID of 1000 to match our host.

We can pass our host’s UID/GID to the docker build command as “build arguments” (–build-arg) and use them in the Dockerfile.

Step-by-Step Example (for php-fpm / www-data)

Let’s say our project has a file named Dockerfile (or docker/Dockerfile.dev).

1. Create the Dockerfile:

# Start from the official PHP-FPM image

FROM php:8.2-fpm

# — Add this section to fix permissions —

# 1. Get host UID/GID from build arguments, with a default

ARG HOST_UID=1000

ARG HOST_GID=1000

# 2. Check if the ‘www-data’ group (GID 33) exists.

#    If it does, change its GID to our host’s GID.

#    If it doesn’t, create a new group with our host’s GID.

RUN if getent group www-data; then \

        groupmod -o -g $HOST_GID www-data; \

    else \

        groupadd -g $HOST_GID www-data; \

    fi

# 3. Check if the ‘www-data’ user (UID 33) exists.

#    If it does, change its UID to our host’s UID and set its group.

#    If it doesn’t, create a new user with our host’s UID/GID.

RUN if getent passwd www-data; then \

        usermod -o -u $HOST_UID -g $HOST_GID www-data; \

    else \

        useradd -u $HOST_UID -g $HOST_GID -m -s /bin/bash www-data; \

    fi

# 4. (Optional) Give www-data user sudo privileges

# RUN apt-get update && apt-get install -y sudo && \

#     echo “www-data ALL=(ALL) NOPASSWD:ALL” >> /etc/sudoers

# — End of permissions fix —

# Continue with your normal Dockerfile…

# For example, install WordPress extensions

RUN docker-php-ext-install pdo pdo_mysql mysqli && docker-php-ext-enable pdo_mysql

# The official image already sets the user to www-data,

# but if it didn’t, you would add this:

# USER www-data

2. Build the new image:

Now, from your terminal, you build this image. We’ll find your host’s UID/GID and pass them as build arguments.

docker build . \

  –build-arg HOST_UID=$(id -u) \

  –build-arg HOST_GID=$(id -g) \

  -t my-custom-php-image:latest

  • $(id -u) automatically inserts your UID (e.g., 1000).
  • $(id -g) automatically inserts your GID (e.g., 1000).
  • -t my-custom-php-image “tags” the image with a friendly name.

3. Use the new image:

Finally, in your docker-compose.yml, instead of using image: php:8.2-fpm, you use your new custom-built image:

services:

  app:

    # Use your new custom image

    image: my-custom-php-image:latest

    # Or, if you want compose to build it for you:

    # build:

    #   context: .

    #   args:

    #     HOST_UID: ${UID}

    #     HOST_GID: ${GID}

    volumes:

      – ./wp-content:/var/www/html/wp-content

(If you use the build key, you’ll need an .env file. More on that in the next section.)

Now, the www-data user inside the container has UID 1000 and GID 1000. When it tries to write to your host’s ./wp-content directory (owned by UID 1000, GID 1000), the permissions match perfectly. No error.

As my colleague and web development expert Itamar Haim often says, “Syncing your UIDs is the cleanest, most portable way to solve Docker permissions. It respects the security model and makes your developer experience seamless.”

Solution 3: The “Easy” Way: Using docker-compose

This is my preferred method for local development because it’s fast and doesn’t require a custom Dockerfile if the image is designed for it.

What is Docker Compose?

If you’re not using it, you should be. Docker Compose is a tool for defining and running multi-container applications. This is my go-to when I’m spinning up a local WordPress environment, which needs a database (like MySQL) and a web server (like Apache/PHP) to work together. You define all your services in a single docker-compose.yml file.

The user Directive

Docker Compose has a simple directive called user that lets you override the default user for a container at runtime.

The strategy is to tell Docker Compose to run the container using your host’s UID and GID.

1. Create an .env file:

In the same directory as your docker-compose.yml, create a file named .env (that’s “dot-e-n-v”). This file will automatically set environment variables for Docker Compose.

# Get your current user’s UID and GID

UID=$(id -u)

GID=$(id -g)

Now, when you run docker-compose up, it will create two variables, UID and GID, with the value 1000 (or whatever your id is).

2. Edit your docker-compose.yml:

Now, in your docker-compose.yml, add the user directive to your service:

version: ‘3.8’

services:

  app:

    image: php:8.2-fpm

    # Add this line:

    user: “${UID}:${GID}”

    volumes:

      – ./my-app-code:/var/www/html

    # … rest of your service

  db:

    image: mysql:8.0

    # … rest of your db service

When you run docker-compose up, Compose will replace ${UID} and ${GID} with 1000 and 1000. The container will start, and its main process will run as user: “1000:1000”.

Just like that, the container process and your host directory owner are the same. Problem solved.

When This Works (and When It Fails)

This user directive is amazing, but it has one big “gotcha.”

  • It works if the container image is flexible. The official php and wordpress images work well with this.
  • It fails if the image is hard-coded to do things as a specific user. For example, if the nginx image’s startup script requires it to be the nginx user (UID 82) to read a config file, and you force it to run as UID 1000, it might fail with a different error.

In those cases, you have to fall back to Solution 2 (Dockerfile) or Solution 4 (Host Permissions). But for most web development stacks, especially PHP, this is the simplest and best local fix.

Solution 4: Modifying Permissions on the Host (The “Good Enough” Fix)

This solution is the reverse of Solution 2. Instead of changing the container’s user to match the host, you change the host’s directory to match the container’s user.

The chown Method

Let’s go back to our original problem:

  • Host folder owned by uid=1000.
  • Container process runs as uid=33 (www-data).

We can just chown (change owner) on our host directory to match the container’s user.

1. Find the container user’s UID/GID:

docker exec -it <container_name> id

# uid=33(www-data) gid=33(www-data)

2. Change your host directory ownership:

Warning: You will need sudo for this. This will make these files owned by a different user on your host machine.

sudo chown -R 33:33 ./my-app-code

This gives ownership of the ./my-app-code directory (and all files inside) to the user with ID 33. Now when the container’s www-data user (UID 33) tries to write to it, it has full permission.

The Downside: This is often annoying. Now, when you (UID 1000) try to edit those files in your code editor, you might get a “permission denied” error! Your editor will likely ask for your sudo password to save the file.

The setfacl Method (Linux Only)

A much more elegant “host-side” fix is to use Access Control Lists (ACLs). This lets you add permissions for a second user, without changing the original owner.

1. Install acl (if not already installed):

sudo apt-get update

sudo apt-get install acl

2. Add permissions for the container user (e.g., UID 33):

# -R = Recursive

# -m = Modify

# u:33:rwx = Give user 33 read-write-execute

# d:u:33:rwx = Give user 33 *default* r-w-x for new files/dirs

sudo setfacl -R -m u:33:rwx -m d:u:33:rwx ./my-app-code

This is the best of both worlds:

  • You (UID 1000) still own the files and can edit them easily.
  • The container user (UID 33) is also given full read/write/execute permissions.

Special Case: Docker on macOS and Windows (Docker Desktop)

If you’re on a Mac or Windows PC, you might be wondering, “Why doesn’t this happen to me all the time?”

Docker Desktop runs Docker inside a lightweight Linux virtual machine (VM). It has a file-sharing system (like VirtioFS) that automatically translates file ownership between your macOS/Windows user and the Linux VM.

For the most part, this “magic” works, and you never see permission errors.

When It Still Breaks: Sometimes, the translation isn’t perfect. Even on a Mac, I’ve seen a node_modules folder, created by npm install inside the container, become locked and un-deletable from my host. The principles are the same, but the “host” is now the VM, which can be harder to debug.

In these cases, the docker-compose user: “${UID}:${GID}” method (Solution 3) is almost always the most reliable fix.

My Local Development Workflow: A Practical Example

When I’m building a new Elementor site for a designer, I need a reliable, fast local environment. Here is the exact docker-compose.yml and .env setup I use to avoid all permission errors.

1. The .env file:

# Set the user and group to our host user

UID=$(id -u)

GID=$(id -g)

2. The docker-compose.yml file:

version: ‘3.8’

services:

  wordpress:

    image: wordpress:latest # Official WordPress image

    container_name: my_wp_site

    # Run the container as our host user

    user: “${UID}:${GID}”

    ports:

      – “8080:80” # Access site at http://localhost:8080

    environment:

      WORDPRESS_DB_HOST: db

      WORDPRESS_DB_USER: root

      WORDPRESS_DB_PASSWORD: password

      WORDPRESS_DB_NAME: wordpress

    volumes:

      # Map our local wp-content

      – ./wp-content:/var/www/html/wp-content

      # Map our custom plugins

      – ./plugins:/var/www/html/wp-content/plugins

      # Map our custom themes

      – ./themes:/var/www/html/wp-content/themes

  db:

    image: mysql:8.0

    container_name: my_wp_db

    environment:

      MYSQL_ROOT_PASSWORD: password

      MYSQL_DATABASE: wordpress

    volumes:

      – db_data:/var/lib/mysql

volumes:

  db_data:

This setup is perfect.

  1. I run docker-compose up -d.
  2. The wordpress container starts, running as my host user (UID 1000).
  3. I can edit theme and plugin files directly in ./plugins or ./themes on my host.
  4. When WordPress or Elementor tries to write to wp-content (to upload images or generate CSS files), it’s running as UID 1000 and writing to a directory owned by UID 1000.
  5. No permission errors. Ever.

The “Forget All This” Solution: Managed Environments

Look, I’m a web creator. My expertise is in building great user experiences with tools like Elementor Pro and optimizing conversions, not in Linux system administration.

Mastering Docker permissions is a powerful skill, but it’s also a deep rabbit hole that takes you away from your real work: building websites. Every hour you spend debugging a Dockerfile is an hour you’re not designing a landing page or optimizing a client’s site.

This is why, for many of my professional projects and almost all my client handoffs, I’ve moved to a fully managed platform.

Using a solution like Elementor Hosting abstracts away all this complexity. It provides an environment that is perfectly optimized for WordPress and Elementor right out of the box. I get top-tier performance from Google Cloud, integrated caching, and automatic security, all without ever having to think about a UID, GID, or chown command ever again.

It’s about choosing the right tool for the job. Docker is fantastic for isolated, complex, custom-stack testing. But for building and delivering professional WordPress sites, a managed, integrated platform lets me focus on what I do best.

Conclusion: You’ve Got This

The Docker “permission denied” error is a rite of passage. It’s frustrating, but it’s a symptom of a very simple and logical problem: a user ID mismatch.

You are now armed with all the solutions. You can diagnose the exact UIDs in conflict, and you can choose the right fix for your situation. For local development, I strongly recommend the docker-compose user directive (Solution 3). For building portable images, the Dockerfile modification (Solution 2) is the professional’s choice.

And if you’re just tired of dealing with infrastructure and want to get back to building, a managed solution is always a great option.

Frequently Asked Questions (FAQ)

1. What is the absolute fastest way to fix “permission denied”? Find the container user’s ID (docker exec <name> id) and run sudo chown -R <UID>:<GID> ./your-directory on your host. This gives the container user ownership. The downside is you might need sudo to edit those files on your host.

2. Is chmod 777 really that bad? Yes. It makes your files and directories writable by any user and any process on your system. On a web server, this is a critical security vulnerability that can lead to your site being hacked.

3. What’s the difference between a bind mount and a Docker volume? A bind mount links a directory from your host (e.g., ./my-code) into the container. You manage its permissions. A named volume (e.g., db_data in the example) is a directory managed by Docker itself. Docker handles all the permissions for named volumes, which is why you never have this error with a database’s data directory.

4. Why is my UID 1000? It’s a standard convention. On most modern desktop Linux distributions (like Ubuntu, Fedora) and macOS, the first non-system user created is assigned UID 1000. The second user is often 1001, and so on.

5. How do I find the UID of a user inside a container? Use docker exec -it <container_name> id -u <username>. For example: docker exec -it my_wp_site id -u www-data will likely return 33. If you just want to see the current user, docker exec -it <name> id is all you need.

6. Can I just run everything as root inside the container? You can (e.g., user: “0:0” in docker-compose.yml), but it’s a very bad security practice. This is called “root-in-container.” If a process inside your container is compromised, the attacker will have root-level privileges inside that container, giving them more power to do harm. Always run processes as a non-privileged user.

7. Does this permission problem happen on Docker for Mac? Rarely, but yes. Docker Desktop for Mac (and Windows) has a file-sharing system (VirtioFS) that does a lot of permission “magic” to prevent this. Most of the time, it works. When it fails, it’s usually because of complex file operations (like npm install), and the user: “${UID}:${GID}” fix is the best solution.

8. My docker-compose.yml user directive isn’t working. Why? The most likely reason is that the container image you’re using is not designed for it. The image’s entrypoint script or CMD might be hard-coded to run as a specific user or require permissions that your host user (UID 1000) doesn’t have. In this case, you must use the Dockerfile (Solution 2) or host permission (Solution 4) method.

9. What is the www-data user? www-data (on Debian/Ubuntu-based images) or apache (on RHEL/CentOS-based images) is the default, non-privileged user that Apache and PHP-FPM web servers use. It’s a security feature. By running as a user with limited permissions, a compromised web script can’t do (as much) damage to the rest of the system.

10. How does this all relate to file permissions in WordPress? It’s the exact same principle. When you get an error in the WordPress dashboard that it “cannot create directory” or “permission denied” when updating a plugin, it’s the same problem. Your web server (running as www-data) is trying to write to a directory (like wp-content/plugins) that is owned by a different user (like your FTP user, my-ftp-user). The fix is the same: chown -R www-data:www-data ./wp-content on the server. We are just applying that same logic to our local Docker setup.