Can I self-host Backblaze Computer Backup?

YES · ONE COMMAND— setup effort 1 of 4

YES — it's called Backrest. It takes one prompt, a 1024 MB VPS, and about 10 minutes. That is $9 a month you stop paying Backblaze Computer Backup — $108 a year on the Personal Backup plan.

Why people pay for Backblaze Computer Backup

Stated as the vendor would want it stated. A replacement you pick without knowing what the subscription actually buys is a replacement you abandon in a fortnight.

Backblaze sells one decision you never have to make again: everything on the computer, and on the drives plugged into it, goes to their cloud for a flat fee, with no quota to watch and no folder to choose. The agent runs whether or not you remember it exists, the price does not move when your photo library doubles, and when the laptop is gone they will courier you a hard drive with your files on it. Unlimited storage for a fixed price per computer is the part nobody self-hosting can match on cost alone.

Backblaze Computer Backup plans and list prices
PlanList priceWhat it buys
Personal Backupthe plan this page prices against$9/moPer computer, unlimited data, external drives included. The billing toggle also offers $8.25 a month billed yearly ($99 a year) and $7.88 a month billed two-yearly ($189).
Business Backup$8.25/moThe business page prints one figure, $99 per computer per year, which is $8.25 a month; it adds a console for managing several people over the personal product and says month, yearly or two-year billing is available.
Enterprise Controlquote onlyQuote only: the pricing page says let's talk. Its comparison table prices the Enterprise Control layer at an added $24 per year per computer over Business.

Vendor list prices in USD, read from the pricing page on 2026-08-06 · confidence: medium

Replaced by Backrest

One project, named before the prompt, so you know what you are about to install.

A web UI and a scheduler over restic, so the backups you keep meaning to set up have somewhere to be configured and something to watch them.

The closest honest answer, as long as you understand the swap. Backblaze backs up your computer to their cloud; Backrest is a web UI and a scheduler over restic, so it backs up whatever machine it runs on to storage you choose, and it is the storage that still costs money. Run it on your own computer and you have replaced the software, kept the encryption, and taken on being the person who tests restores. Run it on a server and it is backing up the server, which is a different job Backblaze never did for you.

The swap

You're paying

Backblaze Computer Backup

$9/mo · $108/yr

is replaced by

You'd run

Backrest

ONE COMMAND · ~10 min to running · 1024 MB RAM

Backblaze Computer Backup Personal Backup · vendor list price · checked 2026-08-06 · source · confidence: medium

Before you start

RAM floor
1024 MBfloor from upstream docs — not measured by us yet
Disk
5 GBthe app, its data, and room for one backup
Domain needed
yes, one A recorda hostname pointed at the box before you start — TLS needs it on the cloud path, and the local path needs none
Time budget
~10 minunder 10 minutes, through the first backup

The prompt

Two paths to the same Backrest: the cloud one assumes Prompt Zero is done on a server you rent, the local one assumes nothing but a computer that can run Docker Desktop. Read whichever you pick before you paste it, which is the whole reason both are on the page instead of behind a download.

authored from upstream docs · not yet machine-verified · Claude Code

Where it runs

315 lines · 14,652 bytes

What this prompt will do
  1. Preflight
  2. Layout
  3. Secrets
  4. compose.yml
  5. Caddy and TLS
  6. Firewall
  7. Start and verify
  8. First backup and restore
  9. Updating later
  10. What will probably go wrong
  11. Out of scope

Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.

paste it into Claude Code in a terminal on your own machine · it runs the install over ssh vps

You are Claude Code on the user's machine. The user has completed Prompt Zero: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny.

Run every command in this prompt on the server over `ssh vps` unless the step says otherwise.

Install Backrest 1.14.1 on that server, reachable at https://<DOMAIN>, behind the existing Caddy
with automatic TLS.

## 1. Preflight

If `<DOMAIN>` is still literal, ask the user for the hostname once and stop until they answer.
Its A record must already point at this server.

Say two things to the user first. Backrest is a web UI and a scheduler over restic: it backs up
the files on the machine it runs on, so here that is /srv, the data every other service on this
box keeps, and not their laptop. And it needs somewhere to send the snapshots, a bucket or a box
that still costs money. What stops is the per-computer software fee, not the storage bill.

Backrest needs 1024 MB of RAM available and 5 GB free on /srv. The image publishes amd64 and
arm64. Measure all four:

```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```

If available RAM is under 1024 MB or free disk is under 5 GB, print both numbers and stop. Do not
install and hope. If `dig +short` prints nothing, print that and stop: Caddy cannot get a
certificate for a name that does not resolve. The 5 GB is the image plus restic's cache, which
grows with the repository index, not with the data copied.

## 2. Layout

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/backrest /srv/backrest/backups
sudo install -d -m 700 -o $(id -u) -g $(id -g) /srv/backrest/config
sudo install -d -m 700 /srv/backrest/data /srv/backrest/cache /srv/backrest/restore
ls -la /srv/backrest
```

Assert: `ls -la` shows five directories, with `config` and `backups` owned by the login user and
`data`, `cache` and `restore` at mode `700` owned by root. The container runs as root, because
the files it reads under /srv were written by other services as other users. `config` is the
login user's because step 3 writes into it first.

## 3. Secrets

One secret: the password for the web login. Generate it on the server, do not print it, do not
repeat it in your summary, and keep it out of every log line. Hex rather than base64, because a
human types this one into a form.

The rest of this step is the security decision here. A Backrest that starts without a
configuration file writes itself a default one with authentication disabled, then serves every
API route to whoever asks. Writing the file first means there is never a minute where that is
true on a public hostname.

```bash
umask 077
cat > /srv/backrest/.env <<EOF
BACKREST_USERNAME=admin
BACKREST_PASSWORD=$(openssl rand -hex 24)
EOF
chmod 600 /srv/backrest/.env
cat > /srv/backrest/config/config.json <<'EOF'
{
  "version": 6,
  "instance": "<DOMAIN>",
  "auth": {"disabled": false, "users": [{"name": "admin", "passwordBcrypt": "PLACEHOLDER"}]}
}
EOF
grep BACKREST_PASSWORD /srv/backrest/.env | cut -d= -f2- | caddy hash-password | base64 | tr -d '\n' > /srv/backrest/config/hash.tmp
awk 'NR==FNR{h=$0;next} {sub(/PLACEHOLDER/,h);print}' /srv/backrest/config/hash.tmp /srv/backrest/config/config.json > /srv/backrest/config/config.tmp
mv /srv/backrest/config/config.tmp /srv/backrest/config/config.json && rm /srv/backrest/config/hash.tmp
chmod 600 /srv/backrest/config/config.json
umask 022
ls -l /srv/backrest/.env /srv/backrest/config/config.json
grep -q PLACEHOLDER /srv/backrest/config/config.json && echo "substitution failed" || echo "hash in place"
```

Replace `<DOMAIN>` in that block with the real hostname before running it. Assert: both files
exist at mode `-rw-------`, and the last line prints `hash in place`. Three upstream facts hold
it together. A password is stored as a bcrypt hash that is then base64 encoded, which is what
`caddy hash-password` piped into `base64` produces. `version` is required: a file with content
and no version number is rejected at start-up, and 6 is this release's migration count.
`instance` names this install inside every snapshot and cannot be changed later.

Tell the user their password is in /srv/backrest/.env, readable with
`sudo grep BACKREST_PASSWORD /srv/backrest/.env`, and that it goes in their password manager now.
Do not print it.

## 4. compose.yml

```bash
cat > /srv/backrest/compose.yml <<'EOF'
# Backrest · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
#   readme and docker ... https://github.com/garethgeorge/backrest/blob/v1.14.1/README.md
#   getting started ..... https://garethgeorge.github.io/backrest/introduction/getting-started
#   entrypoint defaults . https://github.com/garethgeorge/backrest/blob/v1.14.1/cmd/docker-entrypoint/main.go
#
# One service. Backrest is a web UI and a scheduler over restic, and the image
# ships the restic it was built against at /bin/restic, so nothing is fetched
# at start-up. It runs as root because the files it reads under /srv were
# written by other services as other users. /userdata is /srv, read-only, so a
# restore cannot go home; that is what the writable /restore is for.
#
# Tag and digest read from ghcr.io on 2026-08-06; the image publishes amd64,
# arm64, arm/v6, arm/v7 and 386.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  backrest:
    image: ghcr.io/garethgeorge/backrest:v1.14.1@sha256:b852979754281026230cc69fb11428e6d57c9a97784ab4a444ffc7934c53a215
    container_name: backrest
    restart: unless-stopped
    environment:
      BACKREST_PORT: "0.0.0.0:9898"
      BACKREST_CONFIG: /config/config.json
      BACKREST_DATA: /data
      XDG_CACHE_HOME: /cache
      BACKREST_RESTIC_COMMAND: /bin/restic
      TZ: UTC
    volumes:
      # Repository URIs, schedules and the login hash, written in step 3.
      - /srv/backrest/config:/config
      # oplog.sqlite, the JWT signing secret, the per-task logs.
      - /srv/backrest/data:/data
      # restic's cache: rebuildable, and it grows with the repository index.
      - /srv/backrest/cache:/cache
      - /srv/backrest/restore:/restore
      # The data this server holds: everything in this catalogue lives in /srv.
      - /srv:/userdata:ro
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8136.
      - "127.0.0.1:8136:9898"
