---
title: "Linux"
description: "The binary, a service account, and a systemd unit."
url: "https://saved.sh/docs/workers/install/linux"
---

The whole install is four things: a binary on `PATH`, an unprivileged user to run it, a
directory holding `config.yaml`, and a unit that points `WorkingDirectory` at that directory.

## Download and verify [#download-and-verify]

```bash
VERSION=v0.1.0
ARCH=$(uname -m); case "$ARCH" in x86_64) ARCH=amd64 ;; aarch64) ARCH=arm64 ;; esac

curl -fsSLO "https://github.com/savedhq/local-worker/releases/download/${VERSION}/local-worker-linux-${ARCH}"
curl -fsSLO "https://github.com/savedhq/local-worker/releases/download/${VERSION}/SHA256SUMS"

grep "local-worker-linux-${ARCH}\$" SHA256SUMS | sha256sum -c -
```

That prints `OK` or it prints `FAILED`. Do not skip it and do not accept the second answer.

```bash
sudo install -m 0755 "local-worker-linux-${ARCH}" /usr/local/bin/local-worker
```

The binary is statically linked, so there is no distribution to match and no package to add.
The **dump tools are separate**: install `postgresql-client`, `mysql-client` or `redis` for
the source types you actually use.

## Create the service account [#create-the-service-account]

The worker needs no privileges of its own. It needs to read one file and reach your sources.

```bash
sudo useradd --system --no-create-home --shell /usr/sbin/nologin saved

sudo install -d -m 0700 -o saved -g saved /etc/saved
sudo install -m 0600 -o saved -g saved config.yaml /etc/saved/config.yaml
```

<Callout type="warn">
  `config.yaml` holds your source passwords and the worker key. Mode `0600` owned by `saved`
  is the whole of its protection: the token is on your disk and nowhere else, so nothing else
  on the host should be able to read it.
</Callout>

On RHEL-family distributions the locked shell is `/sbin/nologin`. If the account already
exists, `useradd` fails harmlessly and the two `install` lines are what matter.

The account also has to reach the sources. A `file` or `folder` backup reads as `saved`, and a
`script` source runs as `saved`, so grant that user the access those paths need rather than
running the worker as root.

## Run it once by hand [#run-it-once-by-hand]

Before writing a unit, prove the config works. The worker reads `config.yaml` **from its
working directory**, so run it from there.

```bash
sudo -u saved sh -c 'cd /etc/saved && /usr/local/bin/local-worker'
```

A healthy start logs the tools it resolved, the hub it dialled, and how many backups the
config declares, then goes quiet.
[Troubleshooting](/docs/workers/troubleshooting#what-healthy-looks-like) shows exactly what
those lines look like. `backups=0` means it is connected and will fail every run it receives.

Stop it with `Ctrl-C` once it looks right.

## The unit [#the-unit]

```ini title="/etc/systemd/system/saved-worker.service"
[Unit]
Description=saved.sh local worker
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=saved
Group=saved
WorkingDirectory=/etc/saved
ExecStart=/usr/local/bin/local-worker
Restart=always
RestartSec=5s

# /var/lib/saved, created 0700 and owned by User. Point local_temp_path inside it.
StateDirectory=saved
StateDirectoryMode=0700

UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectControlGroups=true
PrivateDevices=true
```

`WorkingDirectory` is not optional. It is the only way the worker finds its config.

Set the staging directory to match, in `/etc/saved/config.yaml`:

```yaml
local_temp_path: /var/lib/saved/tmp
```

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now saved-worker
sudo journalctl -u saved-worker -f
```

<Callout type="warn">
  **`ProtectSystem=strict` makes the whole filesystem read-only except the state directory.**
  Reads are unaffected, so `file` and `folder` backups still work. A `script` source that
  writes somewhere other than `$SAVED_OUTPUT` will not. Drop the line, or add a
  `ReadWritePaths=`, if you have a script that needs to write.
</Callout>

<Callout>
  `PrivateTmp=true` is safe here &#x2A;*only because `local_temp_path` is under
  `/var/lib/saved`**. Leave the default and the worker stages dumps in the unit's private
  `/tmp`, which you cannot inspect from outside the unit and which is destroyed on restart.
  Set one or the other, not neither.
</Callout>

## Check it took [#check-it-took]

```bash
systemctl is-active saved-worker
journalctl -u saved-worker | grep 'local-worker starting'
```

Then confirm from our side that the worker is polling:

```bash
sctl worker list
```

An instance count of exactly one is what you want. Two means a second copy of the same config
is running somewhere, which [breaks runs](/docs/workers/lifecycle#one-process-per-credential).

## Next [#next]

<Cards>
  <Card href="/docs/workers/configuration" title="Configuration" description="Every key the config file accepts." />

  <Card href="/docs/workers/upgrades" title="Upgrades" description="Replacing the binary without breaking a run." />

  <Card href="/docs/workers/troubleshooting" title="Troubleshooting" description="When the unit starts and nothing happens." />
</Cards>
