Skip to content

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.

Terminal window
cp env.example .env # then fill in the required values
docker compose up -d

On 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.

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.

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 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-stopped
  • image is RESTOW_IMAGE. Compose stops with the message above when the variable is empty.
  • env_file hands the whole .env to the three roles as their environment, so every variable you set there reaches api, worker and scheduler alike. caddy gets only four variables (below), and updater and mounter get none.
  • depends_on waits for a healthy postgres. worker and scheduler write their own depends_on, because a service’s own key replaces the shared one: it names postgres again and adds the api.
  • restart: unless-stopped restarts the container after a crash or a reboot, unless you stopped it.

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_USER and POSTGRES_DB (both default restow) and POSTGRES_PASSWORD, which is required: Compose does not start postgres without it. Change the user and the database name only before the first start, and use the same names in DATABASE_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 volume pgdata; after a move to another disk (Installation → Database) it is that host directory. create_host_path: false makes 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, plus ROLE=api, RESTOW_UPDATER_URL (default http://updater:8090, where the api looks for the opt-in updater) and RESTOW_MOUNTER_URL (default http://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}:3000 Loopback only: health checks and the REST API on the host itself. Caddy reaches the api over 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_PORT is 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: set JOURNAL_SMTP_PORT=25 and JOURNAL_SMTP_BIND=0.0.0.0. The receiver needs a TLS certificate, see The journal receiver’s certificate.
  • Networks (since 0.4.0): default and the internal network runners. File share runners reach the api only through runners, which has no route to the outside (see Networks).

  • Volumes:

    Mount Why
    restow-data at /data The default local chunk store: STORAGE_LOCAL_PATH=/data/chunks lives here.
    ${RESTOW_IMPORT_DIR:-./import} at /var/lib/restow/import, read-only The server-side import folder: the api lists it, the worker reads 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-only The 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.pem and privkey.pem there and point JOURNAL_TLS_CERT_PATH and JOURNAL_TLS_KEY_PATH at /etc/restow/journal-tls/<file> in .env. When the api itself needs a restart, see The journal receiver’s certificate.
    restow-updater-shared at /updater-shared, read-only The updater’s shared secret, which the api sends with every request. Empty until the updater profile ran once. Keep this line.
    restow-mounter-shared at /mounter-shared, read-only The same for the mounter. Empty until the mounts profile ran once. Keep this line. An api without 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.

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 (default http://mounter:8091: the worker asks the mounter to start file share runners), RESTOW_MOUNTER_MAX_RUNNERS (default 8) and RESTOW_MOUNTER_RUNNER_MAX_MEMORY_MIB (default 16384), the same caps the mounter gets. Without a mounter, file share runs wait with share.mounter_unavailable.
  • Ports: none.
  • Volumes: restow-data at /data, the same chunk store as the api, the import folder, read-only, and (since 0.4.0) restow-mounter-shared at /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 a docker 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 most WORKER_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 postgres and a healthy api: the api applies 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.

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 postgres and a healthy api, for the same reason as the worker.
  • 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 as ghcr.io/restow-backup/restow-web:0.4.7 or ghcr.io/restow-backup/restow-web-community:0.4.7.
  • Settings: only four variables, no env_file. RESTOW_APP_DOMAIN is required, for example app.example.com (Compose stops without it). RESTOW_EDGE_HSTS and RESTOW_EDGE_TRUSTED_PROXIES default to empty. RESTOW_EDGE_TLS defaults to acme, and an empty value means acme too.
  • RESTOW_EDGE_TLS: acme gets the certificate from Let’s Encrypt for a public domain, or from Caddy’s own authority for localhost and internal names. internal uses Caddy’s own certificate authority for RESTOW_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.crt in the caddy-data volume, for example docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt .. See Behind a reverse proxy.
  • Ports: ${RESTOW_HTTP_PORT:-80}:80 and ${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-data at /data (the certificates and Caddy’s own authority) and caddy-config at /config.
  • Depends on: api (started, no health condition). Health check: none. Restart: unless-stopped.

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_IMAGE if set, else RESTOW_IMAGE. See How the updater and the mounter pin their image.

  • Settings: ROLE=updater, RESTOW_UPDATER_PROJECT_DIR (from RESTOW_PROJECT_DIR), RESTOW_UPDATER_SELF_UPDATE (default true), RESTOW_UPDATER_API_URL (http://api:3000), RESTOW_UPDATER_SOURCE_HOSTS, RESTOW_UPDATER_IMAGE_REPOSITORY and RESTOW_UPDATER_WEB_IMAGE_REPOSITORY. No env_file: it has no database access and holds no application secret.

  • Ports: none published. The service exposes 8090 on the Compose network only; the api reaches it at RESTOW_UPDATER_URL.

  • Volumes:

    Mount Why
    /var/run/docker.sock To run docker and docker compose against the host’s Docker. Root on the host.
    ${RESTOW_PROJECT_DIR:-.} at ${RESTOW_PROJECT_DIR:-/project} The directory with docker-compose.yml and .env. It is mounted at /project and the updater reads its host path from the mount. Set RESTOW_PROJECT_DIR (the absolute path, for example /opt/restow) only to mount it at the same path instead; the install script does that.
    restow-updater at /state The updater’s state and the database dumps it takes before an update.
    restow-updater-shared at /updater-shared The shared secret the updater writes on its first start and the api reads.
  • security_opt: no-new-privileges:true.

  • Health check: /healthz on port 8090 every 30 seconds, timeout 5 seconds, 3 retries, 10 seconds start period. Restart: unless-stopped.

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_IMAGE if set, else RESTOW_IMAGE.
  • Settings: ROLE=mounter, RESTOW_MOUNTER_PROJECT_DIR (from RESTOW_PROJECT_DIR) and, since 0.4.0, RESTOW_MOUNTER_MAX_RUNNERS (default 8, the hard cap of file share runners) and RESTOW_MOUNTER_RUNNER_MAX_MEMORY_MIB (default 16384, the upper bound of the memory per runner). No env_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 api reaches it.
  • Volumes: the Docker socket, the project directory (mounted as for the updater), restow-mounter at /state (its operation log) and restow-mounter-shared at /mounter-shared (the shared secret the api reads).
  • security_opt: no-new-privileges:true.
  • Health check: /healthz on 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.yml was made before 0.3.1 (0.3.0 was never published) has no mounter service, so Compose answers no such service: mounter, and its api has no mounter-shared mount. Run the install script again on it, see The two builds and no 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):

  1. While it is empty, the service starts the image in RESTOW_IMAGE. On that first start the updater writes RESTOW_UPDATER_IMAGE into .env itself, pinned by digest to the image it runs (for example ghcr.io/restow-backup/restow:0.4.7@sha256:...).
  2. From then on its image no longer follows RESTOW_IMAGE. If it cannot pin (a locally built image has no registry digest, or .env is not writable), the Updates tab shows the blocker “updater image not pinned”.
  3. After an update from a signed release succeeded, the updater sets RESTOW_UPDATER_IMAGE to that release’s verified image, by digest, and recreates itself. It never moves to an image built from source. RESTOW_UPDATER_SELF_UPDATE=false switches that off, and you then move it by hand.
  4. If you set RESTOW_UPDATER_IMAGE yourself, use an image of the same build as RESTOW_IMAGE (restow-community for the Community build).

The mounter (RESTOW_MOUNTER_IMAGE, optional):

  1. While it is empty, the service starts the image in RESTOW_IMAGE. On that first start the mounter writes RESTOW_MOUNTER_IMAGE into .env, pinned by digest to the image it runs. A local build has no digest and is not pinned.
  2. With the opt-in updater running, the updater moves it to the verified image of every signed update it installs and recreates the mounter.
  3. Without the updater, set RESTOW_MOUNTER_IMAGE to the new release’s image (or empty it) and run docker compose --profile mounts up -d mounter, once no change of a share is running.
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.

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).

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 with restow-nfs- (one per share, restow-nfs-<name>-<hash>), and the entries of the api and worker volumes whose source starts with restow-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 replaces docker-compose.yml with 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 .env must not set COMPOSE_FILE.

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.

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.

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.

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.

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 of docker-compose.yml:

    Terminal window
    docker compose cp "caddy:/data/caddy/certificates/<ACME directory>/<host>/<host>.crt" ./journal-tls/fullchain.pem
    docker compose cp "caddy:/data/caddy/certificates/<ACME directory>/<host>/<host>.key" ./journal-tls/privkey.pem

    List the <ACME directory> with docker 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_DIR with cp -L, not the symlinks of certbot’s live directory.

The full set-up, with DNS, port 25 and the Microsoft 365 side, is on Exchange journaling.

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.

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).