Skip to content

Get started as an administrator

This part of the documentation is for the person who installs, configures and operates Restow for an organization, or, in the Service Provider edition, for several tenants. If you are looking after only your own mailbox, see For end users → Get started instead.

Getting a running installation is two steps, covered on this page: requirements, then install, either with the install script (recommended) or by hand with Docker Compose. After that, Setup wizard walks through the guided first run, and First steps covers connecting a source and proving your first backup.

Run Restow on a dedicated Linux VM (what it needs is listed below). Put that VM on other hardware than the systems it backs up: a backup that fails together with the host it protects is no backup. Keep the backups off-site as well, preferably on S3-compatible object storage with Object Lock; the default target, a Docker volume on the VM itself, is meant for a first test.

  • LXC containers work on a best-effort basis only: Docker needs nesting and keyctl there, and some hosts still break it.
  • Running Restow natively, without Docker, is not supported.
  • On any other Linux system with Docker, install by hand with Docker Compose; the install script supports only the systems below.

Before it changes anything, the install script checks the points below. A failed check stops it and nothing is changed; a warning does not.

What Requirement The install script
Operating system Debian 12 or 13; Ubuntu 22.04, 24.04 or 26.04 LTS Stops on any other system.
Architecture amd64 or arm64 Stops on any other.
Machine A dedicated VM. LXC, OpenVZ and other containers work on a best-effort basis only (in LXC, Docker needs nesting and keyctl, and some hosts still break it). Stops inside Docker, Podman and WSL; warns in LXC, OpenVZ and other containers.
Memory At least 4 GiB assigned to the VM, 8 GiB recommended (mail parsing helper processes use up to 512 MB each, see IMPORT_PARSE_WORKERS in Environment variables). From 0.2.0: counts what the system reports plus the memory the kernel reserves for kdump, warns below 7 GiB (a VM with 8 GiB shows a little less) and stops below 3 GiB. The 0.1.0 script stops below 3.5 GiB of what the system reports. See Memory on Proxmox and other hypervisors.
Disk At least 1 GiB free for /opt/restow; at least 10 GiB free for Docker’s data directory (/var/lib/docker by default; images and database), 50 GiB or more recommended, because the backups go into a Docker volume there until you add another storage target. Stops below 1 GiB for /opt/restow or below 10 GiB for Docker; warns below 50 GiB for Docker. From 0.2.0, on an LVM volume whose volume group has unused space, it prints the command that grows it, see Disk on Ubuntu with LVM.
Clock Synchronised (NTP): certificates, signature checks and authenticator codes need the right time. Warns if it is not.
Rights root, or sudo Stops without.
Tools curl, awk, sed, od, base64, tr, df and mktemp Stops if one is missing.
Docker Installed by the script from Docker’s apt repository when it is missing. Docker from snap is not supported. An installed Docker needs Engine 24.0 and Compose 2.20 or newer. Stops on Docker from snap, on an older version, and when Docker is missing and packages that conflict with Docker’s own are installed (for example docker.io, containerd, runc).
Outbound HTTPS Port 443 to github.com, ghcr.io, registry-1.docker.io, tuf-repo-cdn.sigstore.dev (signature checks) and, when Docker is missing, download.docker.com Stops if one does not answer.
Images The images of the build and version you install can be pulled from ghcr.io without a login. From 0.2.0: asks the registry before it changes anything and stops if an image is not public or does not exist (check --version and --edition). The 0.1.0 script only reports a failed pull, after Docker is installed.
Ports 80 and 443 free on the host (the Caddy edge, see Ports). Behind a reverse proxy (from 0.2.0) only the one port the proxy forwards to: 443, or 80 for the plain HTTP hop. A journal receiver for Exchange Online and for copies that a mail provider forwards (Business and Service Provider) needs one more port, usually 25; you set it up after the installation, see Exchange journaling. Stops if 80 or 443 is in use (behind a reverse proxy: if the one port it needs is). Does not check the journal port.
Domain Public installation: a domain whose A or AAAA record points at the server, with ports 80 and 443 reachable from the internet; Caddy gets the Let’s Encrypt certificate there (see DNS and TLS). Not needed with --local (evaluation only). Behind a reverse proxy: the name your proxy serves; this host needs no DNS record of its own and no inbound port from the internet. Warns if the domain does not resolve to this host. Behind a reverse proxy (from 0.2.0) the name points at the proxy, so DNS is not checked.

