Linux VPS

Create system users for apps and services

Run each app on your Linux VPS under its own system account with no password and no shell, so a bug or breach in one app cannot take over the whole server.

IntermediateLinux server4 min read

Every service you run, whether a Node.js app, a Python worker or a game server, should run as its own unprivileged account. If the app has a bug or is attacked, the damage stays inside what that account can touch. This guide shows how to create a system user without a password or shell, give it a working folder and run a service as that user with systemd.

Before you start

  • A Linux VPS with Ubuntu 22.04 or 24.04, Debian 12, or AlmaLinux or Rocky Linux 8 or 9.
  • Root or sudo access over SSH.
  • The app you want to run, and a name for its account, such as myapp.

System users vs normal users

A system user is an ordinary account with a few differences:

  • Its UID comes from the system range, usually below 1000, so it is hidden from login screens and user lists that show people.
  • It normally has no password, so nobody can log in with it.
  • It normally has the shell /usr/sbin/nologin, so even su into it is refused.
  • It has no home folder in /home, or a home folder where the app keeps its data.

Packages already use this pattern: www-data, mysql and postgres are all system users.

Create the user on AlmaLinux and Rocky Linux

Bash
sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
  • --system picks a UID from the system range and skips password ageing.
  • --no-create-home (short -M) avoids an unwanted folder.
  • --shell /usr/sbin/nologin blocks interactive logins.

A private group named myapp is created as well, because user private groups are the default on these systems.

If the app needs a data folder, you can set it as the home folder in the same command:

Bash
sudo useradd --system --home-dir /var/lib/myapp --create-home --shell /usr/sbin/nologin myapp

Create the user on Ubuntu and Debian

useradd works exactly the same way as above. Debian also offers adduser --system, which picks safe defaults for you:

Bash
sudo adduser --system --group --no-create-home myapp
  • --system uses a system UID, no password and the nologin shell.
  • --group creates a matching myapp group. Without it, the user joins the shared nogroup group.
  • --home /var/lib/myapp sets a data folder instead of --no-create-home if you need one.

Give the app a place for its files

Keep code owned by root or a deploy user, and give the service account write access only where it needs it, such as data, cache or logs:

Bash
sudo mkdir -p /opt/myapp /var/lib/myapp
sudo chown -R root:root /opt/myapp
sudo chown myapp:myapp /var/lib/myapp
sudo chmod 750 /var/lib/myapp

This way, even if the app is compromised, it cannot rewrite its own code.

Run the service as that user

Set the user in the systemd unit. A minimal example in /etc/systemd/system/myapp.service:

INI
[Unit]
Description=My app
After=network.target

[Service]
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/myapp
Restart=on-failure

[Install]
WantedBy=multi-user.target

Load and start it:

Bash
sudo systemctl daemon-reload
sudo systemctl enable --now myapp

The full walkthrough is in Create a systemd service.

Tip: systemd can also create a throwaway user for you each time the service starts with DynamicUser=yes. It suits simple stateless services. For apps that keep files between restarts, a fixed system user is easier to reason about.

Run a one-off command as the user

Because the account has no shell, su myapp is refused. Use sudo -u to run a single command as that user:

Bash
sudo -u myapp /opt/myapp/bin/myapp --version

If you really need an interactive shell for debugging, pass the shell explicitly:

Bash
sudo -u myapp -s /bin/bash

Exit as soon as you are done.

Ports below 1024

Unprivileged users cannot bind to ports such as 80 or 443. The usual fix is to run the app on a high port, such as 3000 or 8000, and put Nginx in front. If the app must listen directly, grant just that ability in the unit file:

INI
AmbientCapabilities=CAP_NET_BIND_SERVICE

Check it works

Confirm the account details:

Bash
getent passwd myapp
myapp:x:998:998::/var/lib/myapp:/usr/sbin/nologin

The UID is below 1000 and the shell is nologin. Then check that the service really runs as that user:

Bash
ps -o user,pid,cmd -C myapp

The USER column should show myapp, not root.

Troubleshooting

  • "Permission denied" in the app log: the account cannot write somewhere. Find the path in the error, then grant access with chown myapp:myapp on that folder only.
  • su myapp says "This account is currently not available": that is expected with nologin. Use sudo -u myapp instead.
  • App fails to bind port 80 or 443: non-root users cannot use ports below 1024. Use a reverse proxy, or add CAP_NET_BIND_SERVICE as above.
  • "useradd: user 'myapp' already exists": a package or earlier attempt created it. Check with getent passwd myapp and reuse it if the settings fit.

Next steps

  • Restrict the service further: Sandbox systemd services.
  • Example for a real app: Run Node.js as a systemd service.
Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.