Nginx Permission Denied: Resolving Unix Socket Access on macOS Local Dev

Troubleshoot Nginx worker process 'permission denied' errors when opening Unix sockets on macOS, often due to user/group mismatches or incorrect socket permissions.


Troubleshoot Nginx worker process 'permission denied' errors when opening Unix sockets on macOS, often due to user/group mismatches or incorrect socket permissions.

When developing web applications locally on macOS, encountering "permission denied" errors related to Nginx and Unix sockets can be a frustrating roadblock. This error typically signifies that your Nginx web server, acting as a frontend proxy, cannot establish a connection with your backend application (such as PHP-FPM, Gunicorn for Python, or a Node.js process) via a Unix domain socket. This guide will meticulously detail the root causes and provide expert, step-by-step resolutions applicable to both Homebrew-managed setups and Dockerized environments on macOS.

Symptom & Error Signature

The most common symptom is your web application failing to load, displaying a "502 Bad Gateway" or similar error in the browser. In your Nginx error logs, you will find entries similar to these:

2023/10/26 10:30:15 [crit] 12345#0: *1 connect() to unix:/usr/local/var/run/php-fpm.sock failed (13: Permission denied) while connecting to upstream, client: 127.0.0.1, server: localhost, request: "GET /index.php HTTP/1.1", upstream: "fastcgi://unix:/usr/local/var/run/php-fpm.sock:", host: "localhost"

Or, if using a different backend like Gunicorn:

2023/10/26 10:30:15 [crit] 12345#0: *1 connect() to unix:/tmp/gunicorn.sock failed (13: Permission denied) while connecting to upstream, client: 127.0.0.1, server: myapp.local, request: "GET / HTTP/1.1", upstream: "http://unix:/tmp/gunicorn.sock:", host: "myapp.local"

You might also see these messages in your system logs or application-specific logs, though the Nginx error log is the primary indicator for this specific issue.

Root Cause Analysis

The "Permission denied (13)" error when Nginx tries to connect to a Unix socket points to a fundamental access control problem. Here's a breakdown of the underlying reasons:

  1. Nginx Worker Process User Mismatch: By default, Nginx worker processes run under a specific user (e.g., _www on macOS Homebrew installations, nginx or www-data in Linux/Docker environments). If this user does not have sufficient read/write permissions to the Unix socket file created by the backend application, Nginx cannot connect.
  2. Incorrect Socket File Permissions: The Unix socket file itself might have restrictive permissions (e.g., 0600) that prevent users other than its creator from accessing it.
  3. Parent Directory Permissions: Even if the socket file has correct permissions, Nginx might be denied access if the directory containing the socket (e.g., /usr/local/var/run/, /var/run/, /tmp/) does not grant execute permissions to the Nginx worker process user. Execute permission on a directory allows listing its contents and traversing into it.
  4. Backend Application User Mismatch: The backend application (e.g., php-fpm, gunicorn, uwsgi) might be configured to run as a user different from Nginx and create the socket with restrictive permissions, leading to the access denial.
  5. Socket Path Mismatch: While less common for "permission denied" specifically, a misconfigured socket path in either Nginx or the backend application can indirectly contribute to issues. Ensure both configurations point to the exact same socket file.

Step-by-Step Resolution

The resolution involves ensuring that the Nginx worker process has the necessary permissions to access the Unix socket. We'll cover common scenarios for macOS.

1. Verify Nginx Worker Process User

First, identify the user Nginx is running as. This user needs read and write access to the socket.

On macOS (Homebrew Nginx):

ps aux | grep nginx | grep 'worker process'

Typically, Nginx installed via Homebrew runs its worker processes as the _www user.

In Dockerized Environments (within Nginx container):

docker ps
# Find your Nginx container ID or name, e.g., myapp_nginx_1
docker exec -it myapp_nginx_1 ps aux | grep nginx | grep 'worker process'

The user might be nginx, www-data, or even root (though root is discouraged for worker processes). Note this user.

2. Identify Backend Socket Path, Permissions, and User

Next, determine where your backend application is creating the Unix socket, what its permissions are, and which user/group creates it.

