# How to manage user system services

This page shows how to run a long-lived process as a systemd user service under your system user.
You will create the service, start and stop it, keep it running after you log out, and read its
logs. For what these services are and which applications use them, see
[What are user system services?](what-is.md).

systemd is a native alternative to tools like supervisord or PM2. It integrates with the system, gives
you full logs through `journalctl`, and needs no root access when you run it as a user service.

## Before you start

- **Secure Shell (SSH) access to the host** - see [SSH access](../../platform/hosts/ssh.md). Run
  every command below as your system user; no root access is needed.
- The command your service should run (for example a worker or consumer command from your
  application).

## Create a service

Create a unit file at `~/.config/systemd/user/<name>.service`, where `~` is your system user's home
directory. This example runs a worker and restarts it if it stops:

```ini
[Unit]
Description=My worker
After=network-online.target
StartLimitIntervalSec=0

[Service]
Type=simple
WorkingDirectory=%h/public_html
ExecStart=%h/public_html/bin/worker
Restart=always
RestartSec=10s

[Install]
WantedBy=default.target
```

- `%h` is your home directory, so the same unit works for any system user.
- `Restart=always` with `RestartSec=10s` restarts the process 10 seconds after it stops.

> [!TIP]
> `StartLimitIntervalSec=0` lets systemd keep retrying every `RestartSec` instead of giving up after
> repeated fast failures.

## Start, enable and check it

After creating or changing a unit file, reload systemd, then enable and start the service:

```bash
systemctl --user daemon-reload
systemctl --user enable --now my-worker.service   # enable at login + start now
systemctl --user status my-worker.service         # check it is active
```

## Keep it running after you log out

By default, user services stop when your last session ends. Enable lingering once so they keep
running after you close SSH and start again after a reboot:

```bash
loginctl enable-linger
```

## Read the logs

Each service logs to the systemd journal. Read or follow it with:

```bash
journalctl --user -u my-worker.service              # recent logs
journalctl --user -u my-worker.service -f           # follow live
journalctl --user -u my-worker.service --since "5 minutes ago"
```

To scan a template unit's instances for fatal errors, match the service name with a wildcard and
filter the output:

```bash
journalctl --user -u "my-worker@*" --no-pager -n 1000 | grep -i FATAL
```

## Stop, restart or remove a service

```bash
systemctl --user restart my-worker.service        # after a code change
systemctl --user stop my-worker.service            # stop, keep it enabled
systemctl --user disable --now my-worker.service   # stop and disable at login
```

## Run many instances from one template

When you need several copies of the same service (for example one message-queue consumer per queue),
use a template unit. Add an `@` to the file name and use `%i` for the instance value:

```ini
# ~/.config/systemd/user/my-worker@.service
ExecStart=%h/public_html/bin/worker %i
```

Enable one service per instance; the value after `@` is passed as `%i`:

```bash
systemctl --user enable --now my-worker@queue1.service
systemctl --user enable --now my-worker@queue2.service
```

## Scale throughput with more instances

A single worker processes one message at a time. Running several in parallel drains a queue faster.
This helps when a backlog builds up, for example during a large import or a sales campaign.

There are two ways to add capacity:

- **More instances:** run the same worker two, four or more times in parallel, one service per
  instance.
- **More threads:** some workers take a thread or process count flag instead (for example
  `--threads=4`). The same total limit below applies.

Add capacity by enabling more instances:

```bash
systemctl --user enable --now my-worker@1.service
systemctl --user enable --now my-worker@2.service
```

Remove capacity by disabling the extra services when the backlog is gone, to free processor time for
the site:

```bash
systemctl --user disable --now my-worker@2.service
```

> [!WARNING]
> Never run more parallel workers than the host has processor cores, and keep a safe margin below
> that number. Background workers compete with PHP, the web server and the database for the same
> cores. Too many workers cause high load and can make the whole server slow or unresponsive.

Check how many cores the host has:

```bash
nproc
```

Start with one or two workers. Measure how fast the queue drains and watch the host load on the
[Health](../../platform/hosts/health.md) tab, then add one worker at a time. On a 4-core host, keep
the total at or below 2 to 3 background workers so the site keeps enough processor time. If the queue
still cannot keep up with a safe number of workers, scale up the host instead of adding more.

## Application examples

The examples below are template units (note the `@` in the file name and `%i`/`%I` for the instance
value). Adjust `WorkingDirectory` and the PHP binary version to match your setup.

> [!NOTE]
> On TurboStack, PHP binaries are at `/usr/bin/php<version>` - for example `/usr/bin/php8.0` or
> `/usr/bin/php8.3`. The default `php` command also points at the host's main version. There is no
> `/usr/local/phpNN` path.

**Laravel queue worker** - `~/.config/systemd/user/laravel-queue@.service`:

```ini
[Unit]
Description=Laravel queue worker (#%i)
After=network-online.target
StartLimitIntervalSec=0

[Service]
Type=simple
WorkingDirectory=%h/public_html
ExecStart=/usr/bin/php8.3 %h/public_html/artisan queue:work --env=prod
Restart=always
RestartSec=10s

[Install]
WantedBy=default.target
```

**CraftCMS queue runner** - `~/.config/systemd/user/craftcms-queue@.service`:

```ini
[Unit]
Description=CraftCMS queue runner (#%i)
After=network-online.target
StartLimitIntervalSec=0

[Service]
Type=simple
WorkingDirectory=%h/public_html
ExecStart=/usr/bin/php8.3 %h/public_html/craft queue/listen
ExecReload=/bin/kill -SIGABRT $MAINPID
Restart=always
RestartSec=10s

[Install]
WantedBy=default.target
```

For CraftCMS, also disable `runQueueAutomatically` in the general config so the queue is not run
inside web requests.

**Magento message-queue consumer** - `~/.config/systemd/user/magento-consumer@.service`:

```ini
[Unit]
Description=Magento consumer (%i)
After=network-online.target
StartLimitIntervalSec=0

[Service]
Type=simple
WorkingDirectory=%h/public_html
ExecStart=/usr/bin/php8.3 %h/public_html/bin/magento queue:consumers:start %I --single-thread --max-messages=10000
Restart=always
RestartSec=10s

[Install]
WantedBy=default.target
```

Here `%I` is the consumer name, so one template serves every queue. `--single-thread` runs one
thread per process and `--max-messages=10000` restarts the process periodically to keep memory in
check. Enable one service per consumer:

```bash
systemctl --user enable --now magento-consumer@product_action_attribute.update.service
systemctl --user status  magento-consumer@sales.rule.update.coupon.usage.service
```

For a worked example, see the Magento message-queue consumers in
[Magento best practices](../../applications/magento/best-practices.md).

## Related

- [What are user system services?](what-is.md)
- [Application file layout and permissions](../../applications/index.md#file-layout-and-permissions)
- [How to keep a Node.js app running](../nodejs/run-with-process-manager.md)
- [SSH access](../../platform/hosts/ssh.md)