EOF
cd /srv/backrest && docker compose config >/dev/null && echo "compose OK"
```

Assert: that prints `compose OK`. One service, one published port, no database: the history is a
SQLite file under /srv/backrest/data, the settings the file step 3 wrote.

## 5. Caddy and TLS

Append the block below to the Caddyfile Prompt Zero installed, with `<DOMAIN>` replaced by the
real hostname. Copy the file first: a syntax error takes down every other site on the box.

```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-backrest
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Backrest · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://garethgeorge.github.io/backrest/cookbooks/reverse-proxy-examples and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. Upstream's own
# example is one reverse_proxy line to 9898; the headers are ours.

<DOMAIN> {
	# Caddy's default encode matcher covers text, JSON, JavaScript and SVG
	# only, so a file downloaded from a snapshot passes through untouched.
	encode zstd gzip

	# Backrest sets no transport or frame headers of its own. HSTS is on
	# because every request here carries the token that reads this server.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "SAMEORIGIN"
		Referrer-Policy "no-referrer"
		-Server
	}

	# 8136 is the loopback port compose publishes on this host. It is not a
	# container port and it is not open in the firewall.
	reverse_proxy 127.0.0.1:8136
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

Assert: `caddy validate` exits 0 and the reload exits 0. If validate fails, restore
/etc/caddy/Caddyfile.before-backrest, reload, and report what it objected to. Caddy asks for the
certificate on the first request and renews it alone.

## 6. Firewall

Two ports open, both Caddy's. Idempotent, so on a Prompt Zero box they change nothing:

```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```

80/tcp answers the ACME challenge and redirects to HTTPS, 443/tcp is the only way in, 443/udp is
HTTP/3. 8136 stays closed because compose binds it to 127.0.0.1. Assert: `ufw status verbose`
prints `Status: active`, shows 80, 443/tcp and 443/udp, and no rule for 8136 or 9898.

## 7. Start and verify

```bash
cd /srv/backrest
docker compose pull
docker compose up -d
for i in $(seq 1 24); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
curl -sS https://<DOMAIN>/ | grep -o '<title>Backrest</title>'
curl -sS -o /dev/null -w '%{http_code}\n' -X POST https://<DOMAIN>/v1.Backrest/GetOperations -H 'Content-Type: application/json' -d '{}'
printf 'user = "admin:%s"\n' "$(sudo grep BACKREST_PASSWORD /srv/backrest/.env | cut -d= -f2-)" | curl -sS -o /dev/null -w '%{http_code}\n' -K - -X POST https://<DOMAIN>/v1.Backrest/GetOperations -H 'Content-Type: application/json' -d '{}'
```

Assert all four, and print what you received for each. The loop ends printing `200`. The second
prints `<title>Backrest</title>`. The third prints `401`, the security assert in this block: an
unauthenticated API call is refused, so step 3 took effect. The fourth prints `200`, proving the
generated password matches the hash in that file; it reads the password from a curl config on
standard input, so the value never reaches a command line or the terminal. If any of the four
misses, stop, run `docker compose logs --tail 40 backrest`, and name the likely earlier step: a
container that exits in seconds is step 3, a `200` where a `401` belongs means the configuration
file was not read, and a `401` on the fourth means the hash and the password disagree. A running
container is not success.

STOP: tell the user to read their password with
`sudo grep BACKREST_PASSWORD /srv/backrest/.env`, put it in their password manager, open
https://<DOMAIN>, and log in as `admin`. Wait. Do not continue until they confirm. The first
screen is a box headed `Login`, with `Username` and `Password` fields and a `Log in` button.

## 8. First backup and restore

Two backups here, and they are not the same thing. First this install's own configuration, the
archive below. Then the user's first snapshot, what they installed this for, which only they can
set up.

```bash
cd /srv/backrest
docker compose stop
sudo tar -czf /srv/backrest/backups/backrest-config-$(date +%F).tar.gz -C /srv/backrest config data compose.yml .env -C /etc/caddy Caddyfile
docker compose start
ls -lh /srv/backrest/backups/
```

Assert: the archive exists and is non-empty. Print its size. The container is stopped on purpose,
because a SQLite history copied mid-write is not a backup, and it costs five seconds. The archive
holds every repository password the user is about to type, so it is as sensitive as what it
describes.

A backup on the same disk as the data is not a backup. Run this from the user's machine, not the
server:

```bash
mkdir -p ~/backups/backrest
scp vps:/srv/backrest/backups/*.tar.gz ~/backups/backrest/
```

STOP: tell the user to open https://<DOMAIN>, click `Add Repo` and set up the storage their
snapshots go to, then `Add Plan` with paths under `/userdata`, then run that plan once and
restore one file from the resulting snapshot into `/restore`. Wait.
Do not continue until they confirm. Three things to tell them while they do it. The
repository password they type is a
restic encryption password: lose it and every snapshot is unreadable, by them too, so it goes in
the password manager beside the login. The paths box wants paths inside the container, where
this server's /srv is `/userdata`, and `/userdata/backrest` belongs in the excludes because it
is this app's own cache. And the storage is a bill from somebody: a B2 or S3 bucket, an SFTP
account, or a disk in a friend's house.

```bash
sudo ls -lR /srv/backrest/restore
```

Assert: the restored file is there and non-empty. Print the listing. A backup nobody has restored
is a hope, and that command turns it into a fact. To restore Backrest itself: `docker compose
down`, `sudo rm -rf /srv/backrest/config /srv/backrest/data`, recreate the directories as in step
2, untar the archive back into /srv/backrest, put the Caddy block back if that was lost, then
`docker compose up -d`.

## 9. Updating later

New versions are listed at https://github.com/garethgeorge/backrest/releases. Take the step 8
archive first, then edit the image line in /srv/backrest/compose.yml to the new tag and digest:

```bash
cd /srv/backrest
docker compose pull
docker compose up -d
docker compose logs --tail 30 backrest
```

Backrest migrates its configuration file forward and rewrites it in place, so watch that log
until it settles, then re-run step 7's four checks before calling the update done.

## 10. What will probably go wrong

The paths. I typed `/srv/shlink` into the plan's path box, got an error, retyped it, and spent
several minutes convinced the read-only mount was broken. It was not: the box wants a path inside
the container, and this server's /srv is `/userdata` in there, so the answer was
`/userdata/shlink`. The autocomplete in that field is the tell, because it offers only paths the
container can see. If the first backup finishes in one second and stores nothing, that is the
same mistake wearing a different hat.

## 11. Out of scope

- Do not disable authentication and do not add a second user. This host answers on the public
  internet, and one account with a generated password is the access model.
- Do not configure command hooks. They run in this container as root with /srv mounted.
- Do not configure an rclone remote in the container. The image ships rclone, but its config
  directory is not mounted, so the remote dies with the next recreate.
- Do not set up multihost sync. It pairs this instance with another Backrest somewhere else,
  which this install does not have.
No terminal agent? Use the chat fallback — slower, you paste the commands