This section is about the memory of the virtual machine that runs Restow. To back up the VMs and containers of Proxmox VE with Restow (a preview), see Proxmox VE backup.

A VM sees less memory than it is given: the kernel and the firmware keep some, and where kdump (crash dumps) is enabled, as it can be on Ubuntu, the kernel sets aside another 320 to 512 MB for a crash kernel (crashkernel= on the kernel command line; Ubuntu’s default is 320 MB for a machine with 2 to 4 GiB and 512 MB for 4 to 32 GiB). free does not count that memory. From 0.2.0 the install script adds it back before it judges the memory, so a VM with 4 GiB, which can show as little as 3.3 GiB, only gets a warning and the installation goes on.

  • Assign at least 4 GiB to the VM, 8 GiB recommended.
  • With ballooning (Proxmox VE: a “Minimum memory” below “Memory”), the VM can be left with less than you assigned. Set “Minimum memory” equal to “Memory”.
  • After you change the memory, shut the VM down and start it again. A reboot from inside the VM does not apply the change.
  • Check inside the VM: free -h shows what the system sees, and cat /sys/kernel/kexec_crash_size the bytes reserved for kdump (0 or no such file: nothing is reserved; newer kernels also offer /sys/kernel/kexec/crash_size).

Ubuntu Server’s installer sets up LVM and often makes the root logical volume smaller than the disk: on a 30 GB disk, / can be 15 GB and the rest of the volume group stays unused. Docker’s data directory (/var/lib/docker) lives on /, so the disk check can stop the install script on a VM whose disk is large enough. Check how much of the volume group is unused (column VFree), then grow the root volume and its file system in one step:

Terminal window
sudo vgs
sudo lvextend -r -l +100%FREE /dev/ubuntu-vg/ubuntu-lv

ubuntu-vg and ubuntu-lv are Ubuntu’s default names; sudo lvs shows yours, and from 0.2.0 the install script prints this command with the right names when it finds unused space. If VFree is 0, there is nothing to extend: the disk itself has to be larger.

Restow runs as one application image with three roles (api, worker and scheduler), a web image with the Caddy edge, and PostgreSQL 16, all defined in one Compose file. You need:

  • A Linux host with Docker Engine and the Docker Compose plugin (the docker compose command, not the older standalone docker-compose v1). The install script installs both from Docker’s signed apt repository when Docker is missing. The release images are built for amd64 and arm64.
  • Enough disk space for PostgreSQL’s data volume and, if you use the default local storage target, the chunk store (STORAGE_LOCAL_PATH, /data/chunks inside the container). Storage needs grow with how much data you protect and how long you retain it, reduced by deduplication within a tenant; an S3-compatible target or a mounted NFS share moves that growth off the Restow host. See Backups and schedules.

Restow 0.4.7 is a public beta: test it before you rely on it, and keep an independent backup alongside it while it is new. You install it from the published release images (signed, with an SBOM), in one of two builds: the full images ghcr.io/restow-backup/restow and ghcr.io/restow-backup/restow-web, or the Community images ghcr.io/restow-backup/restow-community and ghcr.io/restow-backup/restow-web-community, as described under Install, below.

See the 0.4.7 release notes for what shipped and what is still known-incomplete. The source code is on GitHub. Verifying a release says what the release checks covered and what they did not: Microsoft 365 backup and restore have not yet been run against a real Microsoft 365 tenant (they are covered by tests against a simulated Graph API), and the endpoint agent for macOS was not part of the release checks.

  • Public mode (an installation reachable from the internet): a domain name with A and/or AAAA records pointing at the server’s IP address, and inbound ports 80 and 443 reachable from the internet. Caddy obtains and renews a Let’s Encrypt certificate automatically for the domain you set as RESTOW_APP_DOMAIN, with no manual certificate handling.
  • Local mode (reachable only over IP or localhost): no domain and no inbound internet access needed. This is the development/evaluation mode; passkeys are not offered in it (see Setup wizard).
  • Behind a reverse proxy (the proxy holds the name and the certificate): a public installation as far as the wizard is concerned, with https://<name> as its public URL, but Restow itself needs no DNS record, no certificate request and no inbound port from the internet. See Behind a reverse proxy.

Operating mode is chosen once in the setup wizard and can be changed later in Settings.

