Release stack reference
The release stack is two files, docker-compose.yml and env.example. Both are attached to every GitHub release and are in deploy/release of the repository (where the example file is called .env.example). The Compose file runs the published images and builds nothing; the docker-compose.yml at the root of the repository builds Restow from source instead. The release pipeline runs its smoke checks against exactly this file, with both builds.
Up to 0.3.5 the explanations on this page were comments in the two files. From 0.3.6 the files carry almost no comments and point here. This page is the reference: what every service does, which ports and volumes it has and why, and what every variable of .env means. For the install steps see Get started; for every other variable and the long explanations see Environment variables.
cp env.example .env # then fill in the required valuesdocker compose up -dOn a dedicated VM the install script does this for you, in /opt/restow.
No secret is written in the Compose file: every value comes from .env. Never commit .env or real values.
The Compose project is called restow (name: restow at the top of the file). The install script, the updater and the mounter rely on this name, and so do the volume names: Compose derives them from it, so the database lives in restow_pgdata and the chunk store in restow_restow-data. See Settings that are dangerous to change.
Services at a glance
Section titled “Services at a glance”docker compose up -d starts the first five services. The last two belong to a Compose profile and start only when you ask for them, once, with their own command. Since 0.4.0 the install script asks for the mounts profile by default, so a new installation runs the mounter too (--no-mounter leaves it off).
| Service | Image | Profile | Published ports | What it is |
|---|---|---|---|---|
postgres |
postgres:16-alpine |
none | none | The database. |
api |
RESTOW_IMAGE, ROLE=api |
none | 127.0.0.1:3000, the journal receiver |
The REST API, the migrations, the journal receiver. |
worker |
RESTOW_IMAGE, ROLE=worker |
none | none | Runs the queued jobs and delivers webhooks. |
scheduler |
RESTOW_IMAGE, ROLE=scheduler |
none | none | Plans the schedules that are due. |
caddy |
RESTOW_WEB_IMAGE |
none | 80 and 443 | The web edge: web interface, TLS, proxy to the api. |
updater |
RESTOW_UPDATER_IMAGE, else RESTOW_IMAGE, ROLE=updater |
updater |
none (8090 inside the Compose network) | Opt-in: applies updates from the web interface. |
mounter |
RESTOW_MOUNTER_IMAGE, else RESTOW_IMAGE, ROLE=mounter |
mounts |
none (8091 inside the Compose network) | Runs the containers that back up and restore file shares (since 0.4.0) and adds NFS shares as repository storage from the web interface. Started by the install script by default, otherwise opt-in. |
The api, worker, scheduler, updater and mounter run one and the same application image; the variable ROLE decides what the process does.
The two builds
Section titled “The two builds”Every release has two builds, and RESTOW_IMAGE and RESTOW_WEB_IMAGE in .env pick one. Use both images of one build, at the same version:
| 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 (Restow Enterprise License), which stay locked until a license key is installed. |
| 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: no Business or Service Provider modules, one tenant. |
All four images carry the same tag, the release version without the leading v. There is no latest tag. The opt-in updater rewrites both lines itself on every update, with the images of the build it runs from. Without it, change the two lines yourself to update or to switch builds, then run docker compose pull && docker compose up -d (see Updates). Do not set RESTOW_VERSION in .env: the image carries its own version. To check the signatures see Verifying a release.
An update changes only .env, never docker-compose.yml. Running the install script (0.3.6 or newer) again on an installation brings docker-compose.yml to the 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>. Put changes of your own into docker-compose.override.yml, not into docker-compose.yml (see docker-compose.override.yml and Running it again).
The shared block x-restow-app
Section titled “The shared block x-restow-app”The three server roles api, worker and scheduler share one block, which each of them includes with <<: *restow-app:
x-restow-app: &restow-app image: ${RESTOW_IMAGE:?set RESTOW_IMAGE in .env (for example ghcr.io/restow-backup/restow:0.4.7 or ghcr.io/restow-backup/restow-community:0.4.7)} env_file: - .env depends_on: postgres: condition: service_healthy restart: unless-stoppedimageisRESTOW_IMAGE. Compose stops with the message above when the variable is empty.env_filehands the whole.envto the three roles as their environment, so every variable you set there reachesapi,workerandscheduleralike.caddygets only four variables (below), andupdaterandmounterget none.depends_onwaits for a healthypostgres.workerandschedulerwrite their owndepends_on, because a service’s own key replaces the shared one: it namespostgresagain and adds theapi.restart: unless-stoppedrestarts the container after a crash or a reboot, unless you stopped it.
The services
Section titled “The services”postgres
Section titled “postgres”PostgreSQL 16 (postgres:16-alpine). It holds metadata, the job queue and the audit log; the backed-up content lives in the chunk store, not here. The api applies the database migrations.
- Settings:
POSTGRES_USERandPOSTGRES_DB(both defaultrestow) andPOSTGRES_PASSWORD, which is required: Compose does not startpostgreswithout it. Change the user and the database name only before the first start, and use the same names inDATABASE_MIGRATION_URL. - Ports: none. The release stack publishes no PostgreSQL port; the other services reach it over the Compose network as
postgres:5432. - Volume: the database files at
/var/lib/postgresql/data. Since 0.4.0 the mount is typed and read from.env:type: ${RESTOW_POSTGRES_DATA_TYPE:-volume},source: ${RESTOW_POSTGRES_DATA:-pgdata},bind.create_host_path: false. With both variables empty it is the Docker volumepgdata; after a move to another disk (Installation → Database) it is that host directory.create_host_path: falsemakes Docker refuse to start PostgreSQL when the directory is missing instead of creating an empty one. Keep this mount as it is, see Database. - Health check:
pg_isready -U ${POSTGRES_USER:-restow} -d ${POSTGRES_DB:-restow}every 10 seconds, timeout 5 seconds, 5 retries, 10 seconds start period. - Depends on: nothing. Restart:
unless-stopped.
The application image with ROLE=api: the REST API that the web interface and the agents use, the archive’s journal receiver, and the database migrations. The api runs the migrations as the database owner before it answers /healthz and refuses to start when one fails. The migration step also creates the application database roles that the other processes sign in with.
-
Settings: the whole
.env, plusROLE=api,RESTOW_UPDATER_URL(defaulthttp://updater:8090, where theapilooks for the opt-in updater) andRESTOW_MOUNTER_URL(defaulthttp://mounter:8091, the same for the mounter). Nothing answers at those two addresses unless the matching profile runs. -
Ports:
Mapping Why 127.0.0.1:${RESTOW_API_PORT:-3000}:3000Loopback only: health checks and the REST API on the host itself. Caddy reaches the apiover the Compose network and does not use this port.${JOURNAL_SMTP_BIND:-127.0.0.1}:${JOURNAL_SMTP_PORT:-2525}:${JOURNAL_SMTP_PORT:-2525}The archive’s journal SMTP receiver (Business and Service Provider). It listens only when JOURNAL_SMTP_PORTis set in.env; until then this maps a closed port (2525) on the loopback interface, so a default installation opens no mail port to the network. Exchange Online delivers to port 25 and must reach the receiver directly, not through Caddy: setJOURNAL_SMTP_PORT=25andJOURNAL_SMTP_BIND=0.0.0.0. The receiver needs a TLS certificate, see The journal receiver’s certificate. -
Networks (since 0.4.0):
defaultand the internal networkrunners. File share runners reach theapionly throughrunners, which has no route to the outside (see Networks). -
Volumes:
Mount Why restow-dataat/dataThe default local chunk store: STORAGE_LOCAL_PATH=/data/chunkslives here.${RESTOW_IMPORT_DIR:-./import}at/var/lib/restow/import, read-onlyThe server-side import folder: the apilists it, theworkerreads it. Read-only on purpose, because Restow never writes to or deletes from it. See Mail file import and export.${JOURNAL_TLS_DIR:-./journal-tls}at/etc/restow/journal-tls, read-onlyThe journal receiver’s TLS certificate chain and private key (PEM). Exchange Online requires TLS, and the receiver does not start without a certificate. A directory and not two files, so that a renewal that replaces the files is noticed: the receiver re-reads them every few minutes, with no restart. Put fullchain.pemandprivkey.pemthere and pointJOURNAL_TLS_CERT_PATHandJOURNAL_TLS_KEY_PATHat/etc/restow/journal-tls/<file>in.env. When theapiitself needs a restart, see The journal receiver’s certificate.restow-updater-sharedat/updater-shared, read-onlyThe updater’s shared secret, which the apisends with every request. Empty until theupdaterprofile ran once. Keep this line.restow-mounter-sharedat/mounter-shared, read-onlyThe same for the mounter. Empty until the mountsprofile ran once. Keep this line. Anapiwithout this mount was started from a Compose file that predates the mounter, see mounter. -
Health check:
node -e "fetch('http://127.0.0.1:3000/healthz')…"every 30 seconds, timeout 5 seconds, 5 retries, 20 seconds start period. -
Depends on: a healthy
postgres. Restart:unless-stopped.
worker
Section titled “worker”The application image with ROLE=worker: runs the queued jobs (backups, restores, checks, imports and exports) and delivers webhooks.
- Settings (since 0.4.0):
ROLE=worker,RESTOW_MOUNTER_URL(defaulthttp://mounter:8091: the worker asks the mounter to start file share runners),RESTOW_MOUNTER_MAX_RUNNERS(default 8) andRESTOW_MOUNTER_RUNNER_MAX_MEMORY_MIB(default 16384), the same caps the mounter gets. Without a mounter, file share runs wait withshare.mounter_unavailable. - Ports: none.
- Volumes:
restow-dataat/data, the same chunk store as theapi, the import folder, read-only, and (since 0.4.0)restow-mounter-sharedat/mounter-shared, read-only, the mounter’s shared secret. The worker has no journal directory and no updater secret. stop_grace_period: 60s(since 0.3.5): the time running jobs get to save a checkpoint when the worker stops, for an update or adocker compose up -d. Docker’s default of 10 seconds cut that short. A job that was interrupted after it saved a checkpoint continues without using up a retry. The worker’s own shutdown waits at mostWORKER_SHUTDOWN_TIMEOUT_MS(50 seconds), which must stay below the 60 seconds. A Compose file you keep by hand needs this line; see Updates.- Depends on: a healthy
postgresand a healthyapi: theapiapplies the migrations and creates the application database roles before it answers/healthz, and the worker signs in with them. - Health check: none of its own. Restart:
unless-stopped.
scheduler
Section titled “scheduler”The application image with ROLE=scheduler: plans the schedules that are due, on a tick of 30 seconds by default (see Scheduler).
- Ports and volumes: none.
- Depends on: a healthy
postgresand a healthyapi, for the same reason as theworker. - Health check: none of its own. Restart:
unless-stopped.
The web image (RESTOW_WEB_IMAGE): the built web interface and the Caddy edge. It serves the web interface, terminates TLS and proxies /api/* (and the agent and install paths, see Ports and roles) to the api. For its certificate, RESTOW_APP_DOMAIN must resolve to this host with ports 80 and 443 reachable.
- Image:
RESTOW_WEB_IMAGE, required. Compose stops without it, and its message names an example such asghcr.io/restow-backup/restow-web:0.4.7orghcr.io/restow-backup/restow-web-community:0.4.7. - Settings: only four variables, no
env_file.RESTOW_APP_DOMAINis required, for exampleapp.example.com(Compose stops without it).RESTOW_EDGE_HSTSandRESTOW_EDGE_TRUSTED_PROXIESdefault to empty.RESTOW_EDGE_TLSdefaults toacme, and an empty value meansacmetoo. RESTOW_EDGE_TLS:acmegets the certificate from Let’s Encrypt for a public domain, or from Caddy’s own authority forlocalhostand internal names.internaluses Caddy’s own certificate authority forRESTOW_APP_DOMAIN, whatever the name, with no public DNS record and no inbound port 80. Use it when another reverse proxy sits in front of this edge and reaches it over HTTPS (the install script’s--behind-proxy, from 0.2.0). The root certificate to verify the edge with is/data/caddy/pki/authorities/local/root.crtin thecaddy-datavolume, for exampledocker compose cp caddy:/data/caddy/pki/authorities/local/root.crt .. See Behind a reverse proxy.- Ports:
${RESTOW_HTTP_PORT:-80}:80and${RESTOW_HTTPS_PORT:-443}:443, for the web interface, the agents and the Let’s Encrypt challenge. Each variable takes a port, or an address and a port (127.0.0.1:8443) to publish it on that interface only. Let’s Encrypt needs both on the standard ports, so change them only behind another reverse proxy or for a local evaluation. See Network and ports. - Volumes:
caddy-dataat/data(the certificates and Caddy’s own authority) andcaddy-configat/config. - Depends on:
api(started, no health condition). Health check: none. Restart:unless-stopped.
updater
Section titled “updater”The opt-in updater, profile updater. It applies an update from Installation → Updates: it verifies and pulls the new images, dumps the database, recreates the services and rolls back when the new version does not come up. Read Updates first.
-
Image: there is no separate updater image. It is the application image started with
ROLE=updater:RESTOW_UPDATER_IMAGEif set, elseRESTOW_IMAGE. See How the updater and the mounter pin their image. -
Settings:
ROLE=updater,RESTOW_UPDATER_PROJECT_DIR(fromRESTOW_PROJECT_DIR),RESTOW_UPDATER_SELF_UPDATE(defaulttrue),RESTOW_UPDATER_API_URL(http://api:3000),RESTOW_UPDATER_SOURCE_HOSTS,RESTOW_UPDATER_IMAGE_REPOSITORYandRESTOW_UPDATER_WEB_IMAGE_REPOSITORY. Noenv_file: it has no database access and holds no application secret. -
Ports: none published. The service exposes 8090 on the Compose network only; the
apireaches it atRESTOW_UPDATER_URL. -
Volumes:
Mount Why /var/run/docker.sockTo run dockeranddocker composeagainst the host’s Docker. Root on the host.${RESTOW_PROJECT_DIR:-.}at${RESTOW_PROJECT_DIR:-/project}The directory with docker-compose.ymland.env. It is mounted at/projectand the updater reads its host path from the mount. SetRESTOW_PROJECT_DIR(the absolute path, for example/opt/restow) only to mount it at the same path instead; the install script does that.restow-updaterat/stateThe updater’s state and the database dumps it takes before an update. restow-updater-sharedat/updater-sharedThe shared secret the updater writes on its first start and the apireads. -
security_opt: no-new-privileges:true. -
Health check:
/healthzon port 8090 every 30 seconds, timeout 5 seconds, 3 retries, 10 seconds start period. Restart:unless-stopped.
mounter
Section titled “mounter”The mounter, profile mounts. Since 0.4.0 it also starts the short-lived runner containers that mount a file share for one run, back it up or restore into it and are removed afterwards: that path never touches the compose override and never restarts the api or the worker (File shares). It adds NFS shares as backup storage from the web interface (Installation → Default repository & network shares): it writes them into docker-compose.override.yml in the project directory as Docker volumes, mounts them into the api and the worker at /mnt/restow/<name> and recreates those two services. Read NFS shares (mounter) first.
- Image: the application image started with
ROLE=mounter:RESTOW_MOUNTER_IMAGEif set, elseRESTOW_IMAGE. - Settings:
ROLE=mounter,RESTOW_MOUNTER_PROJECT_DIR(fromRESTOW_PROJECT_DIR) and, since 0.4.0,RESTOW_MOUNTER_MAX_RUNNERS(default 8, the hard cap of file share runners) andRESTOW_MOUNTER_RUNNER_MAX_MEMORY_MIB(default 16384, the upper bound of the memory per runner). Noenv_file: it holds none of the application’s credentials. The mounter’s further settings are in Environment variables. - Ports: none published. The service exposes 8091 on the Compose network, and nobody but the
apireaches it. - Volumes: the Docker socket, the project directory (mounted as for the updater),
restow-mounterat/state(its operation log) andrestow-mounter-sharedat/mounter-shared(the shared secret theapireads). security_opt: no-new-privileges:true.- Health check:
/healthzon port 8091 every 30 seconds, timeout 5 seconds, 3 retries, 10 seconds start period. Restart:unless-stopped. - A Compose file from before the mounter: an installation whose
docker-compose.ymlwas made before 0.3.1 (0.3.0 was never published) has nomounterservice, so Compose answersno such service: mounter, and itsapihas nomounter-sharedmount. Run the install script again on it, see The two builds andno such service: mounter.
How the updater and the mounter pin their image
Section titled “How the updater and the mounter pin their image”Both hold the Docker socket, so their image must not change with the rewrite of RESTOW_IMAGE that every update makes. Each pins its own image by digest in .env.
The updater (RESTOW_UPDATER_IMAGE, optional):
- While it is empty, the service starts the image in
RESTOW_IMAGE. On that first start the updater writesRESTOW_UPDATER_IMAGEinto.envitself, pinned by digest to the image it runs (for exampleghcr.io/restow-backup/restow:0.4.7@sha256:...). - From then on its image no longer follows
RESTOW_IMAGE. If it cannot pin (a locally built image has no registry digest, or.envis not writable), the Updates tab shows the blocker “updater image not pinned”. - After an update from a signed release succeeded, the updater sets
RESTOW_UPDATER_IMAGEto that release’s verified image, by digest, and recreates itself. It never moves to an image built from source.RESTOW_UPDATER_SELF_UPDATE=falseswitches that off, and you then move it by hand. - If you set
RESTOW_UPDATER_IMAGEyourself, use an image of the same build asRESTOW_IMAGE(restow-communityfor the Community build).
The mounter (RESTOW_MOUNTER_IMAGE, optional):
- While it is empty, the service starts the image in
RESTOW_IMAGE. On that first start the mounter writesRESTOW_MOUNTER_IMAGEinto.env, pinned by digest to the image it runs. A local build has no digest and is not pinned. - With the opt-in updater running, the updater moves it to the verified image of every signed update it installs and recreates the mounter.
- Without the updater, set
RESTOW_MOUNTER_IMAGEto the new release’s image (or empty it) and rundocker compose --profile mounts up -d mounter, once no change of a share is running.
Volumes
Section titled “Volumes”| Volume | Mounted by | Holds |
|---|---|---|
pgdata |
postgres at /var/lib/postgresql/data |
The database files, unless RESTOW_POSTGRES_DATA points at a host directory (since 0.4.0, see postgres). |
restow-data |
api and worker at /data |
The default local chunk store (/data/chunks). A folder on it, for example /data/restic-cache, can hold the restic caches (RESTOW_RESTIC_CACHE_DIR). |
caddy-data |
caddy at /data |
Caddy’s certificates and its own certificate authority. |
caddy-config |
caddy at /config |
Caddy’s configuration directory. |
restow-updater |
updater at /state |
The updater’s state and database dumps. |
restow-updater-shared |
updater (read and write), api (read-only) |
The updater’s shared secret. |
restow-mounter |
mounter at /state |
The mounter’s operation log. |
restow-mounter-shared |
mounter (read and write), api and worker (read-only, the worker since 0.4.0) |
The mounter’s shared secret. |
Compose prefixes every volume with the project name, so on the host they are called restow_pgdata, restow_restow-data, restow_caddy-data and so on (docker volume ls). The install script, the updater and the mounter rely on the project name, and with it on these volume names; see Settings that are dangerous to change.
Besides these, the Compose file mounts three directories of the host: the import folder (RESTOW_IMPORT_DIR), the journal certificate directory (JOURNAL_TLS_DIR) and the project directory. A fourth, the database directory (RESTOW_POSTGRES_DATA), joins them once you move the database to another disk. See Backing up Restow itself for what to keep a copy of.
Networks
Section titled “Networks”Since 0.4.0 the file has one network of its own besides the default one:
| Network | Used by | Why |
|---|---|---|
runners (internal: true) |
api, and the file share runner containers the mounter starts |
Runners have no network of their own except this one: it reaches the api (the runner’s restic route and its progress reports) and nothing else, and has no route to the outside. A run that does not find it fails with runner_network_missing: the compose file is older than 0.4.0 (run the installer again). The Docker name is <project>_runners, restow_runners. |
The runner reaches the file server through the mount the Docker host makes, not through this network (Network and ports).
docker-compose.override.yml
Section titled “docker-compose.override.yml”The mounter writes the NFS shares into docker-compose.override.yml next to docker-compose.yml (or into the override that is already there). Compose reads that file on every docker compose command in the directory, so the shares survive restarts, updates and a manual docker compose up -d.
- What the mounter touches: the list
x-restow-mounts, top-level volumes whose name starts withrestow-nfs-(one per share,restow-nfs-<name>-<hash>), and the entries of theapiandworkervolumes whose source starts withrestow-nfs-. Everything else in the file stays as it is, comments included. Do not edit those parts by hand; change shares in the web interface. - What belongs there: your own changes to the stack. Keep them in the override file and not in
docker-compose.yml. From 0.3.6 the install script replacesdocker-compose.ymlwith the release’s file when you run it again, and keeps the old file as a backup (see Running it again). - Requirements: Compose has to find the override on its own, so the project must use one of the default file names (
docker-compose.yml,compose.yaml, …) and.envmust not setCOMPOSE_FILE.
The .env variables
Section titled “The .env variables”Copy env.example to .env and fill in the required values. env.example carries empty placeholders only; never commit .env or real values. The file lists the variables in four groups. An empty value falls back to the default named here. Sections of Environment variables carry the long explanations; the tables below give the default and the meaning in short. Variables that are not in env.example, such as the Microsoft, mail transport and S3 settings, are on that page, and the repository’s root .env.example is the full list.
Required
Section titled “Required”Compose stops without RESTOW_IMAGE, RESTOW_WEB_IMAGE, RESTOW_APP_DOMAIN and POSTGRES_PASSWORD. There are three database roles with three different passwords; generate each with openssl rand -hex 16.
| Variable | Default | Meaning |
|---|---|---|
RESTOW_IMAGE |
none | The application image of one build, with its version, for example ghcr.io/restow-backup/restow:0.4.7 or ghcr.io/restow-backup/restow-community:0.4.7. Used by api, worker, scheduler and, by default, updater and mounter. |
RESTOW_WEB_IMAGE |
none | The web image of the same build and version, ghcr.io/restow-backup/restow-web:0.4.7 or ghcr.io/restow-backup/restow-web-community:0.4.7. |
RESTOW_PUBLIC_URL |
none | The origin the browser uses (origin only), for example https://app.example.com. |
RESTOW_APP_DOMAIN |
none | The domain the edge serves and gets a certificate for, for example app.example.com. |
POSTGRES_PASSWORD |
none | Password of the database owner role that the postgres service creates. |
DATABASE_MIGRATION_URL |
none | The database owner, which runs the migrations only: postgres://restow:<POSTGRES_PASSWORD>@postgres:5432/restow. |
DATABASE_URL |
none | The application role, subject to Row Level Security: postgres://restow_app:<password>@postgres:5432/restow. |
DATABASE_PROVIDER_URL |
none | The installation role: postgres://restow_provider:<password>@postgres:5432/restow. |
RESTOW_MASTER_KEY |
none | The 32-byte key-encryption key, from openssl rand -base64 32. It wraps every tenant key. Keep a copy offline: without it no backup can be read again. |
BETTER_AUTH_SECRET |
none | The secret of the sign-in system, from openssl rand -base64 32. |
The migration step creates the application roles with the names and passwords given in these connection strings, and the role that sign-in runs on (since 0.3.5) with a login it derives from them (restow_app_auth). There is nothing to set for it. DATABASE_AUTH_URL is optional, for a login of your own: see Database.
Optional
Section titled “Optional”Everything below can stay empty.
| Variable | Default | Meaning |
|---|---|---|
RESTOW_HTTP_PORT, RESTOW_HTTPS_PORT |
80, 443 |
Host ports of the Caddy edge. Let’s Encrypt needs both on the standard ports, so change them only behind another reverse proxy or for a local evaluation. A value may carry an address (127.0.0.1:8443) to publish that port on this interface only. |
RESTOW_API_PORT |
3000 |
Host port on which the api is published on 127.0.0.1 only. The edge reaches the api over the Compose network and does not use it. |
RESTOW_EDGE_TRUSTED_PROXIES |
loopback only | Peers whose X-Forwarded-For the edge keeps (and appends to) instead of replacing with the peer’s own address: space-separated CIDR ranges, or private_ranges. Needed only when another reverse proxy sits in front of this one; nothing changes for a normal installation. See Edge. |
RESTOW_EDGE_TLS |
acme |
acme (also when empty) or internal: where the edge gets its certificate. internal is Caddy’s own authority for RESTOW_APP_DOMAIN, for an edge behind another reverse proxy; the root certificate is /data/caddy/pki/authorities/local/root.crt in the caddy-data volume. See caddy. |
RESTOW_EDGE_HSTS |
false |
true sends Strict-Transport-Security, with includeSubDomains. Only once RESTOW_APP_DOMAIN is a real, publicly resolvable domain for which this edge terminates its own valid TLS certificate. The value must be exactly lowercase true; anything else leaves HSTS off. Never set it for a local or IP installation or a bare HTTP domain: browsers then stay on HTTPS for a host that may never serve it, until the max-age expires. |
POSTGRES_USER, POSTGRES_DB |
restow, restow |
Owner role and database name of the postgres service. Change them only before the first start, and use the same names in DATABASE_MIGRATION_URL. |
RESTOW_POSTGRES_DATA_TYPE, RESTOW_POSTGRES_DATA |
empty: the volume pgdata |
Since 0.4.0. Where the database keeps its data: volume or bind, and the volume name or the host directory. Written by the updater when it moves the database (Installation → Database); once set, never start the stack from a compose file that does not read them. See Database and Environment variables. |
RESTOW_SETUP_TOKEN |
empty | The one-time token the setup wizard asks for. Empty: the api prints a fresh token to its log at every start until the setup is complete (docker compose logs api | grep 'SETUP TOKEN'). Set one (at least 16 characters, for example openssl rand -hex 16) for an unattended installation, or when container logs are shipped elsewhere; it is then never logged. No effect once the setup is complete. |
STORAGE_TARGET |
local |
The default storage target: local or s3. |
STORAGE_LOCAL_PATH |
/data/chunks |
The local path inside the api and worker containers (the restow-data volume). |
RESTOW_IMPORT_DIR |
./import |
Host directory mounted read-only at /var/lib/restow/import in the api and the worker. Each tenant uses its own subfolder <directory>/<tenant slug>/, which you create yourself: drop EML, MSG, MBOX or ZIP files there and pick them under Add source → Import mail files. See Mail file import and export and Environment variables. |
IMPORT_DIR |
/var/lib/restow/import |
The path inside the containers. Change it only together with the mount above. |
IMPORT_MAX_FILE_BYTES |
10 GiB | Largest file the upload accepts, in bytes. |
IMPORT_UPLOAD_TTL_HOURS |
48 |
Hours an unfinished or unused upload is kept before it is deleted. |
IMPORT_SEGMENT_BYTES |
8 MiB | Size of one upload chunk in bytes (64 KiB to 32 MiB). |
IMPORT_MAX_MESSAGE_BYTES |
256 MiB | Largest single message that is read into memory. |
IMPORT_MAX_STAGING_BYTES |
100 GiB | Bytes the uploads of one tenant may hold in staging together, counted by declared file size until their import has ended. A new upload that does not fit is refused. |
IMPORT_PARSE_WORKERS |
2 (1 to 8) |
Parser processes the worker runs at the same time for MSG files and message metadata. Each may use up to 512 MB; a file above 8 MiB gets a process of its own, up to 3 GB. Lower it on a small host. |
EXPORT_TTL_HOURS |
24 |
Hours a finished export can be downloaded before its file is deleted. |
EXPORT_MAX_TENANT_BYTES |
50 GiB | Bytes all unexpired export files of one tenant may take together. A new export request is refused, and one that no longer fits fails with a message, until older exports have expired. |
PREVIEW_PARSE_WORKERS |
2 (1 to 8) |
Parser processes the api runs at the same time for the mail preview and attachment downloads. |
PREVIEW_TIMEOUT_MS |
10000 |
How long one message may take for its formatted preview. A message that needs more is shown as text or as unreadable. |
RESTIC_BINARY, RESTOW_AGENT_DIR, RESTOW_AGENT_INSTALL_DIR |
restic from the PATH, /srv/agent, /srv/agent/install |
The image ships restic (/usr/local/bin/restic), the agent downloads (/srv/agent/<version>/<os>-<arch>/) and the install scripts (/srv/agent/install). The defaults are right for this stack; point them elsewhere only for a custom image. See Endpoint backup. |
RESTOW_RESTIC_CACHE_DIR |
the temp folder | Where restic keeps its per-endpoint caches. A folder on the data volume, for example /data/restic-cache, keeps them across restarts and makes browsing faster. |
PVE_BLOCK_MAP_CACHE_MB |
256 |
Since 0.4.0. Memory in MiB for the block maps of the VM disks being browsed or restored (Proxmox VE, a preview). Read by the api. See Environment variables. |
RESTOW_ENDPOINT_QUOTA_GIB, RESTOW_ENDPOINT_TENANT_QUOTA_GIB |
2048 (2 TiB), 20480 (20 TiB) |
Storage budget of the endpoint repositories in GiB (whole or decimal numbers; 0 means no budget): per endpoint, and for all endpoints of one tenant together. An administrator can set another value per machine. An upload beyond a budget is refused, restores keep working, and the alerts warn from 90 percent. Read by api and worker. |
Opt-in updater and mounter
Section titled “Opt-in updater and mounter”They mount the Docker socket, which is root on the host. Read Updates before you enable the updater and Mounter and NFS storage before you enable the mounter (a new installation starts it by default since 0.4.0). The mounter’s file share limits, RESTOW_MOUNTER_MAX_RUNNERS and RESTOW_MOUNTER_RUNNER_MAX_MEMORY_MIB, are not in env.example: they have defaults in the Compose file and are described in Environment variables. Enabling them needs no line below: you start each one with its own command (see updater and mounter). The mounter uses RESTOW_PROJECT_DIR like the updater. Everything here is optional.
| Variable | Default | Meaning |
|---|---|---|
RESTOW_PROJECT_DIR |
empty | Absolute path of the directory that holds docker-compose.yml and .env on the host, for example /opt/restow; the install script sets it. Empty: the directory is mounted at /project and the updater and the mounter read its host path from the mount. Set: it is mounted at the same path instead. The api also reads it, to show commands with cd <directory>. |
RESTOW_UPDATER_URL |
http://updater:8090 |
Where the api looks for the updater. Change it only for an updater that runs elsewhere. |
RESTOW_UPDATER_IMAGE |
empty | Written by the updater itself, pinned by digest; leave it empty. If you set it, use an image of the same build as RESTOW_IMAGE. See How the updater and the mounter pin their image. |
RESTOW_UPDATER_SELF_UPDATE |
true |
The updater moves itself to every signed release it installed. false keeps it where it is, and you then move it by hand. |
RESTOW_UPDATER_IMAGE_REPOSITORY, RESTOW_UPDATER_WEB_IMAGE_REPOSITORY |
empty: the release images of the updater’s own build | Where the updater pulls the application image and the web image from. Empty: the release images of its own build (ghcr.io/restow-backup/restow and restow-web, or restow-community and restow-web-community). Set both only for a registry mirror of the release images, and copy the signatures with them. |
RESTOW_UPDATER_SOURCE_HOSTS |
empty: source mode off | Repositories (git.example.com/acme/restow, github.com/acme/restow) or whole hosts (git.example.com), comma-separated, that the updater may build an update from (“source” mode, a custom update source under Settings, Updates). Empty: only the signed release images of the default source can be installed from the web interface. Only the operator sets this, in .env (see Two modes). Whatever is built from these repositories runs with access to all data and keys: list only repositories you trust as much as the release itself. Do not list github.com alone. Recreate the updater after a change: docker compose --profile updater up -d updater. |
RESTOW_MOUNTER_IMAGE |
empty | Written by the mounter itself, pinned by digest; leave it empty. |
RESTOW_MOUNTER_URL |
http://mounter:8091 |
Where the api looks for the mounter. Change it only for a mounter that runs elsewhere. |
| Variable | Default | Meaning |
|---|---|---|
RESTOW_PRODUCT_NAME |
Restow |
The product name that the web app, mails, reports and the API documentation show. A display name only; the variable names, programs and headers keep theirs. |
JOURNAL_SMTP_PORT |
empty: receiver off | Port of the archive’s journal SMTP receiver (Business and Service Provider). Empty keeps it closed. For Exchange Online journal reports set 25 and JOURNAL_SMTP_BIND=0.0.0.0: the receiver must be reachable from the internet. |
JOURNAL_SMTP_BIND |
127.0.0.1 |
The host address the receiver’s port is published on. Without a bind address it is mapped on the loopback interface only. |
JOURNAL_HOSTNAME |
none | The host name Exchange Online delivers to, for example archive.example.com, without scheme, port or mail address. It forms the tenants’ journal addresses, journal+<token>@<this host>, shown on the archive page; publish it in DNS. |
JOURNAL_TLS_DIR |
./journal-tls |
Host directory, mounted read-only into the api as /etc/restow/journal-tls, that holds the certificate and the key. |
JOURNAL_TLS_CERT_PATH |
none | /etc/restow/journal-tls/fullchain.pem: the certificate chain (PEM, leaf first). |
JOURNAL_TLS_KEY_PATH |
none | /etc/restow/journal-tls/privkey.pem: the private key (PEM, without a passphrase). |
Settings that env.example does not list (since 0.4.4). The release file and its env.example have changed since 0.4.3 only in the image tags. New optional settings reach api and worker through the .env file all the same, because env_file hands the whole file to them: JOURNAL_RATE_PER_IP (600 connections per minute) and JOURNAL_RATE_PER_ADDRESS (300 deliveries per minute) for the journal receiver, and the ARCHIVE_CAPTURE_*, ARCHIVE_BACKFILL_CONCURRENCY, ARCHIVE_GRAPH_* and ARCHIVE_IMAP_* settings of the archive capture. Since 0.4.5 RESTOW_RUN_AS_COMMUNITY belongs to them (empty by default; the repository’s .env.example lists it, the release’s does not): see License and editions. So does ENTRA_CLIENT_SECRET_EXPIRES_AT since 0.4.7 (empty by default; the date for the expiry reminders of a client secret in .env, see Environment variables). Add the ones you need to .env and recreate the services; the list with defaults is under Archive capture from the mailbox and Journal SMTP receiver. No port, volume or service changed: Graph notifications arrive on the edge’s 443, and forwarded copies on the same journal port as Exchange journaling.
The journal receiver’s certificate
Section titled “The journal receiver’s certificate”Exchange Online requires TLS (and so does the receiver for every other sender, a mail provider that forwards copies included), so the journal receiver does not start without a certificate and key, and it never falls back to a built-in certificate: the archive page then says “no TLS certificate”. The certificate must come from a publicly trusted authority and match JOURNAL_HOSTNAME. Put the two files (fullchain.pem and privkey.pem) into JOURNAL_TLS_DIR and set JOURNAL_TLS_CERT_PATH and JOURNAL_TLS_KEY_PATH to the paths above.
A renewed certificate is picked up from the same files within a few minutes, without a restart. The api reads the files at start and expects them to be valid: restart it after you first put them there, or after a start that failed because they were missing or expired.
Where the certificate comes from:
-
The same host as
RESTOW_APP_DOMAIN: copy the certificate that Caddy already holds, for example daily, in the directory ofdocker-compose.yml:Terminal window docker compose cp "caddy:/data/caddy/certificates/<ACME directory>/<host>/<host>.crt" ./journal-tls/fullchain.pemdocker compose cp "caddy:/data/caddy/certificates/<ACME directory>/<host>/<host>.key" ./journal-tls/privkey.pemList the
<ACME directory>withdocker compose exec caddy ls /data/caddy/certificates. -
Any other host name: an ACME client on the host (certbot, acme.sh, lego) with the DNS challenge, because Caddy holds port 80, and a deploy hook that copies the files into
JOURNAL_TLS_DIRwithcp -L, not the symlinks of certbot’slivedirectory.
The full set-up, with DNS, port 25 and the Microsoft 365 side, is on Exchange journaling.
What changed since 0.3.6
Section titled “What changed since 0.3.6”Compared with the file of 0.3.6, the release stack differs in these places. An installation keeps the file it was installed with, so after the update to 0.4.0 run the installer again to get it (Upgrading to 0.4.0). From 0.4.0 to 0.4.7 only the image tags in the examples changed: the file of 0.4.0 stays.
| Where | Change |
|---|---|
postgres volume |
The data directory is a typed mount read from RESTOW_POSTGRES_DATA_TYPE and RESTOW_POSTGRES_DATA (default: the volume pgdata), with create_host_path: false. |
api |
Joins the internal network runners besides default. |
worker |
New settings RESTOW_MOUNTER_URL, RESTOW_MOUNTER_MAX_RUNNERS (8) and RESTOW_MOUNTER_RUNNER_MAX_MEMORY_MIB (16384); new read-only volume restow-mounter-shared at /mounter-shared. |
mounter |
New settings RESTOW_MOUNTER_MAX_RUNNERS and RESTOW_MOUNTER_RUNNER_MAX_MEMORY_MIB; now also runs the file share runners; started by install.sh by default. |
networks |
New top-level network runners with internal: true. |
env.example |
New empty lines RESTOW_POSTGRES_DATA_TYPE, RESTOW_POSTGRES_DATA and PVE_BLOCK_MAP_CACHE_MB. |
Settings that are dangerous to change
Section titled “Settings that are dangerous to change”Keep changes of your own in docker-compose.override.yml (see above) and leave these lines of docker-compose.yml as they are:
| Setting | What depends on it |
|---|---|
name: restow |
The install script, the updater, the mounter and the volume names (restow_pgdata, restow_restow-data and the others, see Volumes) depend on it. |
The restow-updater-shared and restow-mounter-shared mounts of the api, and the restow-mounter-shared mount of the worker |
Without them the web interface cannot reach the updater or the mounter, and the worker cannot start file share runs. |
The database mount of postgres (type, source and create_host_path: false) |
After a database move the location lives in .env; a file without these lines starts PostgreSQL on the wrong directory. install.sh refuses such a file (exit code 8). |
The runners network, internal: true, and the networks of the api |
File share runners must reach the api and nothing else. Without the network the mounter reports runner_network_missing. |
stop_grace_period: 60s of the worker |
A shorter time cuts running jobs off before they save a checkpoint (see worker). |
The Docker socket mounts of the updater and the mounter |
They are root on the host. Do not add the socket to any other service. |
127.0.0.1 in the port of the api |
The api port stays on the loopback interface. The updater and the mounter publish no port at all. |
:ro on the import folder and on the journal TLS directory |
Both are mounted read-only on purpose (Restow never writes to or deletes from the import folder). |
${RESTOW_PROJECT_DIR:-.}:${RESTOW_PROJECT_DIR:-/project} |
The updater and the mounter find the host path of the project through this mount (see updater). |
Related pages
Section titled “Related pages”- Get started: requirements and the install, with the script or by hand.
- Environment variables: every variable, including those not in
env.example. - Network and ports and Ports and roles: every connection and who opens it.
- Updates: updating by hand and with the updater.
- Mounter and NFS storage: the mounter in use.
- File shares: what the mounter’s runners back up.
- Database: the database location and moving it.
- Exchange journaling: the journal receiver, DNS and the Microsoft 365 side.
- Mail file import and export: the import folder.
- Endpoint backup: the agent downloads and the restic settings.