uWSGI Emperor Mode: Run Multiple Apps on One Host
uWSGI Emperor lets you run multiple isolated Python apps on one host through a single file-watched master, with per-app users and graceful reloads triggered by touching config files.
06 Aug 2025, 22:32 UTC

The Problem: One systemd Unit Per App Does Not Scale
You run a Django application, a FastAPI service, and a small internal dashboard on the same virtual machine. Each needs its own virtualenv, socket, worker count, and system user. The default approach is one systemd unit per app, each launching its own uWSGI master. That works for two apps, but adding a third means new unit files, daemon-reload, and more processes to monitor. Worse, if every unit runs as www-data, a compromise or memory leak in one app affects them all.
uWSGI Emperor mode treats multi-app hosting as a file-management problem instead of a service-management problem. A single privileged master watches a directory of vassal configuration files. Each file defines an independent application with its own uid/gid, workers, virtualenv, and resource limits. Add a file, the vassal starts. Touch it, the vassal reloads. Delete it, the vassal stops. The Emperor itself never needs restarting for routine deploys.
How Emperor Mode Works
The Emperor is a uWSGI master process that monitors one or more directories for vassal config files (.ini, .yaml, or .json). When it sees a new file, it spawns a vassal uWSGI master for that application. Because the Emperor starts as root, it can switch to a per-vassal user and group before the application code loads. This requires CAP_SETUID and CAP_SETGID; running the Emperor unprivileged forces every vassal to share the same user, which removes the isolation benefit.
Global Emperor options apply to the Emperor process itself. Vassals inherit some settings but can override them locally. The Emperor logs lifecycle events—spawn, reload, reap—to its own stream, while each vassal handles its application logs independently.
Worked Example: Two Apps, One Emperor
Assumptions: a Linux host, uWSGI 2.x installed, and two Python apps. Confirm your version with uwsgi --version; do not assume a specific patch release is current.
1. Emperor systemd unit
Run as root so the Emperor can drop privileges per vassal. Type=simple is used here because Type=notify requires confirming that your uWSGI build sends systemd readiness notifications; --emperor-procname only sets the process name and does not enable notify behavior.
# /etc/systemd/system/uwsgi-emperor.service
[Unit]
Description=uWSGI Emperor
After=network.target
[Service]
Type=simple
User=root
Group=root
ExecStart=/usr/local/bin/uwsgi \
--emperor /etc/uwsgi/vassals \
--emperor-procname uwsgi-emperor \
--master \
--logto /var/log/uwsgi-emperor.log \
--pidfile /run/uwsgi-emperor.pid
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
2. Shared socket directory
Vassals bind sockets here. Give vassal users group access so they can write to the directory without making it world-writable.
# Run as root
mkdir -p /run/uwsgi
chown root:www-data /run/uwsgi
chmod 0770 /run/uwsgi
Add each vassal system user to the www-data group, or use per-vassal socket directories with matching ownership.
3. Vassal configurations
Create /etc/uwsgi/vassals/django-api.ini:
[uwsgi]
chdir = /opt/apps/django-api
module = config.wsgi:application
venv = /opt/venvs/django-api
uid = 1001
gid = 1001
processes = 4
threads = 2
max-requests = 1000
max-requests-delta = 50
socket = /run/uwsgi/django-api.sock
chmod-socket = 660
vacuum = true
# Limits virtual address space, not resident memory.
limit-as = 512
Create /etc/uwsgi/vassals/flask-admin.ini:
[uwsgi]
chdir = /opt/apps/flask-admin
module = app:create_app
venv = /opt/venvs/flask-admin
uid = 1002
gid = 1002
processes = 2
threads = 1
socket = /run/uwsgi/flask-admin.sock
chmod-socket = 660
vacuum = true
limit-as = 256
Note that module = app:create_app omits parentheses; uWSGI imports the callable named create_app, not the result of calling it.
4. Start, verify, and reload
- Create the vassal users and application directories as root.
- Start the Emperor:
systemctl daemon-reload systemctl enable --now uwsgi-emperor - Drop the vassal
.inifiles into/etc/uwsgi/vassals/. The Emperor log should show each vassal being spawned. Check with:tail -f /var/log/uwsgi-emperor.log - Verify process isolation. The Emperor master runs as root; vassals should run as their configured users:
Then inspect a vassal process:ps -eo pid,user,group,cmd | grep uwsgi
The numeric Uid/Gid should match the vassal config.cat /proc/<vassal-pid>/status | grep -E '^(Uid|Gid):' - Zero-downtime deploy: update application code, then touch the vassal config:
The Emperor sends SIGHUP to the vassal master, which gracefully reloads workers. This is not SIGTERM; SIGTERM stops the process, while SIGHUP triggers the configured graceful reload behavior.touch /etc/uwsgi/vassals/django-api.ini
Trade-offs and Limitations
- Privilege requirement: Per-vassal uid/gid isolation requires root or
CAP_SETUIDandCAP_SETGID. Without these, all vassals share the Emperor’s user. - Emperor-level settings are not vassal aggregates:
--logto,--pidfile, and--statsset by the Emperor apply to the Emperor process. A vassal that sets its ownpidfileto the same path will conflict. Use unique vassal stats sockets if you need per-app metrics. - Restart loops: A vassal that crashes on startup is respawned. Check your uWSGI version’s documentation for Emperor retry controls to throttle rapid failures.
limit-ascaveat: This limits virtual address space, not RSS. Python workers with large heaps can hit the limit before resident memory becomes a concern.- No built-in service discovery: Emperor does not provide load balancing, DNS, or inter-vassal communication. Shared state requires an external system such as Redis, a database, or uWSGI’s own cache/rpc facilities.
- Single point of failure: If the Emperor master dies, its vassals are reaped. Run the Emperor under systemd with
Restart=on-failure.
Closing Checklist
- Run
uwsgi --versionand confirm you are on a supported 2.x release. - Test with a single vassal first; verify
/proc/<pid>/statusshows the correct Uid/Gid. - Ensure vassal users can write to the socket directory.
- Use
touchon a vassal config for graceful reloads, and confirm the reload appears in the Emperor log. - Assign unique stats sockets per vassal if per-app metrics are required.
- Configure log rotation for the Emperor log and any vassal-specific logs.
- Document the vassal directory path and naming convention for your deployment pipeline.
Emperor mode is a good fit when you manage several Python applications on one host and want per-app isolation without maintaining a systemd unit for every service. It scales until you need cross-host scheduling, at which point the vassal configs remain portable while the orchestration layer changes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.