Only 80 and 443 (Caddy) need to be reachable from outside the host in public mode (behind a reverse proxy: only the proxy has to reach one of them, see Behind a reverse proxy); the release stack binds the api container’s own port to loopback only and publishes no PostgreSQL port. Servers and clients backed up with the Restow agent reach the same address over HTTPS; they need no other port. The full table is in Reference → Ports and roles.

Restow does not yet publish official sizing benchmarks (early beta), and Microsoft Graph’s own throttling is usually the limiting factor for how fast a first backup of a large tenant completes (it can take days rather than hours; Restow shows the wait, it does not hide it). What’s true by design:

  • PostgreSQL holds metadata, the job queue and the audit log, not the backed-up content itself, which lives in the chunk store.
  • Worker throughput is configurable: WORKER_CONCURRENCY (parallel jobs per queue in one worker process) and WORKER_TENANT_CONCURRENCY (parallel jobs per tenant), both default 2. Run more than one worker container to scale further.
  • Storage grows with retained, deduplicated, encrypted data; a weekly sample scrub and a monthly full scrub check pack integrity regardless of target size.

Every release carries install.sh, which sets up the release stack on a VM as described under Platform, in /opt/restow. Download it with its checksum file, check it, then run it:

Terminal window
curl -fsSLO https://github.com/restow-backup/restow/releases/download/v0.4.7/install.sh
curl -fsSLO https://github.com/restow-backup/restow/releases/download/v0.4.7/install.sh.sha256
sha256sum -c install.sh.sha256
sudo bash install.sh

install.sh.sha256 only proves that the download is complete. To check that the script is the one the release workflow published, verify the signed checksum list of the release with cosign first (see Verifying a release):

Terminal window
curl -fsSLO https://github.com/restow-backup/restow/releases/download/v0.4.7/SHA256SUMS
curl -fsSLO https://github.com/restow-backup/restow/releases/download/v0.4.7/SHA256SUMS.sigstore.json
cosign verify-blob SHA256SUMS --bundle SHA256SUMS.sigstore.json \
--certificate-identity https://github.com/restow-backup/restow/.github/workflows/release.yml@refs/tags/v0.4.7 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
sha256sum -c --ignore-missing SHA256SUMS

The same script can also be fetched and run in one line:

Terminal window
curl -fsSL https://github.com/restow-backup/restow/releases/latest/download/install.sh | sudo bash

Unattended, without questions:

Terminal window
curl -fsSL https://github.com/restow-backup/restow/releases/latest/download/install.sh | sudo bash -s -- --non-interactive --domain backup.example.com --edition full

With --non-interactive the script does not show the master key. Copy RESTOW_MASTER_KEY from /opt/restow/.env to an offline place right away (see Options).

The download, check and run path above stays the recommended one. With the one-liner, the script runs as root as soon as it arrives, so nothing checks it against the release’s signed checksum list first; you trust the connection to GitHub instead. Either way the script itself checks the release files and images it downloads (checksums and cosign signatures), and it does nothing until it has been downloaded completely.

latest or a fixed version. releases/latest/download/install.sh is a redirect to the newest release, so it always fetches the script of the newest version, and that script installs its own version (today 0.4.7). releases/download/v0.4.7/install.sh always fetches the script of exactly 0.4.7, which installs 0.4.7. Use the fixed form when servers or instructions must stay on a known version; pre-releases are never latest.