For ChatGPT or Claude in a browser. The model cannot touch your server, so it hands you one command at a time and you run each one. Same install, more of your evening.

This path is slower: you paste every command yourself, and there is nobody watching the output
but you. If you can run Claude Code, use the other tab.

You are installing Backrest 1.14.1 on a VPS where Prompt Zero is done: `ssh vps` works, Docker
and Caddy are installed, the firewall is default-deny. Run everything over `ssh vps` unless a
step says otherwise, and replace `<DOMAIN>` with the hostname whose A record already points at
the box.

Read this before step 1. Backrest is a web UI and a scheduler over restic, and it backs up the
files on the machine it runs on. On this server that means /srv, the data your other services
keep, and not your laptop. It also needs somewhere to send the snapshots, which is a bucket or a
box that still costs money. What you stop paying is the per-computer software fee.

## 1. Preflight

```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```

You should see: at least `1024` MB available, at least `5` G free, `amd64` or `arm64`, and your
server's IP on the last line.

If you do not: an empty last line means the A record does not exist yet. Add it, wait a minute,
and run `dig +short <DOMAIN>` again, because Caddy cannot get a certificate for a name that does
not resolve and failed attempts count against a rate limit you cannot see. If free disk is under
5 GB, stop and add disk: that 5 GB is the image plus restic's cache, which grows with the
repository index rather than with the data you copy.

## 2. Layout

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/backrest /srv/backrest/backups
sudo install -d -m 700 -o $(id -u) -g $(id -g) /srv/backrest/config
sudo install -d -m 700 /srv/backrest/data /srv/backrest/cache /srv/backrest/restore
ls -la /srv/backrest
```

You should see: five directories, `config` and `backups` owned by you, and `data`, `cache` and
`restore` at mode `drwx------` owned by root.

If you do not: leave the three root-owned ones alone. The container runs as root, because the
files it reads under /srv were written by your other services as other users, and it writes its
own state as root to match. `config` is yours because step 3 writes a file into it before the
container exists.

## 3. Secrets

One secret: the password for the web login. It is generated here, on the server, and it goes
into a file only you can read. The second half of this step is the security decision in this
install: a Backrest that starts with no configuration file writes itself a default one with
authentication disabled, and then serves every API route to whoever asks. Writing the file first
means that is never true on a public hostname.

Replace `<DOMAIN>` on the `instance` line with your hostname before you paste this.

```bash
umask 077
cat > /srv/backrest/.env <<EOF
BACKREST_USERNAME=admin
BACKREST_PASSWORD=$(openssl rand -hex 24)
EOF
chmod 600 /srv/backrest/.env
cat > /srv/backrest/config/config.json <<'EOF'
{
  "version": 6,
  "instance": "<DOMAIN>",
  "auth": {"disabled": false, "users": [{"name": "admin", "passwordBcrypt": "PLACEHOLDER"}]}
}
EOF
grep BACKREST_PASSWORD /srv/backrest/.env | cut -d= -f2- | caddy hash-password | base64 | tr -d '\n' > /srv/backrest/config/hash.tmp
awk 'NR==FNR{h=$0;next} {sub(/PLACEHOLDER/,h);print}' /srv/backrest/config/hash.tmp /srv/backrest/config/config.json > /srv/backrest/config/config.tmp
mv /srv/backrest/config/config.tmp /srv/backrest/config/config.json && rm /srv/backrest/config/hash.tmp
chmod 600 /srv/backrest/config/config.json
umask 022
ls -l /srv/backrest/.env /srv/backrest/config/config.json
grep -q PLACEHOLDER /srv/backrest/config/config.json && echo "substitution failed" || echo "hash in place"
```

You should see: two files at mode `-rw-------`, and `hash in place` on the last line.

If you do not: `substitution failed` means the `caddy hash-password` line produced nothing,
usually because Caddy is not on this box, which would also mean Prompt Zero was never run here.
A mode of `-rw-r--r--` means `umask 077` did not take effect, which happens if you pasted the
lines separately in different shells; run the two `chmod 600` commands again. Three upstream
facts hold that block together: a password is stored as a bcrypt hash that is then base64
encoded, which is what `caddy hash-password` piped into `base64` produces; `version` is required,
because a configuration file with content and no version number is rejected at start-up, and 6
is this release's migration count; and `instance` names this install inside every snapshot it
writes and cannot be changed from the UI later.

Read your password once with `sudo grep BACKREST_PASSWORD /srv/backrest/.env` and put it in your
password manager. Do not paste that file, that password, or any command output containing it
into this chat window. The chat has no reason to see it, and once it is in a transcript you do
not control it any more.

## 4. compose.yml

Paste the whole block at once, including the last two lines.

```bash
cat > /srv/backrest/compose.yml <<'EOF'
# Backrest · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
#   readme and docker ... https://github.com/garethgeorge/backrest/blob/v1.14.1/README.md
#   getting started ..... https://garethgeorge.github.io/backrest/introduction/getting-started
#   entrypoint defaults . https://github.com/garethgeorge/backrest/blob/v1.14.1/cmd/docker-entrypoint/main.go
#
# One service. Backrest is a web UI and a scheduler over restic, and the image
# ships the restic it was built against at /bin/restic, so nothing is fetched
# at start-up. It runs as root because the files it reads under /srv were
# written by other services as other users. /userdata is /srv, read-only, so a
# restore cannot go home; that is what the writable /restore is for.
#
# Tag and digest read from ghcr.io on 2026-08-06; the image publishes amd64,
# arm64, arm/v6, arm/v7 and 386.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  backrest:
    image: ghcr.io/garethgeorge/backrest:v1.14.1@sha256:b852979754281026230cc69fb11428e6d57c9a97784ab4a444ffc7934c53a215
    container_name: backrest
    restart: unless-stopped
    environment:
      BACKREST_PORT: "0.0.0.0:9898"
      BACKREST_CONFIG: /config/config.json
      BACKREST_DATA: /data
      XDG_CACHE_HOME: /cache
      BACKREST_RESTIC_COMMAND: /bin/restic
      TZ: UTC
    volumes:
      # Repository URIs, schedules and the login hash, written in step 3.
      - /srv/backrest/config:/config
      # oplog.sqlite, the JWT signing secret, the per-task logs.
      - /srv/backrest/data:/data
      # restic's cache: rebuildable, and it grows with the repository index.
      - /srv/backrest/cache:/cache
      - /srv/backrest/restore:/restore
      # The data this server holds: everything in this catalogue lives in /srv.
      - /srv:/userdata:ro
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8136.
      - "127.0.0.1:8136:9898"
EOF
cd /srv/backrest && docker compose config >/dev/null && echo "compose OK"
```

You should see: `compose OK` and nothing else.

If you do not: `services must be a mapping` means the indentation was lost between the page and
your terminal. Run `rm /srv/backrest/compose.yml` and paste again in one go. `no configuration
file provided` means the file landed somewhere other than /srv/backrest.

## 5. Caddy and TLS

This appends one site block to the Caddy config Prompt Zero installed. Replace `<DOMAIN>` in the
block with your hostname before you paste. The first line takes a copy, because a syntax error
here takes down every other site on the box.

```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-backrest
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Backrest · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://garethgeorge.github.io/backrest/cookbooks/reverse-proxy-examples and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. Upstream's own
# example is one reverse_proxy line to 9898; the headers are ours.

<DOMAIN> {
	# Caddy's default encode matcher covers text, JSON, JavaScript and SVG
	# only, so a file downloaded from a snapshot passes through untouched.
	encode zstd gzip

	# Backrest sets no transport or frame headers of its own. HSTS is on
	# because every request here carries the token that reads this server.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "SAMEORIGIN"
		Referrer-Policy "no-referrer"
		-Server
	}

	# 8136 is the loopback port compose publishes on this host. It is not a
	# container port and it is not open in the firewall.
	reverse_proxy 127.0.0.1:8136
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

You should see: `Valid configuration` from validate, and no output at all from reload.

If you do not: run `sudo cp /etc/caddy/Caddyfile.before-backrest /etc/caddy/Caddyfile`, reload,
and paste again. Caddy asks for the certificate on the first request to the hostname and renews
it on its own, so there is nothing to schedule and nothing to renew by hand.

