TMUX Service Setup

By Edward, Published January 13 2024, Updated March 3 2024

The server I have set up consists of a dedicated server user account, a home directory, and server scripts. The server runs in Terminal Multiplexor (TMUX). The server user serverd has a home directory set up in /usr/servers. Included is a ~/hooks/ folder including /usr/servers/hooks/startup/ and /usr/servers/hooks/shutdown/ and includes a $SERVER.sh file for each relevant server instance. A service file is placed inside /etc/systemd/system/ and launches the /usr/server/hooks/startup/$SERVER.sh file and terminates the server using the /usr/server/hooks/shutdown/$SERVER.sh file. 

All setup commands should be run as root or with sudo.

I have also set up a github repo that automatically installs TMUX on Ubuntu Server. Tested in my VM. https://github.com/anedward01/tmux-deployment

Use Case

TMUX can be used to create several terminal instances that allow user interaction. In my specific use case, I use it to run video game servers that may require additional input, or just need to review session logs. On that end, I set up TMUX to create several sessions, load up the server files, and run the servers. However, this system requires time and manual effort to do on each boot. For that purpose, I investigated how to launch TMUX on boot as its own user, with its own directory.


User and TMUX Setup

The command to set up the user account is quite simple. We will make the user and group, then add the new user to the new group.

mkdir /usr/servers
useradd serverd -d /usr/servers -s /bin/bash
groupadd servermod
usermod -aG servermod serverd
chown -R serverd:servermod /usr/servers
Bash

Tmux requires a bit more setup than running a standard session. In order to connect to the server session, a socket must be created. This will launch a session instance.

tmux -S /usr/servers/Server
Bash

Disconnecting from the session and listing the home directory contents reveals a new file Server. This file is constantly referenced to connect to the tmux session. 

To make sure the servermod group can access Server, run the following command:

chown serverd:servermod /usr/servers/Server
chmod 770 /usr/servers/Server
Bash

Overview

User: serverd (server daemon)
Group: servermod (server moderators)
Home: /usr/servers/
Scripts: /usr/servers/hooks/
Start: /usr/servers/hooks/startup/$SERVER.sh
Stop: /usr/servers/hooks/shutdown/$SERVER.sh
Service: /etc/systemd/system/$SERVER.service
Socket: /usr/servers/Server

Full code

mkdir /usr/servers
useradd serverd -d /usr/servers -s /bin/bash
groupadd servermod
usermod -aG servermod serverd
chown -R serverd:servermod /usr/servers
tmux -S /usr/servers/Server
chown serverd:servermod /usr/servers/Server
chmod 770 /usr/servers/Server
Bash

TMUX on Boot

The general setup for TMUX is now done. To have it automatically start up, create /usr/servers/tmux.sh with the following code

/usr/servers/tmux.sh
#!/usr/bin/env bash
# This file begins the Terminal MUltipleXor (TMUX) session the servers
# will be running on.

SESSION="tmux -S /usr/servers/Server send-keys -t Servers"
tmux -S "/usr/servers/Server" new -s "Servers" -d
$SESSION "tmux rename-window 'Management'" C-m
Bash

Then allow the file to be run as an executable and set the owner of the whole /usr/servers folder back to serverd.

chmod ug+x /usr/servers/tmux.sh
chown -R serverd:servermod /usr/servers
Bash

Next, create /etc/systemd/system/serverd.service and add the following code

/etc/systemd/system/serverd.service
[Unit]
Description=Server Terminal Multiplexor
Wants=network.target
After=network.target

[Service]
User=serverd
Group=servermod
Type=forking
ProtectHome=true
ProtectSystem=full
PrivateDevices=true
NoNewPrivileges=true
InaccessibleDirectories=/root /sys /srv -/opt /media -/lost+found
ReadWriteDirectories=/usr/servers
WorkingDirectory=/usr/servers

ExecStart="/usr/servers/tmux.sh"

[Install]
WantedBy=multi-user.target
Systemd Service

Finally, reload systemctl, launch the service, and enable it

systemctl daemon-reload
systemctl start serverd.service
systemctl enable serverd.service
Bash

To make sure the session is working, run the following command:

tmux -S /usr/servers/Server attach -t Servers
Bash

If all went well, you should see the new window. If it didn’t work, review the steps, check systemctl logs.

Quick notes

Use absolute references for the server session

tmux.sh line 8 new -s "Servers" -d creates a new session named Servers and immediately detaches the program from the session.

serverd.service line 12 Type=forking is necessary to prevent systemctl from killing serverd.service and its child processes, which includes the TMUX session.

serverd.service does not include ExecStop= since it is not immediately necessary. However, it can be added as desired.

serverd.service line 10-16 are extra commands which prevent privilege escalation and stop the service from modifying system files

Troubleshooting

After reloading systemctl, if it throws any errors, use one or both of the commands below

journalctl -eu serverd.service
systemctl status serverd.service
Bash

Each will contain relevant information towards the crash.