All commands for operating an installation are collected in the command reference.

  1. On a terminal and without options, it starts with a short introduction (what it will do, about 10 to 20 minutes, what you need), from 0.2.0. Then it checks the machine: root rights, operating system, architecture, virtualization, memory, disk, ports 80 and 443, outbound HTTPS, the clock, the domain’s DNS record, whether the images can be pulled without a login (from 0.2.0), and an earlier installation. If a check fails, it stops and changes nothing. What the install script checks lists what each check needs.
  2. It asks how Restow is reached (from 0.2.0): 1 public, with its own certificate (a domain that points at the server, ports 80 and 443 open to the internet); 2 behind a reverse proxy you already run, which holds the name and the certificate; 3 a local evaluation, the same as --local. It asks for the domain (for option 2 also for the proxy’s address) and the build (full or Community, see the two builds) and shows its plan before it changes anything. The 0.1.0 script asks only for the domain and the build.
  3. It installs Docker Engine and the Compose plugin from Docker’s signed apt repository if Docker is missing. Docker from snap is refused.
  4. It downloads docker-compose.yml and env.example of the release and checks them against the release’s SHA256SUMS and its cosign signature.
  5. It writes /opt/restow/.env (mode 0600) with secrets from the kernel’s random generator.
  6. It shows RESTOW_MASTER_KEY once, on the terminal and never in its log, and asks you to confirm that you stored it offline. Store it offline then: it is not shown again (it stays in .env).
  7. It pulls the two images of the chosen build, checks their cosign signatures against the release workflow of exactly that version (an image without a valid signature is removed again, and nothing is started) and starts the stack. Since 0.4.0 it also starts the mounter (Compose profile mounts), because file share backup runs through it; --no-mounter leaves it off (see Options).
  8. At the end it waits until the api is healthy, reads the one-time setup token from the api’s log and prints, as its last lines, a box with the address to open and the token, from 0.2.0. The token goes to the terminal only, never to the install log, and a run whose output is collected (no terminal) prints how to read it instead. It stays valid until the setup is finished; read it again on the server with cd /opt/restow && sudo docker compose logs api | grep 'SETUP TOKEN'. The 0.1.0 script prints the setup URL and that command. The setup wizard first asks for the language, then for the token.

Its log is /var/log/restow-install.log, without secrets.

bash install.sh --help lists every option and the exit codes. The ones you are most likely to need:

