---
title: "Install"
description: "The Deployment that works today, and the operator install that will replace it."
url: "https://saved.sh/docs/kubernetes/install"
---

The worker in a cluster is one Deployment, one Secret and one scratch volume. Nothing listens,
nothing is exposed, and there is no Service.

Provision the worker first: the config below contains the key that provisioning prints once.
See [Registration](/docs/workers/registration).

## The Secret [#the-secret]

The config holds the worker key and every source password this worker uses. It is a
**Secret**, never a ConfigMap.

```bash
kubectl create namespace saved

kubectl create secret generic saved-worker-config \
  --namespace saved \
  --from-file=config.yaml=./config.yaml
```

Point the staging directory at the volume the Deployment mounts, in that same `config.yaml`:

```yaml title="config.yaml"
api_url: https://api.saved.sh
token: "<shown once when the worker was provisioned>"
local_temp_path: /var/lib/saved
```

Everything else the file accepts is on the
[configuration page](/docs/workers/configuration).

## The Deployment [#the-deployment]

```yaml title="worker.yaml"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: saved-worker
  namespace: saved
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app.kubernetes.io/name: saved-worker
  template:
    metadata:
      labels:
        app.kubernetes.io/name: saved-worker
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65532
        runAsGroup: 65532
        fsGroup: 65532
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: worker
          image: ghcr.io/savedhq/local-worker:v0.1.0
          workingDir: /etc/saved
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: config
              mountPath: /etc/saved
              readOnly: true
            - name: scratch
              mountPath: /var/lib/saved
          resources:
            requests:
              cpu: 50m
              memory: 128Mi
            limits:
              memory: 1Gi
      volumes:
        - name: config
          secret:
            secretName: saved-worker-config
            defaultMode: 0440
        - name: scratch
          emptyDir:
            sizeLimit: 20Gi
```

```bash
kubectl apply -f worker.yaml
kubectl -n saved logs -f deploy/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).

## Six things that are load-bearing [#six-things-that-are-load-bearing]

**`workingDir: /etc/saved`.** The image sets no working directory and the worker reads
`config.yaml` from whatever the working directory is. Setting it here means the Secret can
mount as a plain directory, so a rotated key propagates into the pod. Mounting the file
directly at `/config.yaml` with `subPath` also works and is worse: a `subPath` mount never
sees an update.

**`replicas: 1` with `strategy: Recreate`.** Two processes sharing one worker credential poll
the same queue, and a run's steps hand each other a path to a file on local disk. `Recreate`
is what stops a rolling update from briefly running two.
[Why](/docs/workers/lifecycle#one-process-per-credential).

**`fsGroup: 65532`.** The image runs as uid `65532`. Without an `fsGroup` the Secret's files
at mode `0440` are root-owned and the worker cannot read them, and the `emptyDir` is
root-owned and it cannot write to it. The pod starts and then fails on a file it was told it
could use.

**A scratch volume with room for two copies.** The dump and its encrypted copy exist at the
same time, so budget roughly twice the dump size. An `emptyDir` without a `sizeLimit` can
evict the node it is running on.

**No probes, no ports, no Service.** The worker dials out and polls. It serves no HTTP, so a
readiness probe has nothing to talk to and will restart a perfectly healthy pod.

**The image is Linux and, for `v0.1.0`, `arm64` only.** On a mixed cluster, pin the node
architecture so the pod does not land somewhere it cannot run:

```yaml
      nodeSelector:
        kubernetes.io/arch: arm64
```

Check what a tag actually carries before you rely on it:

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

## Reaching your sources [#reaching-your-sources]

The worker connects to sources the way any other pod does. An in-cluster Postgres is
`postgres.default.svc.cluster.local` in `config.yaml`; anything outside the cluster needs the
usual egress path. Outbound TCP 443 to `api.saved.sh` and `hub.saved.sh` is required, and
**nothing needs to reach the pod**.

A default-deny `NetworkPolicy` needs an egress rule. There is no ingress rule to write.

## Rotating the key [#rotating-the-key]

Rotation revokes the old key the moment the new one is issued, so the pod has to restart.

```bash
kubectl -n saved create secret generic saved-worker-config \
  --from-file=config.yaml=./config.yaml \
  --dry-run=client -o yaml | kubectl apply -f -

kubectl -n saved rollout restart deploy/saved-worker
```

See [rotation versus deletion](/docs/workers/registration#rotation-versus-deletion).

## When the operator ships [#when-the-operator-ships]

<Callout type="warn" title="Not yet available">
  This section describes the intended design. The operator has no reconcile logic today, so
  none of it works yet.
</Callout>

The operator replaces the Deployment above with a
[`LocalWorker` resource](/docs/kubernetes/resources), and takes over the six load-bearing
details with it. It will install as a Helm chart, with plain kustomize manifests as the
alternative for clusters that do not use Helm.

What will not change: the config still lives in a `Secret` you create, the worker still needs
one credential per running process, and the key still never leaves your cluster.

## Next [#next]

<Cards>
  <Card href="/docs/kubernetes/resources" title="Resources" description="The LocalWorker custom resource." />

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

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