When you fire up a heavy build, a data‑scraping job, or a local web server inside a tmux session, the usual pattern is to detach and log out. tmux will happily keep the session alive as long as your login shell stays around, but as soon as you hit logout or PAM times you out, the whole tree dies. Wrapping tmux in a systemd unit makes it immune to those surprises and gives you the same tooling you already use for other services—logging, restart policies, limits, and isolation.
Why not just detach?
Detaching is as easy as:
tmux new-session -d -s build 'make -j$(nproc)'
But that trick only works while your shell is alive. If the user logs out, the session closes. nohup or screen -dm can help, but they’re clunky and lack the fine‑grained control that systemd offers. With a unit you can:
- Set a
Restart=policy (e.g.on-failure). - Log everything straight to
journalctl. - Run the session as a specific user or group.
- Enforce limits (
MemoryMax=,CPUQuota=). - Use socket activation to start tmux on demand.
Choosing the right unit type
There are two common ways to run tmux under systemd:
System‑wide unit (
/etc/systemd/system/tmux-build.service).
Runs as root or a dedicated system user. Good for services that must start at boot or be shared across users.User unit (
~/.config/systemd/user/tmux-build.service).
Runs in the user’s session, inherits that user’s environment, and stops automatically when the user logs out. This is usually the simplest way to keep a single user’s long‑running job alive.
For most home‑lab or dev‑ops setups, a user unit does the job. If you need the job to start before anyone logs in, use a system unit and systemd‑run to bind it to a specific user.
Building a user unit
Create ~/.config/systemd/user/tmux-build.service:
[Unit]
Description=tmux session for long‑running build
After=network-online.target
[Service]
Type=exec
User=%i
Environment=TERM=xterm-256color
ExecStart=/usr/bin/tmux new-session -d -s %i 'make -j$(nproc)'
ExecStop=/usr/bin/tmux kill-session -t %i
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
# Optional limits
# MemoryMax=2G
# CPUQuota=50%
[Install]
WantedBy=default.target
Key directives, broken down
| Directive | Why it matters |
|---|---|
Type=exec | Tells systemd that the command will replace the ExecStart process; tmux becomes the main PID. |
ExecStart= | Starts tmux detached (-d) with a named session (-s %i). The %i placeholder expands to the instance name you’ll use when launching the unit. |
ExecStop= | Kills the tmux session cleanly when the unit stops. |
Restart= | Keeps the session alive if it crashes. |
StandardOutput/StandardError=journal | Sends all tmux output straight to the journal, reachable via journalctl -u tmux-build@user. |
MemoryMax= / CPUQuota= | Optional cgroup limits to keep runaway jobs from eating the host. |
Start it up
systemctl --user daemon-reload
systemctl --user enable --now tmux-build@alice.service
Swap alice for whatever user you’re targeting. The @ syntax lets you spin up multiple instances for different users or projects.
Check the status
systemctl --user status tmux-build@alice.service
journalctl -u tmux-build@alice.service -f
You’ll see the tmux session in ps and the build logs in the journal.
Using systemd‑run for ad‑hoc jobs
If you only need a one‑off long‑running task, systemd-run can spin a transient unit that runs tmux:
systemd-run --user --unit=tmp-build --scope \
/usr/bin/tmux new-session -d -s tmp-build 'python3 long_script.py'
--scope creates a transient scope unit that inherits the user’s environment but doesn’t leave a permanent unit file. Handy for quick experiments.
Security considerations
| Risk | Mitigation |
|---|---|
| Privilege escalation | Run the unit as a dedicated low‑privilege user. Avoid User=root. |
| Untrusted input | Don’t inject shell‑expanded variables directly into ExecStart=. Use -- to separate options from the command. |
| Resource abuse | Set MemoryMax= and CPUQuota= to cap runaway jobs. |
| Logging sensitive data | If the job outputs secrets, redirect output to a protected file or use StandardOutput=null. |
| Session hijacking | Run inside the user’s cgroup hierarchy; systemd‑user isolation limits what the session can see. |
Because tmux inherits the user’s environment, be careful with env vars that may contain secrets (e.g. AWS_ACCESS_KEY_ID). If you need credentials, use EnvironmentFile= pointing to a file owned by the user with 0600 permissions.
Trade‑offs compared to plain tmux
| Feature | tmux alone | tmux + systemd |
|---|---|---|
| Automatic restart | No | Yes (configurable) |
| Centralized logging | Manual tee or script | journalctl |
| Resource limits | Manual ulimit | cgroup limits |
| Startup on boot | No | Yes (system unit) |
| User‑session isolation | No | Yes (user unit) |
| Complexity | Low | Moderate (unit file + daemon‑reload) |
If you’re only running a handful of long‑running jobs, the extra setup may feel like overkill. For production or shared environments, the benefits usually outweigh the modest added complexity.
Common pitfalls and troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Unit fails to start: “Failed to start tmux build” | tmux not in $PATH for the user unit | Use absolute path (/usr/bin/tmux) or set Environment=PATH=/usr/bin:/usr/local/bin |
| Logs missing | StandardOutput=journal not set | Add the directive or use StandardOutput=syslog |
| Session dies on logout | Using a system unit without User= | Switch to a user unit or add RemainAfterExit=yes |
| Resource limits not applied | cgroup limits require systemd 247+ | Verify systemd version (systemd --version) |
systemctl --user commands fail | User systemd not enabled | Run loginctl enable-linger <user> or log in once to start the user manager |
When to use a system unit instead of a user unit
- The job must start before any user logs in (e.g. a nightly backup).
- The job needs to run as a specific system user that isn’t tied to a login shell.
- You
See also
- How to pull a single file from a Borg backup without unpacking the entire archive
- Summarizing /var/log/syslog Errors into JSON with jq for Grafana Dashboards
- Why ssh keeps asking for a password after adding a key and how to correct the PubkeyAuthentication setting
- When systemd‑resolved overrides /etc/hosts: a quick fix
- Fixing “Host key verification failed” After Renaming a Jump Host Server