Option Effect
--local An evaluation without a public domain: the edge serves https://localhost or an internal name such as restow.internal over HTTPS, with a certificate from Caddy’s own authority. Browsers warn about it, and passkeys are not offered. Not for production.
--with-updater Also starts the opt-in updater, which stays off unless you ask for it. It mounts the Docker socket: read Updates first.
--no-mounter (or --without-mounter) Since 0.4.0 a new installation starts the mounter by default (Compose profile mounts): file share backup runs through it, and it adds NFS shares as repository storage from the web interface. It mounts the Docker socket, which is root access to the host: read Mounter and NFS storage first. Scripted installations that do not want it pass --no-mounter; you can start it later (Enable network shares in the web interface when the updater runs, or docker compose --profile mounts up -d mounter). With the updater on, the mounter follows every signed update by itself.
--with-mounter Starts the mounter explicitly. Up to 0.3.6 it was off unless you passed this option; since 0.4.0 it is the default. Needs release 0.3.1 or newer; an older release has no mounter.
--behind-proxy --domain <name> --proxy-ip <address> From 0.2.0: an installation behind a reverse proxy you already run, which holds the name and the certificate. --proxy-ip is the address of the proxy itself (repeat it or separate several with spaces; a bare address gets /32 or /128), required with --non-interactive. By default the hop to the proxy is encrypted (forward to https://<this host>:443). See Behind a reverse proxy.
--proxy-hop http With --behind-proxy, any release: the proxy forwards to http://<this host>:80 instead. The hop is not encrypted; only for a proxy on the same host or in an isolated network.
--non-interactive --domain <name> --edition full|community An unattended installation: no questions, the answers come from the options (--edition defaults to full). The master key is then not shown, so copy it from /opt/restow/.env to an offline place right away.
--dry-run Runs the checks and prints what it would do; changes nothing.
--help Lists every option, including --version, --dir (default /opt/restow) and --skip-signature-check (for tests and unsigned mirrors only, never for production), and the exit codes.

For example:

Terminal window
sudo bash install.sh --non-interactive --domain backup.example.com --edition community

Running the script again is safe: it finds the installation, never changes .env, a secret or data, checks the images named in .env and makes sure the stack runs. It refuses to start when Docker holds data of an earlier installation without its .env. It never updates an installation (see Updates), and it has no uninstall, on purpose, because removing an installation can destroy backups; the manual steps are in the README, “Removing Restow”.

From 0.3.6: it also updates docker-compose.yml. An installation keeps the file it was installed with, and an update, also through the updater, changes only the images in .env. An older file therefore lacks newer services such as the mounter. From 0.3.6, running the script again brings docker-compose.yml to the checked file of the release named in .env (checked against the release’s signed SHA256SUMS). A file that differs is kept as docker-compose.yml.bak-<time>, and .env is never changed. Put changes of your own into docker-compose.override.yml (Release stack reference), not into docker-compose.yml. When RESTOW_IMAGE names no release version, the file stays as it is.

Since 0.4.0: the mounter choice stays, and a moved database is protected. Running the 0.4.0 or a newer installer again on an existing installation keeps that installation’s choice about the mounter: it does not start a mounter you left off. File share backup and moving the database need the docker-compose.yml of 0.4.0 or later, so run the installer again after updating to 0.4.0 (see Upgrading to 0.4.0). Once the database was moved to another disk (Installation → Database), the installer refuses, with exit code 8 and nothing changed, to put in place or start a docker-compose.yml that does not read RESTOW_POSTGRES_DATA from .env: an older release file would start the stack without its database. Use the docker-compose.yml of release 0.4.0 or later; the 0.3.6 file does not read that setting.

If a reverse proxy you already run (Nginx Proxy Manager, Traefik, Caddy, HAProxy and the like) holds your public names and certificates, Restow needs neither a public DNS record on its own host nor ports 80 and 443 from the internet, and it gets no certificate of its own from Let’s Encrypt. The proxy terminates TLS for the public name and forwards to the Caddy edge of Restow, on the Restow host.

By default the connection between the proxy and Restow is encrypted as well: the edge serves HTTPS with a certificate from Caddy’s own authority, and the proxy forwards to https://<Restow host>:443. A plain HTTP hop is possible as an exception, see The plain HTTP hop.

Forward to port 443 (or 80 for the plain hop), never to 3000. Port 3000 is the api, which the release stack binds to the loopback of the Restow host on purpose; a proxy cannot reach it, and it does not serve the web interface.

Terminal window
sudo bash install.sh --behind-proxy --domain backup.example.com --proxy-ip 192.168.1.20

Without options, answer 2 at the question “How will people reach Restow?”; the script then asks for the name and the proxy’s address. --domain is the name your proxy serves. --proxy-ip is the address of the proxy itself, as the Restow host sees it, not your network: Restow believes the client address that these peers report (X-Forwarded-For) for its audit log and its sign-in limits, so list only the proxy. Several proxies: repeat the option or separate the addresses with spaces. A bare address gets /32 (IPv4) or /128 (IPv6); a network such as 192.168.1.0/29 is taken as given. If you are unsure which address the proxy connects from, load the page once and run sudo ss -tn state established '( sport = :443 )' on the Restow host. In an unattended run (--non-interactive) both options are required.

The script checks that port 443 is free (port 80 is not needed), does not look at DNS (the name points at your proxy, not at this host), and shows in its plan where the proxy must forward to. It writes these values to /opt/restow/.env:

Variable Value What it does
RESTOW_APP_DOMAIN backup.example.com The name the edge serves. No scheme: the edge serves HTTPS for it.
RESTOW_PUBLIC_URL https://backup.example.com The address browsers use, which defines the passkey domain and the links in mails.
RESTOW_EDGE_TLS internal The edge shows a certificate from Caddy’s own authority instead of asking Let’s Encrypt. A proxy that sends no server name (nginx sends none for an address) still gets the certificate of the name. Unset or acme is the behaviour of every other installation.
RESTOW_EDGE_TRUSTED_PROXIES 192.168.1.20/32 The proxies whose client address is believed.
RESTOW_HTTP_PORT 127.0.0.1: Port 80 is not needed. It is published on the loopback only, on a port Docker picks, so nothing else on the host can be in its way and the network cannot reach it.
RESTOW_EDGE_HSTS empty HSTS belongs to the proxy that holds the certificate, see below.

When the stack runs, the script copies the root certificate of the edge’s own authority to /opt/restow/edge-root-ca.crt. The proxy can verify the edge with it; nothing requires that. At the end it prints the address and the setup token in a box, and, in that box, where the proxy must forward to.

Nginx Proxy Manager, Proxy Host:

  1. Details: Domain Names backup.example.com; Scheme https; Forward Hostname / IP the address of the Restow host; Forward Port 443. Websockets Support is not needed: Restow uses server-sent events, not WebSockets (leaving the toggle on does no harm).

  2. SSL: your certificate for the name (for example from Let’s Encrypt, requested by Nginx Proxy Manager), Force SSL, HTTP/2 Support. Turn HSTS on here if you want it; Restow’s edge sends none in this mode.

  3. Advanced: the custom configuration

    proxy_ssl_server_name on;
    proxy_ssl_name $host;
    proxy_buffering off;

    proxy_ssl_server_name on and proxy_ssl_name $host send the public name as the TLS server name, so the edge answers with the certificate of that name. proxy_buffering off lets live updates reach the browser at once: they are server-sent events, which nginx holds back while it buffers responses.

To also verify the edge, copy /opt/restow/edge-root-ca.crt to where the proxy’s container can read it and add (the path is the one inside the proxy’s container):

proxy_ssl_verify on;
proxy_ssl_trusted_certificate /data/restow-edge-root-ca.crt;

Other proxies need the same things: upstream https://<Restow host>:443; the public name sent as the TLS server name and the Host header kept (the edge answers for that name only); X-Forwarded-For passed on; responses not buffered (Restow’s live channel is server-sent events over a normal request, not a WebSocket: turn response buffering off, for nginx proxy_buffering off;); and request bodies and timeouts that fit uploads (agents send packs of up to 128 MiB, imports segments of 32 MiB; nginx’s default limit of 1 MB is too small). With Caddy in front:

backup.example.com {
reverse_proxy https://192.168.1.50:443 {
transport http {
tls_server_name backup.example.com
tls_trust_pool file /etc/caddy/restow-edge-root-ca.crt
}
}
}

