Recently I heard about Nginx Unit, a piece of software which lets you run Python web application on production with less components. It's like you let Nginx run your Python code, no longer Nginx - Gunicorn separation. So I installed it and learned how to use, but the default setup is not convenient: You have to switch to root
user too often. So I share extra steps of setup to save you from it.
Nginx Unit is not configured via file like Nginx. It is done via HTTP API, which is nice because Nginx Unit can get update with new configuration without restarting, although I don't know what is the benefit over using SIGHUP signal to trigger rereading. The API communication is done via Unix domain socket, leveraging Linux authentication for restricting access, which is very reasonable because we don't have to remember port, username and password. But the issue is that, the default setup makes the socket writable by root only.
The annoyance come from the combination of these factors:
-
Very few CLI HTTP clients support Unix domain socket: I only know cURL and HTTPie + plugin. I don't like cURL's verbose syntax, I pick HTTPie for its neat syntax and colored JSON display.
-
The HTTPie distributed by Ubuntu repo does not include the plugin for Unix socket. So I have to install via
pipx
. -
When invoking HTTPie with
sudo
,sudo
doesn't findHTTPie
because the later is installed to my user home folder (~/.local/bin) bypipx
.
If you have the same taste as me, we will do extra setup steps to make the socket writable for our user. Concretely, we will make the socket file is under adm
group and add ourselves to adm
group. This adm
group pre-exists in Debian/Ubuntu system. I'm not sure about other distro.
To add ourselves to adm
group:
$ sudo adduser $USER adm
We will need to log out and login again to make the command about effective.
Now the socket. Let's check the systemd unit file for launching Nginx Unit (/usr/lib/systemd/system/unit.service):
[Unit]
Description=NGINX Unit
Wants=network-online.target
After=network-online.target
[Service]
Type=forking
PIDFile=/var/run/unit.pid
EnvironmentFile=-/etc/default/unit
ExecStart=/usr/sbin/unitd $DAEMON_ARGS
ExecReload=
[Install]
WantedBy=multi-user.target
We can see that, this file tells systemd to read the $DAEMON_ARGS
variable from the /etc/default/unit and use it as arguments for the unitd
command. Now, check what unitd
can accept:
$ unitd --help
unit options:
--version print unit version and configure options
--no-daemon run unit in non-daemon mode
--control ADDRESS set address of control API socket
default: "unix:/var/run/control.unit.sock"
--control-mode MODE set mode of the control API socket
default: 0600
--control-user USER set the owner of the control API socket
--control-group GROUP set the group of the control API socket
--pid FILE set pid filename
default: "/var/run/unit.pid"
--log FILE set log filename
default: "/var/log/unit.log"
--modulesdir DIR set modules directory name
default: "/usr/lib/unit/modules"
--statedir DIR set state directory name
default: "/var/lib/unit"
--tmpdir DIR set tmp directory name
default: "/var/tmp"
--user USER set non-privileged processes to run as specified user
default: "unit"
--group GROUP set non-privileged processes to run as specified group
default: "unit"
Ok, the --control-mode
and --control-group
options are what we want. We will create the /etc/default/unit file with this content:
DAEMON_ARGS=--control-mode 660 --control-group adm
Restart the systemd service and check the file permission:
$ sudo systemctl restart unit.service
Try calling the API:
❯ https $'http+unix://('/run/control.unit.sock' | url encode -a)/status'
Output
HTTP/1.1 200 OK
Connection: close
Content-Length: 274
Content-Type: application/json
Date: Fri, 09 May 2025 23:31:45 GMT
Server: Unit/1.34.2
{
"applications": {},
"connections": {
"accepted": 0,
"active": 0,
"closed": 0,
"idle": 0
},
"modules": {
"python": {
"lib": "/usr/lib/unit/modules/python3.12.unit.so",
"version": "3.12.3"
}
},
"requests": {
"total": 0
}
}
It works!
You may notice that the command line above is unusual. It is the Nushell syntax. I switched to Nushell to easily convert the socket file path to the URL form that HTTPie's plugin requires (percent encoding). And because there is a built-in command http
in Nushell, I use https
to make Nushell invoke HTTPie instead of its built-in command.