Sometimes a permissions issue prevents /usr/servers/tmux.sh from starting.

chmod ug+x /usr/servers/tmux.sh
Bash

If it throws an access issue or read-write denial, serverd probably lacks permissions for tmux.sh.

chown -R serverd:servermod /usr/servers
Bash

Session Startup Script

Great! If you made it this far, your TMUX socket session is up and running properly. That’s pretty much the hard part. The rest from here is just appendages for the server.

Source code for ~/hooks/startup/example.sh

/usr/servers/hooks/startup/example.sh
#!/usr/bin/env bash

# This script is a hook for the file located at /usr/servers/tmux.sh
# and adds a window to TMUX session "Servers".

SESSION_DIR="/usr/servers/Server"

# Pulls filename for TMUX window. Typically name this file a concatenated string
# "exampleServer.sh" or "ExampleServer.sh" to avoid issues
FILE_NAME=$(basename -s .sh $0)

SESSION="tmux -S $SESSION_DIR send-keys -t Servers"

WINDOW="$SESSION:$FILE_NAME"

MANAGE="tmux select-window -t Management"

# The following creates a new window, renames it to the corresponding server,
# then reselects the Management window for conflict prevention.
$SESSION":Management" "$MANAGE && tmux new-window && tmux rename-window $FILE_NAME && $MANAGE" C-m

# This is necessary. Without it, the following commands won't find its window.
sleep 0.01

# The following runs the actual server commands to its specific window.
# Make sure the folders are made before launching this script
$WINDOW "cd /usr/servers/serverFiles/example" C-m

# Example command of running a Minecraft server
#$WINDOW "java -Xmx10G -jar server.jar nogui" C-m
Bash

Notes

Don’t forget to make this file executable

chmod ug+x /usr/servers/hooks/startup/example.sh
Bash

Variables

SESSION_DIR states the TMUX socket

FILE_NAME states the server’s name

SESSION sets the relevant keypress command

WINDOW sets the window to send the keypress command to from SESSIONMANAGE sets the sesson’s main window, through which startup commands should be input

Elaboration

sleep 0.01 is necessary. Without it, the server won’t be able to find it’s relevant window.

The abundance of variables is used to cut down on overall length of commands and to make the instance operation structure

For example, during boot server1 and server2 boot in parallel, and if line 16 is split in two commands, then their commands may overlap and conflict.


Session Shutdown Script

Great! If you made it this far, your TMUX socket session is up and running properly. That’s pretty much the hard part. The rest from here is just appendages for the server.

Source code for ~/hooks/startup/example.sh

/usr/servers/hooks/shutdown/example.sh
#!/usr/bin/env bash
# This script is a hook for the file located at /usr/servers/tmux.sh
# and sends the shutdown procedure for the program

SESSION_DIR="/usr/servers/Server"
FILE_NAME=$(basename -s .sh $0)
SESSION="tmux -S $SESSION_DIR send-keys -t Servers"
WINDOW="$SESSION:$FILE_NAME"
MANAGE="tmux select-window -t Management"

# Insert shutdown commands here. Begin with $WINDOW to direct commands
# into its proper window. This prevents collisions with other programs.
#$WINDOW "stop" C-m
$WINDOW "exit" C-m
Bash

Notes

Don’t forget to make this file executable

chmod ug+x /usr/servers/hooks/shutdown/example.sh
Bash

Since all commands are piped through their corresponding window, sleep is not necessary for the base file.

Session Service Script

/etc/systemd/system/example.service
[Unit]
Description=Example Server Service
Wants=network.target
Wants=serverd.service

[Service]
User=serverd
Group=servermod
Type=oneshot
RemainAfterExit=true

ProtectHome=true
ProtectSystem=full
PrivateDevices=true
NoNewPrivileges=true
InaccessibleDirectories=/root /sys /srv -/opt /media -/lost+found
ReadWriteDirectories=/usr/servers
WorkingDirectory=/usr/servers/hooks/
ExecStartPre=/bin/sleep 1
ExecStart=/usr/servers/hooks/startup/example.sh
ExecStop=/usr/servers/hooks/shutdown/example.sh

[Install]
WantedBy=multi-user.target
Systemd Service

Notes

Type=oneshot is necessary to prevent example’s server from being shut down, while still marking the service as active until the TMUX window closes or the service shutdown command is sent

ExecStartPre is necessary to prevent conflict with other servers at bootup. Use alongside a Wants=previousservice.service entry.

Final Steps

Run the same systemctl steps as for serverd.service

systemctl daemon-reload
systemctl start example.service
Bash

Make sure the server works

tmux -S /usr/servers/Server attach -t Servers:example
Bash

Finally, if the server runs and all is right:

systemctl enable example.service
Bash

Final Notes

TMUX is amazing for running programs that need occasional input from the terminal, rather than running as a straight service with no interaction. Creating an automated TMUX instance and sessions helps create an amazing platform that can easily be scaled and modified based on needs.