Fix Gunicorn Socket Permission Denied (Nginx + Django)

Fix Permission denied on a Gunicorn Unix socket: socket ownership and mode, directory traversal, systemd RuntimeDirectory, umask, and SELinux.

Problem statement

Nginx returns 502 Bad Gateway, and its error log contains:

connect() to unix:/run/myapp/gunicorn.sock failed (13: Permission denied)
while connecting to upstream, client: 203.0.113.7, server: www.example.com,
request: "GET / HTTP/1.1", upstream: "http://unix:/run/myapp/gunicorn.sock:/"

Gunicorn is running and the socket file exists, but the Nginx worker process cannot open it. Error 13 is the operating system's EACCES: permission denied.

This is different from the neighbouring failures:

  • (2: No such file or directory): the socket path does not exist, usually because Gunicorn is not running or binds elsewhere
  • (111: Connection refused): something is listening on a TCP port but refusing, or the socket is stale with no process behind it
  • 403 Forbidden on static files: a filesystem permission problem in the static file path rather than the socket

Unix sockets are files. Access depends on the socket's owner, group, and mode, and on execute (traverse) permission for every directory in its path. Getting those right once, in the systemd unit, makes the problem permanent rather than something you re-fix after every reboot.

Reaching a Unix socketNginx workerwww-dataSocket directoryneeds traverseSocket filegroup writeGunicornowns the socketBoth the directory and the socket file must be reachable by the Nginx user.