For PHP-FPM (Homebrew on macOS or within Docker container):

  1. Locate PHP-FPM configuration:

    • Homebrew: Typically $(brew --prefix)/etc/php/<php_version>/php-fpm.d/www.conf
    • Docker: Often /etc/php/<php_version>/fpm/pool.d/www.conf or /usr/local/etc/php-fpm.d/www.conf inside the PHP-FPM container.
  2. Find the listen directive: This specifies the socket path.

    ; Example from www.conf
    listen = /usr/local/var/run/php-fpm.sock
    ; or for Docker
    listen = /var/run/php-fpm/php-fpm.sock
    
  3. Find listen.owner, listen.group, and listen.mode: These directives control the ownership and permissions of the created socket.

    ; Example from www.conf
    listen.owner = _www
    listen.group = _www
    listen.mode = 0660 ; This means owner and group can read/write, others no access
    
    • On Homebrew, _www is typical for both Nginx and PHP-FPM.
    • In Docker, www-data is common.
  4. Verify actual socket status:

    ls -la /usr/local/var/run/php-fpm.sock # Or your specific socket path
    

    Examine the owner, group, and permissions (e.g., srw-rw----). If the Nginx user is not the owner or in the group, and listen.mode is restrictive (like 0660), you have found the issue.

For Gunicorn/uWSGI (Python apps):

The socket path and permissions are typically configured in your Gunicorn/uWSGI startup command or configuration file.

  • Gunicorn: Look for --bind unix:/path/to/socket.sock --user <user> --group <group>
  • uWSGI: Look for socket = /path/to/socket.sock and chown-socket = <user>:<group> or chmod-socket.

3. Adjust Socket File Permissions and Ownership

This is the core fix. The goal is to ensure the Nginx worker process user can read and write to the socket.

Option A: For Homebrew-Managed Nginx and PHP-FPM on macOS

The most robust solution is to ensure PHP-FPM creates the socket with permissions that Nginx can access.

  1. Edit PHP-FPM pool configuration (www.conf): Open $(brew --prefix)/etc/php/<php_version>/php-fpm.d/www.conf in your preferred editor (e.g., nano or vi).

    ; Find these lines or add them if missing
    listen.owner = _www
    listen.group = _www
    listen.mode = 0660
    

    Ensure listen.owner and listen.group match the Nginx worker process user identified in Step 1. For Homebrew, this is typically _www. listen.mode = 0660 grants read/write access to the owner (_www) and group (_www), which is suitable if Nginx also runs as _www.

  2. Check Parent Directory Permissions: Verify that the directory containing the socket (e.g., /usr/local/var/run/) allows the _www user to traverse it.

    ls -ld /usr/local/var/run/
    

    Output should show something like drwxr-xr-x for _www owner. If not, adjust:

    sudo chmod 755 /usr/local/var/run/
    sudo chown _www:_www /usr/local/var/run/ # Only if ownership is incorrect
    
  3. Restart Services: After making changes, restart both PHP-FPM and Nginx to apply them.

    brew services restart php
    brew services restart nginx
    
Option B: For Dockerized Environments on macOS