## 6. Firewall

```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```

You should see: `Status: active`, rules for `80/tcp`, `443/tcp` and `443/udp`, and no rule
mentioning `8136` or `9898`.

If you do not: delete anything for `8136` with `sudo ufw delete allow 8136`. That port is bound
to 127.0.0.1 by the compose file, so nothing outside this box can reach it and no rule should
exist for it. 80/tcp answers the ACME challenge and redirects to HTTPS, 443/tcp is the only way
in, and 443/udp is HTTP/3, which Caddy offers by default. `Status: inactive` is a different
problem: Prompt Zero left this firewall enabled, so something has turned it off since, and
`sudo ufw enable` puts it back before you go any further.

## 7. Start and verify

```bash
cd /srv/backrest
docker compose pull
docker compose up -d
for i in $(seq 1 24); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
curl -sS https://<DOMAIN>/ | grep -o '<title>Backrest</title>'
curl -sS -o /dev/null -w '%{http_code}\n' -X POST https://<DOMAIN>/v1.Backrest/GetOperations -H 'Content-Type: application/json' -d '{}'
printf 'user = "admin:%s"\n' "$(sudo grep BACKREST_PASSWORD /srv/backrest/.env | cut -d= -f2-)" | curl -sS -o /dev/null -w '%{http_code}\n' -K - -X POST https://<DOMAIN>/v1.Backrest/GetOperations -H 'Content-Type: application/json' -d '{}'
```

You should see, in order: the loop reaching `200`, then `<title>Backrest</title>`, then `401`,
then `200`.

If you do not: the `401` is the one worth understanding. It means an API call with no credentials
was refused, so the configuration file you wrote in step 3 was read and authentication is on. A
`200` in its place means the file was not read, and you should stop and check `docker compose
logs --tail 40 backrest` before going anywhere near a browser, because the instance is open. The
last command is the opposite check: it feeds your password to curl through a config on standard
input, so the value never appears in a command line or in your shell history, and a `200` proves
the password in .env matches the hash in config.json. A `401` there means the two disagree, which
means the `caddy hash-password` line in step 3 did not do what it should have. A running
container is not success.

Now read your password with `sudo grep BACKREST_PASSWORD /srv/backrest/.env`, open
https://<DOMAIN>, and log in as `admin`. The first screen is a box headed `Login`, with
`Username` and `Password` fields and a `Log in` button.

You should see: the Backrest dashboard, empty, with `Add Repo` and `Add Plan` in the side menu.

If you do not: `Password is invalid` with the password you copied out of .env usually means a
partial copy, so select the whole value after the `=` and try once more.

## 8. First backup and restore

Two backups here, and they are not the same thing. First this install's own configuration, which
is the archive below. Then your first real snapshot, which is what you installed this for.

```bash
cd /srv/backrest
docker compose stop
sudo tar -czf /srv/backrest/backups/backrest-config-$(date +%F).tar.gz -C /srv/backrest config data compose.yml .env -C /etc/caddy Caddyfile
docker compose start
ls -lh /srv/backrest/backups/
```

You should see: one archive, a few kilobytes on a fresh install. The container is stopped for
about five seconds on purpose, because a SQLite history copied mid-write is not a backup.

If you do not: an archive of about 45 bytes means tar found nothing, so check that you are in
/srv/backrest and that step 3 wrote both files. Treat this archive as a secret from here on: it
holds the .env and, from the next step, every repository password you type.

A backup on the same disk as the data is not a backup. Run this one on your own machine, not the
server:

```bash
mkdir -p ~/backups/backrest
scp vps:/srv/backrest/backups/*.tar.gz ~/backups/backrest/
```

You should see: one file copied, and it listed by `ls -lh ~/backups/backrest/`.

If you do not: `Permission denied (publickey)` means you ran it on the server. The `vps:` prefix
only means something on your own machine, where the alias Prompt Zero created lives.

Now the part only you can do. In the web UI: `Add Repo`, and point it at the storage your
snapshots go to. Then `Add Plan`, with paths under `/userdata`. Then run that plan once, and
restore one file from the snapshot into `/restore`.

Three things while you do it. The repository password you type is a restic encryption password:
lose it and every snapshot is unreadable, including by you, so it goes in the password manager
beside the login. The paths box wants paths inside the container, where this server's /srv is
`/userdata`, and `/userdata/backrest` belongs in the excludes because it is this app's own cache
and history. And the storage you point at is a bill from somebody: a B2 or S3 bucket, an SFTP
account, or a disk in a friend's house.

```bash
sudo ls -lR /srv/backrest/restore
```

You should see: the file you restored, non-empty.

If you do not: an empty listing means the restore went somewhere else. In the restore dialog the
target path has to be `/restore`, which is the one writable place the container has, because
`/userdata` is mounted read-only on purpose. A backup nobody has restored is a hope, and that
listing is what turns it into a fact.

To restore Backrest itself: `docker compose down`, `sudo rm -rf /srv/backrest/config
/srv/backrest/data`, recreate the directories as in step 2, untar the archive back into
/srv/backrest, put the Caddy block back if that is what was lost, then `docker compose up -d`.

## 9. Updating later

New versions are listed at https://github.com/garethgeorge/backrest/releases. Take the step 8
archive first, then edit the image line in /srv/backrest/compose.yml to the new tag and digest.

```bash
cd /srv/backrest
docker compose pull
docker compose up -d
docker compose logs --tail 30 backrest
```

You should see: the version line, then the server starting, and no repeating restart.

If you do not: put the old tag and digest back and run the same three commands. Backrest
rewrites its configuration file in place as it migrates, so watch that log until it settles, then
re-run step 7's four checks before you call the update done.

## 10. What will probably go wrong

The paths. I typed `/srv/shlink` into the plan's path box, got an error, retyped it, and spent
several minutes convinced the read-only mount was broken. It was not: the box wants a path inside
the container, and this server's /srv is `/userdata` in there, so the answer was
`/userdata/shlink`. The autocomplete in that field is the tell, because it offers only paths the
container can see. If the first backup finishes in one second and stores nothing, that is the
same mistake wearing a different hat.

## 11. Out of scope

- Do not disable authentication and do not add a second user. This host answers on the public
  internet, and one account with a generated password is the access model.
- Do not configure command hooks. They run in this container as root with /srv mounted.
- Do not configure an rclone remote in the container. The image ships rclone, but its config
  directory is not mounted, so the remote dies with the next recreate.
- Do not set up multihost sync. It pairs this instance with another Backrest somewhere else,
  which this install does not have.

313 lines · 14,991 bytes

What this prompt will do
  1. Preflight
  2. Docker
  3. Layout
  4. Secrets
  5. compose.yml
  6. Nothing is public
  7. Start and verify
  8. First backup and restore
  9. Updating later
  10. What will probably go wrong
  11. Out of scope

Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.

paste it into Claude Code in a terminal on this computer · installs Docker Desktop if it is missing · no server, no domain

You are Claude Code on the user's own computer. There is no server and no Prompt Zero:
everything in this prompt runs on this machine and stays on it.

Run every command on this computer, in the shell you are already in. Nothing in this prompt
uses ssh.

Install Backrest 1.14.1 under ~/selfhost/backrest, answering at http://localhost:8136, to copy
folders from this computer to storage the user chooses.

## 1. Preflight

Say this to the user before step 2 runs. Backrest is a web UI and a scheduler over restic, and on
this path it backs up the files on this computer, which is what a backup product is meant to do.
Two consequences. A plan set for 3am runs only if the computer is awake then, so on a laptop the
honest schedule is an interval with the clock set to last run time. And the storage is a bill
from somebody: what stops is the per-computer fee.

Detect the OS and measure the machine.

```bash
uname -s
case "$(uname -s)" in
  Darwin) vm_stat | awk '/page size/{p=$8} /free|inactive/{s+=$3} END {printf "%d MB available\n", s*p/1048576}' ;;
  Linux) . /etc/os-release && echo "$ID $VERSION_CODENAME"; free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}' ;;
  MINGW*|MSYS*) powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" | awk '$1+0 {printf "%d MB available\n", $1/1024}' ;;
esac
df -h ~
```