Quick answer

  1. Read the exact socket path from the Nginx error log.
  2. Check the socket: ls -l /run/myapp/gunicorn.sock and every parent directory with namei -l.
  3. Run Gunicorn as a dedicated app user with Group=www-data (the Nginx user's group on Debian and Ubuntu) and --umask 007, so the socket is group-accessible.
  4. Create the socket's directory with RuntimeDirectory= in the systemd unit, so it exists after every reboot with the right ownership.
  5. Confirm the Nginx worker user matches the socket's group.
  6. On RHEL-family systems, check SELinux denials.
  7. Reload both services and verify with a request through Nginx.

Step-by-step solution

1. Confirm the failing path and the two users involved

sudo tail -n 20 /var/log/nginx/error.log | grep "Permission denied"
ps -o user,cmd -C nginx | sort -u | head -3
ps -o user,group,cmd -C gunicorn | sort -u | head -3

The Nginx master runs as root, but its workers run as www-data (Debian and Ubuntu) or nginx (RHEL family). The worker user is the one that must reach the socket.

2. Inspect the socket and its path

ls -l /run/myapp/gunicorn.sock
namei -l /run/myapp/gunicorn.sock
the socket and the directory both matterdrwxr-x---myapp www-data /run/gunicornsrwxrwx---myappwww-data/run/gunicorn/gunicorn.sock1231www-data may enter the directory (x).2Connecting needs write on the socket.3Nginx’s group, shared with Gunicorn.Nginx needs execute on the directory and write on the socketdjango-deployment.com
sudo -u www-data test -w on the socket checks it directly.

A working setup looks like this:

srw-rw---- 1 myapp www-data 0 Sep 17 09:12 /run/myapp/gunicorn.sock
  • owner myapp: the application user that created it
  • group www-data: the Nginx worker's group
  • mode srw-rw---- (660): owner and group can read and write, others cannot

namei -l shows every directory on the path. Each one needs at least execute permission for www-data:

f: /run/myapp/gunicorn.sock
drwxr-xr-x root  root     /
drwxr-xr-x root  root     run
drwxr-xr-x myapp www-data myapp
srw-rw---- myapp www-data gunicorn.sock

If a directory shows drwx------, Nginx cannot enter it regardless of the socket's own mode.

3. Test as the Nginx user

sudo -u www-data test -r /run/myapp/gunicorn.sock && echo "readable" || echo "NOT readable"
sudo -u www-data python3 -c "
import socket
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(3)
s.connect('/run/myapp/gunicorn.sock')
print('connected')
s.close()
"

A successful connection here proves the permission problem is solved; a failure reproduces exactly what Nginx sees, without restarting anything.

4. Fix the systemd unit (the durable fix)

[Unit]
Description=Gunicorn for myapp
After=network.target

[Service]
Type=notify
User=myapp
Group=www-data
WorkingDirectory=/srv/myapp/app
EnvironmentFile=/etc/myapp/env
RuntimeDirectory=myapp
RuntimeDirectoryMode=0750
ExecStart=/srv/myapp/venv/bin/gunicorn config.wsgi:application \
    --bind unix:/run/myapp/gunicorn.sock \
    --umask 007 \
    --workers 3 \
    --access-logfile -
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure

[Install]
WantedBy=multi-user.target
the durable fix lives in the unitValueUser=myappGroup=✓www-data (Nginx’s group)RuntimeDirectory=gunicorn → /run/gunicornRuntimeDirectoryMode=0750UMask=✓0007: socket gets group writechmod by hand is undone at the next restartdjango-deployment.com
On RHEL-family systems the Nginx group is nginx, not www-data.

Why each line matters:

  • User=myapp: the application runs as its own unprivileged user. Do not run Gunicorn as www-data; that gives the application the web server's identity and access to everything the web server can read.
  • Group=www-data: the socket inherits this group, so Nginx workers can connect through group permission.
  • RuntimeDirectory=myapp: systemd creates /run/myapp on start, owned by User and Group, and removes it on stop. /run is a tmpfs recreated at boot, which is why a manually created directory disappears after a reboot.
  • RuntimeDirectoryMode=0750: owner full access, group traverse and read, others nothing.
  • --umask 007: Gunicorn creates the socket with mode 770 masked to 660, so the group can use it. Without it, Gunicorn's default umask can produce a socket others can also reach, or, combined with a restrictive process umask, one the group cannot.

Apply the change:

sudo systemctl daemon-reload
sudo systemctl restart myapp
ls -l /run/myapp/gunicorn.sock

5. Point Nginx at the same path

upstream myapp {
    server unix:/run/myapp/gunicorn.sock;
}

server {
    listen 80;
    server_name www.example.com;

    location / {
        proxy_pass http://myapp;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
sudo nginx -t && sudo systemctl reload nginx

The path in proxy_pass must match --bind exactly, including the directory.

6. SELinux on RHEL, Rocky, Alma, and Fedora

If permissions look correct and the error persists, SELinux may be denying the connection:

getenforce
sudo ausearch -m avc -ts recent | grep -i nginx | tail -5

A denial mentioning unix_stream_socket and httpd_t means the socket's label is not one Nginx may use. Options:

# Allow the web server to connect to network/socket resources
sudo setsebool -P httpd_can_network_connect 1

# Label the socket directory as writable web content
sudo semanage fcontext -a -t httpd_var_run_t "/run/myapp(/.*)?"
sudo restorecon -Rv /run/myapp

Keep SELinux enforcing; labelling is the correct fix, not disabling it.

7. Verify

curl -sI --unix-socket /run/myapp/gunicorn.sock http://localhost/ -H "Host: www.example.com" | head -1
curl -sI http://127.0.0.1/ -H "Host: www.example.com" | head -1
sudo grep -c "Permission denied" /var/log/nginx/error.log
sudo systemctl restart myapp && sleep 2 && ls -l /run/myapp/gunicorn.sock

Verification check:

  • a request directly to the socket returns an HTTP status line
  • the same request through Nginx returns the same status
  • no new Permission denied entries appear in the Nginx error log
  • after a service restart, the socket is recreated with owner myapp, group www-data, mode 660
  • after a full reboot, the site comes back without manual intervention

Explanation

A Unix domain socket is a filesystem object with an owner, a group, and a mode, and the kernel checks those permissions when a process calls connect(). Nginx workers therefore need either group access to the socket (the setup above) or world access (which would let any local process talk to your application server). Because /run is wiped on every boot, the durable answer is to let systemd create the directory and let Gunicorn create the socket with the right group and umask, instead of fixing permissions by hand after each restart.

Edge cases / notes

The socket file is missing entirely

ls reports "No such file or directory", and Nginx logs error 2 rather than 13. Gunicorn is not running, failed to start, or binds to a different path; check systemctl status and the journal.

Sockets in the project directory

Older tutorials place the socket in /srv/myapp or a home directory. That works but makes permissions depend on the project directory's mode, and the socket can survive crashes as a stale file. /run managed by systemd avoids both problems.

Containers

In Docker, sharing a Unix socket between an application container and an Nginx container requires a shared volume and matching numeric UIDs or GIDs, because usernames do not map across containers. Many teams simply use a TCP port on the internal network instead.

Sockets versus TCP

permissions exist only for socketsUnix socketfile permissions control access✓no port for anyone to reachgroup and mode must be rightTCP 127.0.0.1:8000✓no file permissions at allany local process can connect✓simplest in containersSwitching to TCP removes this error class, with its own trade-offdjango-deployment.com
Either is fine on a single server; consistency matters more.

Unix sockets avoid the TCP stack and are slightly faster for same-host communication; a local TCP port (127.0.0.1:8000) is easier to test with curl and works across containers. Either is fine for production.

Common mistakes

  • Running Gunicorn as www-data to "fix" permissions, which gives the app the web server's identity.
  • Running chmod 777 on the socket or its directory, allowing any local process to reach the application.
  • Creating /run/myapp by hand; it disappears on the next reboot.
  • Changing proxy_pass but not --bind (or the reverse), so the paths no longer match.

Next steps

FAQ

Why does Nginx get permission denied on the Gunicorn socket?

The Nginx worker user is not the socket's owner and is not in its group, or a directory on the socket's path does not grant it execute permission.

Should Gunicorn run as www-data?

No. Run it as a dedicated application user and set Group=www-data so the socket is group-accessible. Sharing the web server's user gives the application more access than it needs.

What socket permissions should Gunicorn use?

Mode 660 with the application user as owner and the Nginx group as group. Gunicorn's --umask 007 produces that from its default socket creation mode.

Why does the socket disappear after a reboot?

/run is a temporary filesystem recreated at boot. Use RuntimeDirectory= in the systemd unit so the directory is created automatically with the correct ownership each time the service starts.

Keep going