With Traefik, in the dynamic configuration:

http:
serversTransports:
restow:
serverName: backup.example.com
rootCAs:
- /etc/traefik/restow-edge-root-ca.crt
services:
restow:
loadBalancer:
serversTransport: restow
servers:
- url: https://192.168.1.50:443

Check the edge on the Restow host: curl -k --resolve backup.example.com:443:127.0.0.1 https://backup.example.com/healthz answers 200. Then open https://backup.example.com through the proxy.

Docker publishes the edge’s port through its own firewall rules, which ufw and firewalld rules for the host do not cover: ufw allow from 192.168.1.20 to any port 443 does not keep anyone else out. If other machines than the proxy can reach the Restow host, restrict the port in one of two ways.

  • Publish it on one address. Set RESTOW_HTTPS_PORT=192.168.1.50:443 in /opt/restow/.env (the address of the Restow host on the network of the proxy) and run docker compose up -d. The edge is then not published on the host’s other interfaces.

  • Allow only the proxy, in Docker’s own chain. Rules in DOCKER-USER run before Docker’s. Replace eth0 with the interface that faces the proxy’s network and the address with the proxy’s:

    Terminal window
    sudo iptables -I DOCKER-USER -i eth0 -p tcp --dport 443 ! -s 192.168.1.20 -j DROP

    --dport 443 is the port inside the container, which is what this chain sees after Docker’s port mapping. The rule is gone after a reboot: keep it where your system restores firewall rules (for example with iptables-persistent), and repeat it with ip6tables if the host has IPv6. Test from a machine other than the proxy.

With --proxy-hop http (any release) everything between the proxy and Restow, session cookies, passwords and restored data included, travels unencrypted, and port 80 of the Restow host is open to every machine that can reach it. Use it only when the proxy and Restow run on one host or in an isolated network. The script then needs port 80 instead of 443 and writes RESTOW_APP_DOMAIN=http://backup.example.com (the http:// makes the edge serve plain HTTP for that name instead of redirecting to HTTPS and asking for a certificate), RESTOW_PUBLIC_URL=https://backup.example.com and RESTOW_EDGE_TRUSTED_PROXIES as above, and publishes port 443 on the loopback only (RESTOW_HTTPS_PORT=127.0.0.1:). In the proxy, use Scheme http and Forward Port 80; the other settings above (proxy_buffering off;, HSTS at the proxy) stay.

In /opt/restow/.env set the three variables, then run cd /opt/restow && docker compose up -d:

Terminal window
RESTOW_APP_DOMAIN=http://backup.example.com
RESTOW_PUBLIC_URL=https://backup.example.com
RESTOW_EDGE_TRUSTED_PROXIES=192.168.1.20/32

Without the http:// prefix the edge treats the name as a public site: it answers port 80 with a redirect to HTTPS (308) and wants a certificate for the name, so a proxy that forwards to port 80 gets the redirect and one that forwards to 443 finds no usable certificate. In the proxy set Scheme http and Forward Port 80 (not 3000) and add proxy_buffering off;. Check on the Restow host: curl -i -H 'Host: backup.example.com' http://127.0.0.1/ answers 200.