`Darwin` is macOS, `Linux` is Linux, `MINGW` or `MSYS` is Windows under Git Bash. On Linux the
distribution ID and codename print next, for step 2. Backrest needs 1024 MB of RAM available and
5 GB free on the home disk, and the image publishes amd64 and arm64. If either floor is missed,
print both numbers and stop. Do not install and hope.

## 2. Docker

Check before installing anything:

```bash
docker info >/dev/null 2>&1 && echo "docker OK" || echo "docker MISSING"
docker compose version 2>/dev/null || true
```

If that printed `docker OK` and a compose version, skip to step 3.

Otherwise, install Docker for the OS step 1 detected:

- macOS: if `command -v brew` succeeds, run `brew install --cask docker`. If there is no
  Homebrew, STOP: tell the user to download Docker Desktop from
  https://www.docker.com/products/docker-desktop/ and install it, and wait until they
  confirm. Either way, then STOP: tell the user to open Docker Desktop once, accept its
  terms, and wait for the whale icon to say it is running. Do not continue until they
  confirm.
- Windows: run `winget install -e --id Docker.DockerDesktop`. If winget is missing or the
  install fails, STOP: tell the user to download Docker Desktop from the URL above and
  install it, and wait until they confirm. Docker Desktop configures WSL 2 itself and may
  ask for a reboot; if it does, STOP and tell the user to reboot and come back, this
  prompt resumes at this step. Then STOP: have the user open Docker Desktop, accept its
  terms, and confirm it says running.
- Linux, Debian or Ubuntu: install Docker Engine from download.docker.com's apt
  repository, with its signing key saved to a file first, never piped into a shell. The
  fence is guarded, a no-op on anything but a Linux with apt:

```bash
if [ "$(uname -s)" = "Linux" ] && command -v apt-get >/dev/null 2>&1; then
  sudo apt-get update
  sudo apt-get install -y ca-certificates curl
  sudo install -m 0755 -d /etc/apt/keyrings
  sudo curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg -o /etc/apt/keyrings/docker.asc
  sudo chmod a+r /etc/apt/keyrings/docker.asc
  echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$(. /etc/os-release && echo "$ID") $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
  sudo apt-get update
  sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
  sudo usermod -aG docker "$USER"
fi
```

  Adding the user to the docker group is root-equivalent on this machine; say that to the
  user in one sentence, and tell them the group change lands at their next login.
- Linux, anything else: STOP. Tell the user to install Docker Engine and the compose
  plugin with their distribution's package manager, and to run this prompt again once
  `docker info` works.

Assert: `docker info` exits 0 and `docker compose version` prints a version. Do not
continue without both.

## 3. Layout

```bash
mkdir -p ~/selfhost/backrest
cd ~/selfhost/backrest
mkdir -p config data cache restore backups
ls -la
```

Assert: `ls -la` shows five folders owned by the user. On macOS and Windows Docker Desktop's file
sharing owns permissions inside them; on Linux the container writes as root, which is why the
step 8 archive is made by the container.

## 4. Secrets

One secret: the password for the web login. Generate it here, print it nowhere, and keep it out
of the summary and every log line. The rest of the step is the security decision: a Backrest with
no configuration file writes itself a default one with authentication disabled, and then answers
every API call from anything on this computer that reaches the port.

```bash
cd ~/selfhost/backrest
umask 077
cat > .env <<EOF
BACKREST_USERNAME=admin
BACKREST_PASSWORD=$(openssl rand -hex 24)
EOF
chmod 600 .env
cat > config/config.json <<'EOF'
{
  "version": 6,
  "instance": "local-computer",
  "auth": {"disabled": false, "users": [{"name": "admin", "passwordBcrypt": "PLACEHOLDER"}]}
}
EOF
grep BACKREST_PASSWORD .env | cut -d= -f2- | docker run --rm -i caddy:2.11.4-alpine@sha256:5f5c8640aae01df9654968d946d8f1a56c497f1dd5c5cda4cf95ab7c14d58648 caddy hash-password | base64 | tr -d '\n' > config/hash.tmp
awk 'NR==FNR{h=$0;next} {sub(/PLACEHOLDER/,h);print}' config/hash.tmp config/config.json > config/config.tmp
mv config/config.tmp config/config.json && rm config/hash.tmp
chmod 600 config/config.json
umask 022
ls -l .env config/config.json
grep -q PLACEHOLDER config/config.json && echo "substitution failed" || echo "hash in place"
```

Assert: both files exist at mode `-rw-------`, and the last line prints `hash in place`. Backrest
stores a password as a bcrypt hash that is then base64 encoded, and the pinned Caddy image runs
for a second to produce it, because no bcrypt tool ships on all three of these systems. `version`
is required: a file with content and no version number is rejected at start-up. On Windows those
mode bits are advisory, and the real boundary is the user's own account.

Tell the user their password is in ~/selfhost/backrest/.env, readable with
`grep BACKREST_PASSWORD ~/selfhost/backrest/.env`, and that it goes in their password manager
now.

## 5. compose.yml

```bash
cat > ~/selfhost/backrest/compose.yml <<'EOF'
# Backrest · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
#   readme and docker ... https://github.com/garethgeorge/backrest/blob/v1.14.1/README.md
#   getting started ..... https://garethgeorge.github.io/backrest/introduction/getting-started
#   entrypoint defaults . https://github.com/garethgeorge/backrest/blob/v1.14.1/cmd/docker-entrypoint/main.go
#
# One service on the computer you are sitting at. Every path is relative to
# ~/selfhost/backrest/, which lets one file work on macOS, Linux and Windows.
# ../.. is the home directory two levels up, read-only: the files this install
# exists to copy somewhere safe. A restored file lands in ./restore.
# ./backups is mounted because the container writes its state as root, so on
# Linux step 8's archive is taken by the container. The VPS file needs no such
# mount: there, sudo does that job on the host.
#
# Tag and digest read from ghcr.io on 2026-08-06; the image publishes amd64,
# arm64, arm/v6, arm/v7 and 386.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  backrest:
    image: ghcr.io/garethgeorge/backrest:v1.14.1@sha256:b852979754281026230cc69fb11428e6d57c9a97784ab4a444ffc7934c53a215
    container_name: backrest
    restart: unless-stopped
    environment:
      BACKREST_PORT: "0.0.0.0:9898"
      BACKREST_CONFIG: /config/config.json
      BACKREST_DATA: /data
      XDG_CACHE_HOME: /cache
      BACKREST_RESTIC_COMMAND: /bin/restic
      TZ: UTC
    volumes:
      # Repository URIs, schedules and the login hash, from step 4.
      - ./config:/config
      - ./data:/data
      # restic's cache: rebuildable, and it grows with the repo index.
      - ./cache:/cache
      - ./restore:/restore
      # Where step 8 writes the archive of this configuration.
      - ./backups:/backups
      # Your home folder, read-only. The plan picks folders inside it.
      - ../..:/userdata:ro
    ports:
      # Loopback only: no other device on the wifi can reach 8136.
      - "127.0.0.1:8136:9898"
EOF
cd ~/selfhost/backrest && docker compose config >/dev/null && echo "compose OK"
```

Assert: that prints `compose OK`. One service, one port, six mounts, no database.

## 6. Nothing is public

No reverse proxy, no certificate, no firewall rule. Each is a decision:

- No DNS. There is no hostname, so nothing to resolve.
- No TLS. A certificate attests a public name and nothing here has one. Browsers treat
  http://localhost as a secure context, so the login form works.
- No firewall rule. Nothing is published beyond loopback, so no port needs closing.

8136 is bound to 127.0.0.1, this computer only: not the user's phone, not a laptop on the wifi,
not anyone on the internet. A backup tool loses little to that: the snapshots travel to the
repository either way. Confirm:

```bash
grep -n '127.0.0.1' ~/selfhost/backrest/compose.yml
```

Assert: one line, `- "127.0.0.1:8136:9898"`.

## 7. Start and verify

