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.
Requirements
Section titled “Requirements”Platform
Section titled “Platform”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.
What the install script checks
Section titled “What the install script checks”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. |
Memory on Proxmox and other hypervisors
Section titled “Memory on Proxmox and other hypervisors”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 -hshows what the system sees, andcat /sys/kernel/kexec_crash_sizethe bytes reserved for kdump (0or no such file: nothing is reserved; newer kernels also offer/sys/kernel/kexec/crash_size).
Disk on Ubuntu with LVM
Section titled “Disk on Ubuntu with LVM”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:
sudo vgssudo lvextend -r -l +100%FREE /dev/ubuntu-vg/ubuntu-lvubuntu-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.
Server
Section titled “Server”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 composecommand, not the older standalonedocker-composev1). 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/chunksinside 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.
DNS and TLS
Section titled “DNS and TLS”- 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.
Sizing
Section titled “Sizing”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) andWORKER_TENANT_CONCURRENCY(parallel jobs per tenant), both default 2. Run more than oneworkercontainer 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.
Install with the script (recommended)
Section titled “Install with the script (recommended)”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:
curl -fsSLO https://github.com/restow-backup/restow/releases/download/v0.4.7/install.shcurl -fsSLO https://github.com/restow-backup/restow/releases/download/v0.4.7/install.sh.sha256sha256sum -c install.sh.sha256sudo bash install.shinstall.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):
curl -fsSLO https://github.com/restow-backup/restow/releases/download/v0.4.7/SHA256SUMScurl -fsSLO https://github.com/restow-backup/restow/releases/download/v0.4.7/SHA256SUMS.sigstore.jsoncosign 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.comsha256sum -c --ignore-missing SHA256SUMSThe one-liner (convenience)
Section titled “The one-liner (convenience)”The same script can also be fetched and run in one line:
curl -fsSL https://github.com/restow-backup/restow/releases/latest/download/install.sh | sudo bashUnattended, without questions:
curl -fsSL https://github.com/restow-backup/restow/releases/latest/download/install.sh | sudo bash -s -- --non-interactive --domain backup.example.com --edition fullWith --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.
What the script does
Section titled “What the script does”- 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.
- 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. - It installs Docker Engine and the Compose plugin from Docker’s signed apt repository if Docker is missing. Docker from snap is refused.
- It downloads
docker-compose.ymlandenv.exampleof the release and checks them against the release’sSHA256SUMSand its cosign signature. - It writes
/opt/restow/.env(mode 0600) with secrets from the kernel’s random generator. - It shows
RESTOW_MASTER_KEYonce, 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). - 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-mounterleaves it off (see Options). - 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.
Options
Section titled “Options”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:
sudo bash install.sh --non-interactive --domain backup.example.com --edition communityRunning it again
Section titled “Running it again”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.
Behind a reverse proxy
Section titled “Behind a reverse proxy”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.
With the install script (from 0.2.0)
Section titled “With the install script (from 0.2.0)”sudo bash install.sh --behind-proxy --domain backup.example.com --proxy-ip 192.168.1.20Without 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.
Set up the proxy
Section titled “Set up the proxy”Nginx Proxy Manager, Proxy Host:
-
Details: Domain Names
backup.example.com; Schemehttps; Forward Hostname / IP the address of the Restow host; Forward Port443. Websockets Support is not needed: Restow uses server-sent events, not WebSockets (leaving the toggle on does no harm). -
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.
-
Advanced: the custom configuration
proxy_ssl_server_name on;proxy_ssl_name $host;proxy_buffering off;proxy_ssl_server_name onandproxy_ssl_name $hostsend the public name as the TLS server name, so the edge answers with the certificate of that name.proxy_buffering offlets 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:443Check 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.
Firewall (optional)
Section titled “Firewall (optional)”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:443in/opt/restow/.env(the address of the Restow host on the network of the proxy) and rundocker 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-USERrun before Docker’s. Replaceeth0with 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 443is 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 withiptables-persistent), and repeat it withip6tablesif the host has IPv6. Test from a machine other than the proxy.
The plain HTTP hop (exception)
Section titled “The plain HTTP hop (exception)”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.
With 0.1.0: by hand
Section titled “With 0.1.0: by hand”In /opt/restow/.env set the three variables, then run cd /opt/restow && docker compose up -d:
RESTOW_APP_DOMAIN=http://backup.example.comRESTOW_PUBLIC_URL=https://backup.example.comRESTOW_EDGE_TRUSTED_PROXIES=192.168.1.20/32Without 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:
- Update as described in Updates. Changing the image lines in
.envdoes not replace the Compose file. - Replace
/opt/restow/docker-compose.ymlwith the one of the 0.2.0 release assets; it passesRESTOW_EDGE_TLSon to the edge, which the 0.1.0 file does not. - In
.envsetRESTOW_APP_DOMAIN=backup.example.com(withouthttp://),RESTOW_EDGE_TLS=internalandRESTOW_HTTP_PORT=127.0.0.1:, then rundocker compose up -d. - In the proxy set Scheme
httpsand Forward Port443, and add the lines of the Advanced tab above.
Install by hand with Docker Compose
Section titled “Install by hand with Docker Compose”On any Linux host with Docker and the Docker Compose plugin, these are the steps the script automates.
1. Get the stack and the environment file
Section titled “1. Get the stack and the environment file”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:
mkdir -p /opt/restow && cd /opt/restowcurl -fsSLO https://github.com/restow-backup/restow/releases/download/v0.4.7/docker-compose.ymlcurl -fsSLO https://github.com/restow-backup/restow/releases/download/v0.4.7/env.examplecp env.example .envNothing 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.
2. Fill in the required values
Section titled “2. Fill in the required values”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:
openssl rand -base64 32 # RESTOW_MASTER_KEYopenssl rand -base64 32 # BETTER_AUTH_SECRETGenerate a distinct password for each database role and for POSTGRES_PASSWORD the same way, for example:
openssl rand -hex 16Everything 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.
The three database roles
Section titled “The three database roles”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:
- Owner (
DATABASE_MIGRATION_URL, Compose’sPOSTGRES_USER): runs the migrations and owns every table. Nothing else uses this role at runtime. - 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. - 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.
3. Start the stack
Section titled “3. Start the stack”docker compose up -dCompose 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.
4. Confirm it’s healthy
Section titled “4. Confirm it’s healthy”curl http://127.0.0.1:3000/healthzcurl http://127.0.0.1:3000/readyzBoth 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:
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.
Verify the images (optional)
Section titled “Verify the images (optional)”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:
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.comCheck 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.
After the install
Section titled “After the install”Import folder
Section titled “Import folder”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.
Updates are opt-in
Section titled “Updates are opt-in”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.
Editions and license key
Section titled “Editions and license key”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.