---
title: "Container"
description: "The image, which ships the dump tools already."
url: "https://saved.sh/docs/workers/install/container"
---

The image is the shortest path to a working worker, because it already contains the four dump
tools: `curl`, `pg_dump` and `mysqldump`. Nothing to install per source type.

```
ghcr.io/savedhq/local-worker
```

## Check the architecture first [#check-the-architecture-first]

Which architectures a tag carries is a property of that tag. Check before you pin one.

```bash
docker buildx imagetools inspect ghcr.io/savedhq/local-worker:v0.1.0
```

<Callout type="warn">
  **`v0.1.0` is `linux/arm64` only.** On an amd64 host it fails at pull time with
  `no matching manifest for linux/amd64 in the manifest list entries`. Until a tag exists that
  lists your architecture, use a [source build](/docs/workers/install/source), which also
  covers building this image yourself.
</Callout>

## Prepare the host [#prepare-the-host]

Two paths matter, and both have to belong to the uid the image runs as.

```bash
sudo install -d -m 0755 /etc/saved
sudo install -m 0400 -o 65532 -g 65532 config.yaml /etc/saved/config.yaml

sudo install -d -m 0700 -o 65532 -g 65532 /var/lib/saved
```

Then set the staging directory to the one you just created, in that same config:

```yaml title="/etc/saved/config.yaml"
local_temp_path: /var/lib/saved
```

## Run it [#run-it]

```bash
docker run -d --name saved-worker \
  --restart unless-stopped \
  -v /etc/saved/config.yaml:/config.yaml:ro \
  -v /var/lib/saved:/var/lib/saved \
  ghcr.io/savedhq/local-worker:v0.1.0
```

```bash
docker logs -f saved-worker
```

A healthy start logs the tools it resolved, the hub it dialled, and how many backups the
config declares. See
[what healthy looks like](/docs/workers/troubleshooting#what-healthy-looks-like).

## Four things the image will not forgive [#four-things-the-image-will-not-forgive]

**The config mounts at `/config.yaml`.** The image sets no working directory, so the worker
looks in `/`. Mount it anywhere else and the worker starts with no backups and fails every run
with `ConfigDrift`.

**It runs as uid `65532`, and there is no user database entry to fall back on.** The mounted
config must be readable by that uid and the staging path writable by it, which is what the
`install -o 65532` lines above are for. A config left as root-owned `0600` is invisible to the
process, and the worker exits with `missing required config: api_url, token`.

**Do not mount a fresh named volume at the staging path.** Docker creates a named volume
root-owned `0755`, uid `65532` cannot write into it, and the first dump fails on a path the
worker was told it could use. A host directory you chowned yourself is predictable; a named
volume is not.

**`api_url` and `token` can come from the environment (`API_URL`, `TOKEN`), but the `backups:`
map cannot.** Any worker that actually serves a source needs the file. Every other top-level
key has an environment equivalent in upper case, `LOCAL_TEMP_PATH` and `DEBUG` among them.

<Callout>
  Give the staging path room for **the dump and its encrypted copy at the same time**, so
  roughly twice the dump size. It is worth a real volume rather than the container filesystem:
  a large dump filling a writable layer is a bad way to find that out.
</Callout>

## Compose [#compose]

```yaml title="docker-compose.yaml"
services:
  saved-worker:
    image: ghcr.io/savedhq/local-worker:v0.1.0
    restart: unless-stopped
    volumes:
      - /etc/saved/config.yaml:/config.yaml:ro
      - /var/lib/saved:/var/lib/saved
```

No `ports:` section, ever. The worker dials out and nothing dials in.

## Podman [#podman]

The same image works, with one difference that will bite you. Under **rootless** Podman, uid
`65532` in the container is not uid `65532` on the host: it maps through your subuid range. Do
the ownership change inside that mapping rather than on the host.

```bash
podman unshare chown 65532:65532 /etc/saved/config.yaml
podman unshare chown 65532:65532 /var/lib/saved
```

Add `:Z` to the volume arguments on a system with SELinux enforcing.

## Tags [#tags]

| Tag           | What it is                    |
| ------------- | ----------------------------- |
| `vX.Y.Z`      | A tagged release. Use this    |
| `latest`      | The tip of `main`             |
| `sha-<short>` | One specific commit on `main` |

**Pin a version.** `latest` is the right choice for a machine you are actively testing against
and the wrong one for the host that holds your production backups. See
[Upgrades](/docs/workers/upgrades) for moving between them.

## Kubernetes [#kubernetes]

The [operator](/docs/kubernetes) has not shipped. Until it does, run the image the way you
would any other single-replica workload: a `Deployment` with `replicas: 1`, the config in a
`Secret` mounted at `/config.yaml`, and a volume for the staging path.

<Callout type="warn">
  **`replicas: 1`, and never a rolling update.** Two processes sharing one worker credential
  both poll the same queue, and a run's steps hand each other a path to a file on local disk.
  Set `strategy.type: Recreate` so a deploy never briefly runs two.
  [Why](/docs/workers/lifecycle#one-process-per-credential).
</Callout>

## 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="Changing the tag without breaking a run." />

  <Card href="/docs/workers/install/source" title="Build from source" description="Including building this image for your own architecture." />
</Cards>