```bash
cd ~/selfhost/backrest
docker compose pull
docker compose up -d
for i in $(seq 1 24); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://localhost:8136/); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
curl -sS http://localhost:8136/ | grep -o '<title>Backrest</title>'
curl -sS -o /dev/null -w '%{http_code}\n' -X POST http://localhost:8136/v1.Backrest/GetOperations -H 'Content-Type: application/json' -d '{}'
printf 'user = "admin:%s"\n' "$(grep BACKREST_PASSWORD ~/selfhost/backrest/.env | cut -d= -f2-)" | curl -sS -o /dev/null -w '%{http_code}\n' -K - -X POST http://localhost:8136/v1.Backrest/GetOperations -H 'Content-Type: application/json' -d '{}'
```

Assert all four, and print what you received for each: the loop ends on `200`; the second prints
`<title>Backrest</title>`; the third prints `401`, the security assert here, because an
unauthenticated call is refused and step 4 therefore took effect; the fourth prints `200`, which
proves the password matches the hash and hands it to curl through a config on standard input, so
it never reaches a command line. If any of the four misses, stop, run
`docker compose logs --tail 40 backrest`, and name the cause: a container that exits in seconds
is step 4, and `port is already allocated` means something else holds 8136
(`lsof -nP -iTCP:8136 -sTCP:LISTEN`, or `netstat -ano | findstr :8136` on Windows). A running
container is not success.

STOP: tell the user to read their password with
`grep BACKREST_PASSWORD ~/selfhost/backrest/.env`, put it in their password manager, open
http://localhost:8136, and log in as `admin`. Wait. Do not continue until they confirm. The
first screen is a box headed `Login`, with `Username` and `Password` fields and a `Log in`
button.

## 8. First backup and restore

Two backups: this install's own configuration, the archive below, and then the user's first
snapshot, which is what they installed this for.

```bash
cd ~/selfhost/backrest
docker compose stop
docker compose run --rm --no-deps -T --entrypoint tar backrest -czf /backups/backrest-config-$(date +%F).tar.gz -C / config data -C /userdata/selfhost/backrest compose.yml
docker compose start
ls -lh ~/selfhost/backrest/backups/
```

Assert: the archive exists and is non-empty. Print its size. The container makes it because on
Linux it owns those files, and the stop is on purpose: a SQLite history copied mid-write is not a
backup. The archive will hold every repository password the user types.

It also sits on the same disk as the data, which is not a backup, and on a laptop the disk and
the machine fail together. Ask the user for a destination that leaves this computer, a folder
their sync service watches or a USB stick, and copy it there with `cp`; in Git Bash a Windows
drive is written `/d/Backups`. Assert: the user confirms the file is listed there.

STOP: tell the user to open http://localhost:8136, click `Add Repo` and set up the storage their
snapshots go to, then `Add Plan` with paths under `/userdata`, run that plan once, and restore
one file from the snapshot into `/restore`. Wait. Do not continue until they confirm. Two things
to tell them. The repository password they type is a restic encryption password: lose it and
every snapshot is unreadable, by them too, so it goes in the password manager beside the login.
And the paths box wants paths inside the container, where the home folder is `/userdata`, so
Documents is `/userdata/Documents`.

```bash
ls -lR ~/selfhost/backrest/restore
```

Assert: the restored file is there and non-empty. Print the listing. A backup nobody has restored
is a hope, and that turns it into a fact. To restore this install itself: `docker compose
down`, delete `config` and `data`, recreate them as in step 3, then run the container command
above with `-xzf` in place of `-czf`, unpacking into `/`.

## 9. Updating later

New versions are listed at https://github.com/garethgeorge/backrest/releases. Take the step 8
archive first, then edit the image line in ~/selfhost/backrest/compose.yml to the new pin:

```bash
cd ~/selfhost/backrest
docker compose pull
docker compose up -d
docker compose logs --tail 30 backrest
```

Backrest rewrites its configuration in place as it migrates, so watch that log until it settles,
then re-run step 7's four checks.

## 10. What will probably go wrong

The schedule, quietly. I set a plan for 3am, came back a week later, and the history showed two
backups instead of seven. Nothing was broken: this computer was asleep at 3am on five of those
nights, and a wall-clock schedule that comes due while the machine is off does not run late. It
does not run. The fix is in the plan: a daily interval with the clock set to last run time, which
fires when the lid opens. Check the history after the first week.

## 11. Out of scope

- Do not expose this to the internet.
- Do not configure port forwarding on the router.
- Do not add a reverse proxy or TLS.
- Do not disable authentication or add a second user. One generated password is the model.
- Do not configure command hooks or an rclone remote in the container. Hooks run as root with
  the home folder mounted, and rclone's config directory is not mounted, so a remote configured
  there dies with the next recreate.
compose.local.ymlthe services, pinned · local layout45 lines

authored from upstream docs, never pasted · 2,096 bytes

# Backrest · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
#   readme and docker ... https://github.com/garethgeorge/backrest/blob/v1.14.1/README.md
#   getting started ..... https://garethgeorge.github.io/backrest/introduction/getting-started
#   entrypoint defaults . https://github.com/garethgeorge/backrest/blob/v1.14.1/cmd/docker-entrypoint/main.go
#
# One service on the computer you are sitting at. Every path is relative to
# ~/selfhost/backrest/, which lets one file work on macOS, Linux and Windows.
# ../.. is the home directory two levels up, read-only: the files this install
# exists to copy somewhere safe. A restored file lands in ./restore.
# ./backups is mounted because the container writes its state as root, so on
# Linux step 8's archive is taken by the container. The VPS file needs no such
# mount: there, sudo does that job on the host.
#
# Tag and digest read from ghcr.io on 2026-08-06; the image publishes amd64,
# arm64, arm/v6, arm/v7 and 386.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  backrest:
    image: ghcr.io/garethgeorge/backrest:v1.14.1@sha256:b852979754281026230cc69fb11428e6d57c9a97784ab4a444ffc7934c53a215
    container_name: backrest
    restart: unless-stopped
    environment:
      BACKREST_PORT: "0.0.0.0:9898"
      BACKREST_CONFIG: /config/config.json
      BACKREST_DATA: /data
      XDG_CACHE_HOME: /cache
      BACKREST_RESTIC_COMMAND: /bin/restic
      TZ: UTC
    volumes:
      # Repository URIs, schedules and the login hash, from step 4.
      - ./config:/config
      - ./data:/data
      # restic's cache: rebuildable, and it grows with the repo index.
      - ./cache:/cache
      - ./restore:/restore
      # Where step 8 writes the archive of this configuration.
      - ./backups:/backups
      # Your home folder, read-only. The plan picks folders inside it.
      - ../..:/userdata:ro
    ports:
      # Loopback only: no other device on the wifi can reach 8136.
      - "127.0.0.1:8136:9898"

agent-readable mirror: /self-host/backblaze-personal-backup.md

The files, if you'd rather do it yourself

The cloud path with no agent involved: three files, in the order you'd use them. The cloud prompt above writes exactly these — if the two ever disagree, the files are the ones CI diffs. The local path ships its own compose file, collapsed under its own prompt.

compose.ymlthe services, pinned42 lines

authored from upstream docs, never pasted · 2,003 bytes

# Backrest · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
#   readme and docker ... https://github.com/garethgeorge/backrest/blob/v1.14.1/README.md
#   getting started ..... https://garethgeorge.github.io/backrest/introduction/getting-started
#   entrypoint defaults . https://github.com/garethgeorge/backrest/blob/v1.14.1/cmd/docker-entrypoint/main.go
#
# One service. Backrest is a web UI and a scheduler over restic, and the image
# ships the restic it was built against at /bin/restic, so nothing is fetched
# at start-up. It runs as root because the files it reads under /srv were
# written by other services as other users. /userdata is /srv, read-only, so a
# restore cannot go home; that is what the writable /restore is for.
#
# Tag and digest read from ghcr.io on 2026-08-06; the image publishes amd64,
# arm64, arm/v6, arm/v7 and 386.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  backrest:
    image: ghcr.io/garethgeorge/backrest:v1.14.1@sha256:b852979754281026230cc69fb11428e6d57c9a97784ab4a444ffc7934c53a215
    container_name: backrest
    restart: unless-stopped
    environment:
      BACKREST_PORT: "0.0.0.0:9898"
      BACKREST_CONFIG: /config/config.json
      BACKREST_DATA: /data
      XDG_CACHE_HOME: /cache
      BACKREST_RESTIC_COMMAND: /bin/restic
      TZ: UTC
    volumes:
      # Repository URIs, schedules and the login hash, written in step 3.
      - /srv/backrest/config:/config
      # oplog.sqlite, the JWT signing secret, the per-task logs.
      - /srv/backrest/data:/data
      # restic's cache: rebuildable, and it grows with the repository index.
      - /srv/backrest/cache:/cache
      - /srv/backrest/restore:/restore
      # The data this server holds: everything in this catalogue lives in /srv.
      - /srv:/userdata:ro
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8136.
      - "127.0.0.1:8136:9898"