Switching to the encrypted hop after the update to 0.2.0:

  1. Update as described in Updates. Changing the image lines in .env does not replace the Compose file.
  2. Replace /opt/restow/docker-compose.yml with the one of the 0.2.0 release assets; it passes RESTOW_EDGE_TLS on to the edge, which the 0.1.0 file does not.
  3. In .env set RESTOW_APP_DOMAIN=backup.example.com (without http://), RESTOW_EDGE_TLS=internal and RESTOW_HTTP_PORT=127.0.0.1:, then run docker compose up -d.
  4. In the proxy set Scheme https and Forward Port 443, and add the lines of the Advanced tab above.

On any Linux host with Docker and the Docker Compose plugin, these are the steps the script automates.

The release stack runs the published images and builds nothing. Its docker-compose.yml and env.example are attached to every GitHub release and are also in deploy/release of the repository (where the example file is called .env.example). Download the two files of the release you want into an empty directory on the server (here for 0.4.7), then create your environment file from the example:

Terminal window
mkdir -p /opt/restow && cd /opt/restow
curl -fsSLO https://github.com/restow-backup/restow/releases/download/v0.4.7/docker-compose.yml
curl -fsSLO https://github.com/restow-backup/restow/releases/download/v0.4.7/env.example
cp env.example .env

Nothing in env.example is a usable secret by itself: every value must be generated or filled in. Never commit .env or share it.

Every release comes in two builds. Pick one and use both of its images, an application image (api, worker and scheduler) and a web image (the Caddy edge and the web interface):

Build RESTOW_IMAGE RESTOW_WEB_IMAGE Contains
Full ghcr.io/restow-backup/restow:0.4.7 ghcr.io/restow-backup/restow-web:0.4.7 The Apache-2.0 core plus the Business and Service Provider modules, which stay locked until a license key is installed. Without a key it runs as Community.
Community ghcr.io/restow-backup/restow-community:0.4.7 ghcr.io/restow-backup/restow-web-community:0.4.7 The Apache-2.0 core only, for one tenant. No Business or Service Provider code and no license settings.

Take the full build if you may want Business or Service Provider later: a license key unlocks them without changing images. Take the Community build if you want the Apache-2.0 core and nothing else. Both builds use the same database schema, so you can switch later by changing the two image lines in .env and running docker compose up -d.

All four images carry the same tag, the release version without the leading v (there is no latest tag), and are built for amd64 and arm64. Every image is signed and ships an SBOM; to check the signature before you start the stack, see Verifying a release.

Fill in every value in the section Required of env.example; the comments in the file say how to generate each one. At minimum:

Variable What it is
RESTOW_IMAGE The application image with its version, for example ghcr.io/restow-backup/restow:0.4.7 (full) or ghcr.io/restow-backup/restow-community:0.4.7 (Community). Compose refuses to start without it.
RESTOW_WEB_IMAGE The web image of the same build and version, for example ghcr.io/restow-backup/restow-web:0.4.7 or ghcr.io/restow-backup/restow-web-community:0.4.7.
POSTGRES_PASSWORD Password for the Postgres superuser Docker Compose creates (POSTGRES_USER, default restow).
DATABASE_MIGRATION_URL Connection string for that same owner role; runs migrations only.
DATABASE_URL The application role, subject to Row Level Security. The migration step creates this role with the name and password you give here.
DATABASE_PROVIDER_URL The installation role (BYPASSRLS), for installation-wide lookups. Created the same way.
RESTOW_MASTER_KEY 32-byte key-encryption key, base64. See the warning below.
BETTER_AUTH_SECRET Secret for the auth system’s own tokens and sessions.
RESTOW_APP_DOMAIN The bare domain Caddy serves. In public mode Caddy requests a certificate for it. Compose refuses to start without it.
RESTOW_PUBLIC_URL The exact origin browsers open, e.g. https://restow.example.com or http://localhost:5173. Sets the passkey origin, the Entra redirect URI and the journal hostname.

Generate the two secrets with:

Terminal window
openssl rand -base64 32 # RESTOW_MASTER_KEY
openssl rand -base64 32 # BETTER_AUTH_SECRET

Generate a distinct password for each database role and for POSTGRES_PASSWORD the same way, for example:

Terminal window
openssl rand -hex 16

Everything else has a documented default and can stay empty unless you need to change it; everything below Optional in env.example works without a value. Release stack reference explains every variable of env.example with its default, and Reference → Environment variables has the complete list.

Restow connects to PostgreSQL with three different roles, on purpose, so Row Level Security is enforced by the database and not only by application code:

  1. Owner (DATABASE_MIGRATION_URL, Compose’s POSTGRES_USER): runs the migrations and owns every table. Nothing else uses this role at runtime.
  2. Application role (DATABASE_URL): NOSUPERUSER, cannot bypass Row Level Security, owns no table. All day-to-day tenant work runs on this role; a query without a tenant pinned sees nothing, and one with a tenant pinned sees only that tenant.
  3. Installation role (DATABASE_PROVIDER_URL, BYPASSRLS): used only for work that has to see across tenants or before a tenant is known: session/API-key lookups, the tenant list, installation-wide settings and license state, the scheduler, webhook delivery, and pg-boss’s own schema.

The migration step creates the application and installation roles with the names and passwords from these two connection strings; you do not create them by hand. api, worker and scheduler each check at startup that their application role cannot bypass Row Level Security, and refuse to start if it can.

Sign-in has a role of its own (since 0.3.5). The auth role is the only one besides the installation role that reaches the sign-in tables (sessions, passwords, passkeys, two-factor secrets), and sign-in, account and membership code run on it, also under Row Level Security. The application role keeps to tenant data: of the sign-in tables it sees nothing but a user’s name and email address, it reaches no installation table (settings, license, providers, service heartbeats, provider team), and in the job queue it may only add a job. The migration creates and keeps the auth role up to date, with a login derived from the other two (<application role>_auth, for example restow_app_auth). You set nothing for it; DATABASE_AUTH_URL is optional, for a login of your own. See Environment variables → Database.

Terminal window
docker compose up -d

Compose pulls the two images on first start and builds nothing. To update later, change the two image lines in .env and run docker compose pull && docker compose up -d; see Updates.

Database migrations run automatically as part of the api container’s startup, before it serves traffic; there is no separate migration command to run by hand.

Terminal window
curl http://127.0.0.1:3000/healthz
curl http://127.0.0.1:3000/readyz

Both should answer successfully once the postgres, api, worker and scheduler containers are all up (docker compose ps). Then open RESTOW_PUBLIC_URL in a browser: Restow shows the setup wizard on first access.

Its first step asks for the language (from 0.2.0), then for the one-time setup token, which proves that you operate the server. Until the setup is complete, the api prints it to its log at every start:

Terminal window
docker compose logs api | grep 'SETUP TOKEN'

Each restart of the api issues a new token; use the one printed last. For an unattended install, set your own token as RESTOW_SETUP_TOKEN in .env (at least 16 characters, for example openssl rand -hex 16); it is then never written to the log. The next step is the operator notice, which you must accept before the setup can be saved.

If an owner later loses their passkey, authenticator app or password, the access is recovered on the server with docker compose exec api restow admin recover --email <owner>; see Recovering administrator access.

Every release image is signed with cosign (keyless, through GitHub’s OIDC identity), so you can check that an image was built by the project’s release workflow:

Terminal window
cosign verify ghcr.io/restow-backup/restow:0.4.7 \
--certificate-identity-regexp '^https://github.com/restow-backup/restow/\.github/workflows/release\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com

Check restow-web, or restow-community and restow-web-community for the Community build, the same way. Verifying a release explains this, the exact identity for one version, the SBOM and the signed checksum lists in full.

The release stack mounts a host folder read-only into the api and worker containers at /var/lib/restow/import, for mail file import. RESTOW_IMPORT_DIR sets the host folder (default ./import next to the Compose file). Each tenant uses its own subfolder, <folder>/<tenant slug>/, which you create yourself. Restow never writes to or deletes from this folder, which must be readable by the user the containers run as. You can leave the variable empty until you need it.

Restow does not update itself. The update check under Settings, Updates is off until an administrator turns it on, and the optional updater runs only if you start it with docker compose --profile updater up -d (it needs no line in .env: from 0.2.1 it pins its own image itself). The updater mounts the Docker socket, which is root on the host, and installs only release images whose signature it has verified. Read Updates before you enable either.

Community (backup, restore, the archive, endpoint backup, and mail file import and export) is free and needs no key: open source under the Apache License 2.0, no mailbox limit. It runs from either build. Business and Service Provider need the full images: there, an offline-verified license key, installed under Installation → License, unlocks the additional features in the same installation, with no separate download or reinstall. See License and editions for what each edition adds, what is available today and how the key is verified.