This involves configuring permissions within your Docker containers and potentially managing shared volume permissions.

  1. Match Nginx and Backend Users: The most common and secure approach is to configure both your Nginx container and your backend application container to use the same user (e.g., www-data or nginx) when interacting with the socket.

    Example php-fpm.conf or www.conf (inside PHP-FPM container):

    listen = /var/run/php-fpm/php-fpm.sock
    listen.owner = www-data # Or 'nginx' if that's Nginx's user
    listen.group = www-data # Or 'nginx'
    listen.mode = 0660
    

    Example nginx.conf (inside Nginx container):

    user www-data; # Or 'nginx' to match PHP-FPM
    # ...
    server {
        # ...
        location ~ .php$ {
            fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
            # ...
        }
    }
    
  2. Adjust Permissions in Dockerfile or Entrypoint Script: Ensure the directory where the socket is created exists and has correct permissions inside the container. This is often done in the Dockerfile for the PHP-FPM image.

    # Dockerfile for PHP-FPM
    FROM php:8.2-fpm-alpine
    # ...
    RUN mkdir -p /var/run/php-fpm && 
        chown www-data:www-data /var/run/php-fpm && 
        chmod 755 /var/run/php-fpm
    # ...
    
  3. Docker Compose Configuration: If you're using Docker Compose, ensure any shared volumes that might contain the socket path are correctly handled. For local development on macOS, Docker Desktop uses a virtualized Linux environment, so permissions inside containers are generally independent of the macOS host, unless you're mounting a host directory directly where the socket would reside.

    # docker-compose.yml
    version: '3.8'
    services:
      nginx:
        image: nginx:stable-alpine
        volumes:
          - ./nginx.conf:/etc/nginx/nginx.conf:ro
          # No need to share /var/run for socket if within container
        ports:
          - "80:80"
      php-fpm:
        image: php:8.2-fpm-alpine
        volumes:
          - ./app:/var/www/html # Your application code
          - ./php-fpm.conf:/usr/local/etc/php-fpm.d/www.conf # Mount custom PHP-FPM config
        # Ensure PHP-FPM runs as www-data user to create socket
        user: www-data
    

    Using the user directive in docker-compose.yml can explicitly define the user a service runs as, affecting the ownership of files it creates, including sockets.

  4. Temporary listen.mode (for debugging): If you're still stuck in a Docker environment, as a temporary debugging step, you can set listen.mode = 0666 or 0777 in www.conf. This makes the socket globally writable. If this fixes the issue, you know it's purely a permissions problem. Immediately revert this and fix the ownership/group setup properly for security.

    listen.mode = 0666 ; WARNING: DO NOT USE IN PRODUCTION!
    

    Setting listen.mode = 0666 or 0777 for a Unix socket grants write access to all users on the system, which is a significant security risk. Only use this for temporary debugging and ensure you revert it immediately. The proper fix is to match user/group ownership.

  5. Restart Docker Compose Services:

    docker-compose restart
    
Option C: Linux Server Environments (e.g., in a VM on macOS)

If your "local environment" on macOS involves a Linux VM (e.g., Vagrant, Multipass) where Nginx and PHP-FPM are installed directly, you'll use standard Linux commands.

  1. Edit PHP-FPM pool configuration (www.conf): Typically /etc/php/<php_version>/fpm/pool.d/www.conf.

    listen = /var/run/php/php<php_version>-fpm.sock
    listen.owner = www-data
    listen.group = www-data
    listen.mode = 0660
    

    In most Debian/Ubuntu systems, Nginx runs as www-data and PHP-FPM also runs as www-data, making listen.owner = www-data and listen.group = www-data the correct configuration.

  2. Check Parent Directory Permissions:

    ls -ld /var/run/php/
    sudo chown www-data:www-data /var/run/php/ # If necessary
    sudo chmod 755 /var/run/php/ # If necessary
    
  3. Restart Services with systemctl:

    sudo systemctl restart php<php_version>-fpm.service
    sudo systemctl restart nginx.service
    

4. Verify Nginx Configuration

Double-check your Nginx server block configuration to ensure the fastcgi_pass (or proxy_pass for Gunicorn/uWSGI) directive points to the correct Unix socket path.

# Nginx site configuration (e.g., /usr/local/etc/nginx/servers/myapp.conf or within Docker container)
server {
    listen 80;
    server_name localhost; # or your domain
    root /path/to/your/webapp;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ .php$ {
        fastcgi_pass unix:/usr/local/var/run/php-fpm.sock; # Ensure this path is correct!
        # For Docker, it might be unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

After any changes to Nginx configuration, always test the syntax:

nginx -t

If nginx -t reports success, reload Nginx:

brew services reload nginx # For Homebrew on macOS
# OR
sudo nginx -s reload # If Nginx is running directly on macOS
# OR
docker exec -it myapp_nginx_1 nginx -s reload # For Docker

5. Confirm Resolution

After applying the changes and restarting services, refresh your web application in the browser. Check the Nginx error logs again (/usr/local/var/log/nginx/error.log for Homebrew, or docker logs <nginx_container>) to ensure the "permission denied" error is no longer appearing.

By systematically verifying user permissions, socket ownership, directory access, and configuration paths, you can effectively resolve the Nginx "permission denied opening unix socket" error on your macOS local development environment.