Caddyfilethe hostname and TLS29 lines

authored from upstream docs, never pasted · 1,102 bytes

# Backrest · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://garethgeorge.github.io/backrest/cookbooks/reverse-proxy-examples and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. Upstream's own
# example is one reverse_proxy line to 9898; the headers are ours.

<DOMAIN> {
	# Caddy's default encode matcher covers text, JSON, JavaScript and SVG
	# only, so a file downloaded from a snapshot passes through untouched.
	encode zstd gzip

	# Backrest sets no transport or frame headers of its own. HSTS is on
	# because every request here carries the token that reads this server.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "SAMEORIGIN"
		Referrer-Policy "no-referrer"
		-Server
	}

	# 8136 is the loopback port compose publishes on this host. It is not a
	# container port and it is not open in the firewall.
	reverse_proxy 127.0.0.1:8136
}
install.shthe same install, no agent184 lines

authored from upstream docs, never pasted · 8,753 bytes

#!/usr/bin/env bash
# Backrest · the agent-free install.
#
# Everything prompt.md tells an agent to do, as a script you can read first.
# Run it on the VPS, as a non-root user who is in the docker group:
#
#   DOMAIN_HOST=backup.example.com ./install.sh
#
# Authored by caniselfhostit from the upstream documentation:
#   https://github.com/garethgeorge/backrest/blob/v1.14.1/README.md
#   https://garethgeorge.github.io/backrest/introduction/getting-started
#   https://garethgeorge.github.io/backrest/docs/operations
#   https://github.com/garethgeorge/backrest/blob/v1.14.1/cmd/docker-entrypoint/main.go
#
# One secret is generated here, on this machine: the password for the web login.
# It goes into /srv/backrest/.env with mode 600 and is never printed. Its bcrypt
# hash goes into /srv/backrest/config/config.json before the container has ever
# run, because a Backrest that starts with no configuration file writes itself a
# default one with authentication disabled and then serves every API route to
# whoever asks.
#
# The repository passwords you type into the UI later are restic encryption
# passwords. Nothing here can recover one. Keep them where you keep this login.
#
# NOT YET VERIFIED: no harness run has been recorded against this script.
set -euo pipefail

APP_DIR="${APP_DIR:-/srv/backrest}"
DOMAIN_HOST="${DOMAIN_HOST:-}"
PORT="8136"

die() { printf 'install.sh: %s\n' "$1" >&2; exit 1; }

# --- 1. Refuse to start on a machine that is not ready -----------------------

[ -n "$DOMAIN_HOST" ] || die "set DOMAIN_HOST to the hostname you pointed at this server, e.g. backup.example.com"
command -v docker >/dev/null 2>&1 || die "docker is not installed. Run Prompt Zero first."
docker compose version >/dev/null 2>&1 || die "the docker compose plugin is missing"
command -v caddy >/dev/null 2>&1 || die "caddy is not installed on the host. Run Prompt Zero first."
command -v openssl >/dev/null 2>&1 || die "openssl is not installed"

avail_mb="$(free -m | awk '/^Mem:/ {print $7}')"
[ "$avail_mb" -ge 1024 ] || die "only ${avail_mb} MB of RAM available; Backrest and restic want 1024 MB"
avail_gb="$(df -BG --output=avail /srv | tail -1 | tr -dc '0-9')"
[ "$avail_gb" -ge 5 ] || die "only ${avail_gb} GB free on /srv; this install wants 5 GB for the image and restic's cache"

resolved="$(getent hosts "$DOMAIN_HOST" | awk '{print $1; exit}' || true)"
[ -n "$resolved" ] || die "$DOMAIN_HOST does not resolve yet. Add the A record, wait a minute, run this again."

# --- 2. Lay the files out ----------------------------------------------------
#
# config and backups belong to the login user; data, cache and restore are
# written by the container, which runs as root because the files it reads under
# /srv were written by other services as other users.

sudo install -d -m 750 -o "$(id -u)" -g "$(id -g)" "$APP_DIR" "$APP_DIR/backups"
sudo install -d -m 700 -o "$(id -u)" -g "$(id -g)" "$APP_DIR/config"
sudo install -d -m 700 "$APP_DIR/data" "$APP_DIR/cache" "$APP_DIR/restore"
install -m 0644 "$(dirname "$0")/compose.yml" "$APP_DIR/compose.yml"
install -m 0644 "$(dirname "$0")/Caddyfile" "$APP_DIR/Caddyfile"

# --- 3. Generate the one secret, on the server -------------------------------
#
# Hex rather than base64, because a human types this one into a login form.
# Read it later with
#   sudo grep BACKREST_PASSWORD /srv/backrest/.env
# Backrest stores a password as a bcrypt hash that is then base64 encoded, and
# `caddy hash-password` piped into `base64` is exactly that. `version` is
# required: a configuration file with content and no version number is rejected
# at start-up, and 6 is the migration count this release ships.

if [ ! -f "$APP_DIR/.env" ]; then
	umask 077
	cat > "$APP_DIR/.env" <<-ENVFILE
		BACKREST_USERNAME=admin
		BACKREST_PASSWORD=$(openssl rand -hex 24)
	ENVFILE
	chmod 600 "$APP_DIR/.env"
	umask 022
fi

if [ ! -f "$APP_DIR/config/config.json" ]; then
	umask 077
	cat > "$APP_DIR/config/config.json" <<-'CONFIGJSON'
		{
		  "version": 6,
		  "instance": "INSTANCE_NAME",
		  "auth": {"disabled": false, "users": [{"name": "admin", "passwordBcrypt": "PLACEHOLDER"}]}
		}
	CONFIGJSON
	grep BACKREST_PASSWORD "$APP_DIR/.env" | cut -d= -f2- | caddy hash-password | base64 | tr -d '\n' > "$APP_DIR/config/hash.tmp"
	awk -v host="$DOMAIN_HOST" 'NR==FNR{h=$0;next} {sub(/PLACEHOLDER/,h);sub(/INSTANCE_NAME/,host);print}' \
		"$APP_DIR/config/hash.tmp" "$APP_DIR/config/config.json" > "$APP_DIR/config/config.tmp"
	mv "$APP_DIR/config/config.tmp" "$APP_DIR/config/config.json"
	rm "$APP_DIR/config/hash.tmp"
	chmod 600 "$APP_DIR/config/config.json"
	umask 022
fi
if grep -q PLACEHOLDER "$APP_DIR/config/config.json"; then
	die "the bcrypt hash was not substituted into config.json"
fi

cd "$APP_DIR"
docker compose config >/dev/null

# --- 4. Caddy site block, on the host ----------------------------------------

if ! sudo grep -qF "$DOMAIN_HOST {" /etc/caddy/Caddyfile; then
	sudo cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.before-backrest"
	printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
	sed "s|<DOMAIN>|${DOMAIN_HOST}|g" "$APP_DIR/Caddyfile" | sudo tee -a /etc/caddy/Caddyfile >/dev/null
fi
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

# --- 5. Ports: two open, and 8136 is not one of them -------------------------

if command -v ufw >/dev/null 2>&1; then
	echo "==> 80/tcp and 443/tcp for Caddy, 443/udp for HTTP/3; ${PORT} stays closed on loopback"
	sudo ufw allow 80/tcp
	sudo ufw allow 443/tcp
	sudo ufw allow 443/udp
	sudo ufw status verbose
fi

# --- 6. Start it -------------------------------------------------------------

docker compose pull
docker compose up -d

echo "==> waiting for https://${DOMAIN_HOST}/"
for _ in $(seq 1 24); do
	code="$(curl -sS -o /dev/null -w '%{http_code}' "https://${DOMAIN_HOST}/" || true)"
	[ "$code" = "200" ] && break
	sleep 5
done
[ "${code:-}" = "200" ] || die "https://${DOMAIN_HOST}/ answered ${code:-nothing}. Check: docker compose logs --tail 40 backrest"

curl -sS "https://${DOMAIN_HOST}/" | grep -q '<title>Backrest</title>' \
	|| die "the page at https://${DOMAIN_HOST}/ is not Backrest's. Check the Caddy site block from step 4."

# The API must refuse a call with no credentials. If this returns 200 the
# configuration file was not read and the instance is open to the internet.
unauth="$(curl -sS -o /dev/null -w '%{http_code}' -X POST "https://${DOMAIN_HOST}/v1.Backrest/GetOperations" \
	-H 'Content-Type: application/json' -d '{}' || true)"
[ "$unauth" = "401" ] || die "an unauthenticated API call returned ${unauth}, not 401. Stop: authentication is off."

# And it must accept the generated one. The password goes to curl through a
# config on standard input, so it never appears in a command line or a log.
authed="$(printf 'user = "admin:%s"\n' "$(sudo grep BACKREST_PASSWORD "$APP_DIR/.env" | cut -d= -f2-)" \
	| curl -sS -o /dev/null -w '%{http_code}' -K - -X POST "https://${DOMAIN_HOST}/v1.Backrest/GetOperations" \
	-H 'Content-Type: application/json' -d '{}' || true)"
[ "$authed" = "200" ] || die "the generated login returned ${authed}, not 200. The hash and the password disagree."

# --- 7. The first backup, before day one ends --------------------------------

STAMP="$(date +%Y%m%d-%H%M%S)"
docker compose stop
sudo tar -czf "$APP_DIR/backups/backrest-config-${STAMP}.tar.gz" -C "$APP_DIR" config data compose.yml .env -C /etc/caddy Caddyfile
docker compose start
ls -lh "$APP_DIR/backups/"
[ -s "$APP_DIR/backups/backrest-config-${STAMP}.tar.gz" ] || die "the configuration archive is empty"

cat <<-DONE

	Backrest is answering at https://${DOMAIN_HOST}/ and refusing unauthenticated API calls.

	  1. Your login is admin. The password is in $APP_DIR/.env, mode 600. Read it with
	       sudo grep BACKREST_PASSWORD $APP_DIR/.env
	     and put it in your password manager. It was not printed here.
	  2. Nothing is backed up yet. Open https://${DOMAIN_HOST}/, log in, then Add Repo
	     for the storage your snapshots go to, then Add Plan for the paths. This
	     server's /srv is /userdata inside the container, and /userdata/backrest
	     belongs in the excludes: it is this app's own cache and history.
	  3. The repository password you type there is a restic encryption password.
	     Lose it and every snapshot is unreadable, including by you.
	  4. Restore one file from your first snapshot into /restore and look at it with
	       sudo ls -lR $APP_DIR/restore
	     A backup nobody has restored is a hope.
	  5. First backup of this install written to $APP_DIR/backups. It holds .env and
	     config.json, so it holds every repository password. It is on the same disk as
	     the data, which is not a backup. Copy it somewhere else tonight.

DONE

What you're signing up for

The part a vendor's comparison page leaves out. None of it is a reason not to do this; all of it is yours the moment you cancel Backblaze Computer Backup.

  • Unlimited storage for a flat fee per computer is genuinely hard to beat, and this does not beat it. Backblaze charges one price whether you have 200 GB or 8 TB; a bucket charges by the gigabyte every month, forever. Below a few hundred gigabytes you are usually ahead, above a terabyte you are usually not, and the honest saving is the software fee rather than the storage bill.
  • You become the person who tests restores. Nothing here emails you when a plan has not run in three weeks, and a backup nobody has restored is a hope rather than a backup. The install ends by making you restore one file, and that habit is the whole product.
  • The repository password is unrecoverable by design. restic encrypts before anything leaves the machine, which is the good news and the bad news: lose that password and the snapshots are noise to you as much as to anyone else. It belongs in a password manager on the day you create it, next to a copy of config.json.
  • This backs up the machine it runs on. On a server that is the server's data; on your own computer it is your own files. It is not an agent for the household's other laptops, and Backblaze's per-computer licence covering every drive attached to that computer has no equivalent here.
  • No mailed hard drive when the restore is 4 TB, no phone app that finds a file from the car, and no support desk. Those are what the subscription is actually selling, and none of them arrives with the container.

Where this came from

“Make sure to save a copy of your repository credentials and encryption keys (e.g. password) in a safe place. Losing these will prevent you from restoring your data.”

  • Backrest runs every operation through restic, and the published image ships the restic binary it was built against, so nothing is downloaded at start-up. source
  • A Backrest that starts with no configuration file writes itself a default one with authentication disabled, and the request middleware serves every API route when the auth block is absent or disabled. source
  • The docker entrypoint defaults BACKREST_PORT to 0.0.0.0:9898, BACKREST_DATA to /data, BACKREST_CONFIG to /config/config.json and XDG_CACHE_HOME to /cache. source
  • A web login password is stored in config.json as a bcrypt hash that has then been base64 encoded, which is what caddy hash-password piped into base64 produces. source
  • A hand-written configuration file has to carry a version number: a config with content and a version of 0 is rejected at start-up rather than migrated. source

Questions people actually ask

Answered from this page's own data — the same numbers, in sentences.

  • Can I self-host Backblaze Computer Backup?

    Not Backblaze Computer Backup itself — the vendor does not ship a version you can run on your own server. What you can self-host is the job people pay it for, and the answer to that is Backrest. A web UI and a scheduler over restic, so the backups you keep meaning to set up have somewhere to be configured and something to watch them. The install is one command: one container behind Caddy with automatic TLS, secrets generated on the server rather than in a chat window, and a first backup taken before the agent says it is done, in about 10 minutes. The prompt on this page does it; the compose.yml, Caddyfile and install.sh below do the same install with no agent at all.

  • What replaces Backblaze Computer Backup?

    Backrest. A web UI and a scheduler over restic, so the backups you keep meaning to set up have somewhere to be configured and something to watch them. The closest honest answer, as long as you understand the swap. Backblaze backs up your computer to their cloud; Backrest is a web UI and a scheduler over restic, so it backs up whatever machine it runs on to storage you choose, and it is the storage that still costs money. Run it on your own computer and you have replaced the software, kept the encryption, and taken on being the person who tests restores. Run it on a server and it is backing up the server, which is a different job Backblaze never did for you. Backrest is GPL-3.0-licensed and free; nothing on this page is a hosted service we sell you.

  • What does self-hosting cost compared to Backblaze Computer Backup?

    1024 MB of RAM and 5 GB of disk — the smallest tier most VPS hosts sell, about $5 a month. Backrest itself is free and GPL-3.0-licensed; the bill is the server, plus a domain you probably already own. What you stop paying: Backblaze Computer Backup Personal Backup, $9/mo — $108 a year.

  • How hard is it really?

    ONE COMMAND — under 10 minutes. The rule that produced that verdict: one container, no database, no outside integration, at most one secret. Nothing to negotiate with anyone else, nothing to back up separately, at most one secret to generate. This is the case where the compose file honestly is the whole install. The tier is derived from seven countable facts about the Backrest install, not from anyone's impression of it, and the whole rubric is published on the methodology page.

  • Can I run Backrest on my own computer instead of a server?

    Yes — that is the second path in the prompt box above. "On my computer" installs the same Backrest on the machine you are sitting at: no VPS, no domain, no DNS, and nothing exposed to the internet. It checks for Docker first and installs Docker Desktop if the machine does not have it — macOS, Windows and Linux each get their own step — then binds everything to loopback, so the app answers on http://localhost and only on that computer. Worth knowing: This is the path Backrest was built for, with one consequence worth saying out loud: a plan scheduled for 3am runs only if the computer is awake at 3am, so on a laptop the honest setting is an interval that fires when the lid opens. Same discipline as the cloud path: pinned images, secrets generated on the machine, and a first backup taken before the prompt says it is done.

Content last checked 2026-08-07. Verdicts are derived from the published rubric on /methodology; corrections go through the issue tracker.