navree
Documentation

Harbor

Provision Ubuntu servers over SSH and ship zero-downtime, health-checked releases, with rollbacks, encrypted secrets, several servers, container images and push-to-deploy.

On this page

Introduction

Harbor takes your app from your repository to your servers. navree harbor:provision turns a fresh Ubuntu server into one ready to run a Navree app: a firewall, Caddy with automatic TLS, PostgreSQL, Redis and systemd units for the web server, the queue workers and the scheduler. navree deploy builds a release, uploads it, migrates the database, starts it next to the running one, and switches traffic only once its health check passes. navree deploy:rollback goes back to the previous release the same way.

Everything lives in one file, harbor.toml, in your repository: the servers and their roles, the processes, the environment, and the secrets, encrypted. Harbor is part of the navree CLI and works over SSH. Nothing runs on your servers but your app and the services it uses: no agent, no control panel, no account.

Deployment explains what a Navree deployment involves: the binary, the settings, the processes, the proxy and the health checks. Harbor does all of it for you, following the same rules, and this page says exactly what it sets up so you always know what is on your servers.

What you need

  • The CLI: cargo install --git https://github.com/NavreeHQ/navree navree-cli.
  • One or more servers running Ubuntu 22.04 or 24.04 LTS, on x86-64 or ARM64, from any provider, with SSH access as root using a key. A server with 1 GB of memory runs a small app with its database; 2 GB or more is comfortable.
  • A domain whose DNS A (and AAAA) record points at the server that takes web traffic, so Caddy can get a certificate.
  • An OpenSSH client (ssh) on the machine you deploy from. macOS, Linux and Windows 10 or later include one. Harbor runs it for every connection, so your ~/.ssh keys and agent work as usual.
  • To build on macOS or Windows, Docker or Podman: Harbor builds the Linux binary in a container. On Linux it can build natively.

Getting started

  1. Create harbor.toml

    From your app's directory:

    Shell
    Text
  2. Describe your server

    Edit harbor.toml: the package to deploy, the domain, and the server's address. One server can hold every role:

    harbor.toml
  3. Add your secrets

    Generate the application key and store the SMTP URL. Both are encrypted into harbor.toml:

    Shell

    The second command asks for the value without echoing it.

  4. Provision the server

    Shell

    Harbor connects as root, installs and configures everything the roles need, and records the server's SSH host key in harbor.toml. It takes a few minutes the first time.

  5. Deploy

    Shell

    Your app is live at https://crm.example.com. Commit harbor.toml; from now on, navree deploy is the whole release process.

harbor.toml

harbor.toml is the single description of where and how your app runs. This one runs the CRM on one server:

harbor.toml

Roles

A server's roles say what it runs. Give one server every role, or spread them over several:

RoleThe server runs
webCaddy, and your app's web process behind it
workerThe worker process: queue:work, or fleet with Fleet
schedulerThe scheduler process: schedule:work
postgresPostgreSQL, with your app's database and user
redisRedis

Processes

processes says what each role runs. The defaults are the ones above; change a command or run more instances of it:

TOML

instances runs that many copies of a worker on each server with the role. The scheduler always runs once per server, and running it on several servers is safe: each task runs once per tick however many schedulers share the database.

The environment

Your app reads its settings from APP_* environment variables, as configuration describes. On a server, Harbor writes them to /etc/<app>/env, readable only by root and the app's user, from three sources, later over earlier:

  1. the settings Harbor knows: APP_HTTP__TRUSTED_PROXIES (Caddy on loopback, plus deploy.trusted_proxies), APP_DATABASE__MIGRATE_ON_BOOT=false (a deploy migrates once, itself), APP_REALTIME__RELAY_URL (Caddy's internal entry, so workers reach the live web instance) and APP_LIGHTHOUSE__RELEASE (the release id, for Lighthouse);
  2. [env], for settings that aren't secret;
  3. [secrets], decrypted on your machine during the deploy.

Each web slot also has its own small file, /etc/<app>/blue.env or green.env, with APP_HTTP__ADDR (the slot's port on loopback) and the release that slot runs. Releases also carry your config/app.toml, so settings that are the same everywhere can stay there. As on any server, an environment variable overrides the TOML file.

On a server without the web role, APP_REALTIME__RELAY_URL points at the first web server's internal entry over the private network (http://<private_ip>:8079), so give your servers a private_ip when workers run apart from the web servers and broadcast.

Provisioning

navree harbor:provision prepares every server in harbor.toml; navree harbor:provision app-1 prepares one. It connects as ssh.provision_user (root) with your key and configures the server for its roles. Running it again is safe: it brings the server in line with harbor.toml and changes only what differs, so you also use it after adding a role, a server or a worker.

Text

Each area is one bash script, rendered from a template and sent over SSH to bash -s on standard input, so passwords and keys never appear on a command line. Harbor first checks the server: anything other than Ubuntu 22.04 or later, on x86-64 or ARM64, is refused before anything changes. A step that fails stops the run with its server, its area and the script's last output lines; fix the cause and run the command again. Provisioning as a user other than root runs each script with sudo -n, so that user needs password-less sudo.

What a server gets

AreaWhat Harbor sets up
SystemPackage updates, unattended-upgrades for security updates, chrony for time, a swap file on servers with less than 4 GB of memory, the time zone set to UTC, journald capped at 1 GB.
UsersA navree user that owns the app and runs its processes, with the keys in ssh.authorized_keys, and in the systemd-journal group so it can read the app's logs. Its sudo rights, in /etc/sudoers.d/harbor-<app>, are limited to starting, stopping and restarting the app's systemd units (and enabling or disabling the web slots), starting the database backup and reloading Caddy.
SSHPassword logins off; root may log in by key only. The host key is recorded in harbor.toml as host_key, and every later connection checks it.
Firewallufw, denying all incoming traffic except SSH, and on web servers ports 80 and 443 (TCP, and UDP for HTTP/3). On postgres and redis servers, ports 5432 and 6379 open to your other servers only. On a web server with a private_ip, port 8079 (the internal entry) open to the private addresses of your servers that run workers or the scheduler without the web role. fail2ban watches SSH.
Caddy (web)Caddy 2 from Caddy's apt repository: the reverse proxy in front of your app, with a certificate from Let's Encrypt for domain, renewed automatically; HTTP redirected to HTTPS; HTTP/2 and HTTP/3; compression; WebSocket upgrades for broadcasting. aliases redirect to domain. /metrics answers only the addresses in web.metrics_allow, and 404 to everyone else. With [web.custom_domains], workspaces' verified domains too, each with its own certificate (custom domains).
PostgreSQL (postgres)PostgreSQL 17 (postgres.version) from the PostgreSQL project's apt repository, a database and a user for the app with a generated password, scram-sha-256 authentication, connections from your servers only, and a daily pg_dump (a systemd timer) kept for postgres.backup_days (7) in /var/backups/postgresql/<app>.
Redis (redis)Redis from Ubuntu's own packages (7.0 on 24.04 LTS, 6.0 on 22.04 LTS), a generated password, maxmemory (redis.maxmemory_mb, 256 MB) with allkeys-lru, listening on loopback, and on the private network when other servers use it.
Node.js (web, with a front end)Node.js frontend.node_version (24) from NodeSource, and pnpm or Yarn through Corepack when the front end's lockfile asks for one.
systemdUnits for the app's processes, below.

The database URL (APP_DATABASE__URL) and the Redis URL (REDIS_URL), with their generated passwords, go straight into [secrets]: you never see or copy them.

The systemd units

Harbor runs your app's processes under systemd, with the same graceful shutdown the deployment page describes. The web server runs in two slots, blue and green, so a new release can start while the old one still serves:

/etc/systemd/system/[email protected]

/etc/crm-api/blue.env sets APP_HTTP__ADDR=127.0.0.1:8081, and green.env port 8082. The worker unit, [email protected] (one instance per processes.worker.instances: @1, @2, ...), and the scheduler unit, crm-api-scheduler.service, are the same with WorkingDirectory=/srv/crm-api/current and their own commands; they start once the first release is live. Every unit has the same hardening: the app can write to shared/ and nowhere else. A postgres server also gets crm-api-backup.service and its daily crm-api-backup.timer.

The Caddy site

/etc/caddy/Caddyfile imports one site file per app from /etc/caddy/sites/, so several apps can share a server:

/etc/caddy/sites/crm-api.caddy

Caddy sends X-Forwarded-For and X-Forwarded-Proto, and Harbor sets APP_HTTP__TRUSTED_PROXIES=127.0.0.1,::1, so your app sees each client's real address, as behind a reverse proxy explains. A deploy rewrites the site file for the new slot and reloads Caddy gracefully; the deploy user owns this one file, and nothing else in /etc/caddy. On a server with a private_ip and workers elsewhere, the internal entry also binds the private address. When your app's Cargo.toml turns on navree's grpc feature (or depends on navree-grpc), Harbor proxies with h2c://127.0.0.1:8081, so gRPC works end to end over HTTP/2.

Secrets

Secrets live in [secrets], encrypted with AES-256-GCM under the key in harbor.key. Each value is encrypted with its name as associated data, so a value can't be copied under another name, and the names stay readable: a pull request that adds a secret shows which one.

Shell
  • The key never leaves your machine. harbor.key is in .gitignore. Share it with your team through a password manager. In CI, put its contents in the NAVREE_HARBOR_KEY environment variable instead.
  • Values are decrypted only during a deploy, on the machine running it, and written over SSH to /etc/<app>/env, inside the script on SSH's standard input (base64-encoded), never on a command line. They never appear in Harbor's output or logs, and never in a release directory.
  • Rotate the key with navree harbor:secrets rotate-key: it re-encrypts every value under a new key and writes the new harbor.key.

--stdin drops the one trailing newline echo adds, and an empty value is refused. A stored value is harbor:v1: followed by the nonce, the ciphertext and its tag in URL-safe base64; harbor.key holds base64: and the 32-byte key, the same form as an APP_KEY. Rotating a secret itself is setting it again and deploying.

Deploying

Shell
Text

A release id is the UTC time of the deploy and the short Git commit. Step by step:

  1. Build. Harbor builds the release binary for your servers' architecture: natively when the machine running the deploy is Linux on the same architecture with a glibc no newer than the servers', otherwise in a container (build.method). The container is Harbor's builder image, Ubuntu 22.04 with a stable Rust toolchain, built once on your machine (or the image in build.image); its glibc is no newer than the servers', so the binary runs on 22.04 and 24.04 alike. Container builds keep Cargo's registry, the toolchain and the target directory in named volumes (navree-harbor-*), so the second build is incremental.
  2. Upload. The binary, the directories in deploy.include (config and resources, when present) and a .harbor-release file naming the commit go to every server in parallel as one compressed archive over SSH, and are unpacked into a new directory under releases/. The directories in deploy.shared are links to shared/, so storage/ survives releases.
  3. Environment. /etc/<app>/env is written from the three sources.
  4. Migrate. migrate runs once, from the new release, on the first web server. If a migration fails, the deploy stops here and nothing else changes.
  5. Start. The idle slot (green) is pointed at the new release and started on its own port, next to the live one.
  6. Health check. Harbor requests deploy.health_check (/health/ready) on the new instance until it answers 200, for up to deploy.health_timeout_secs (60). Readiness runs every health check your modules register, the database included, so a release that can't reach its database never gets traffic. If it doesn't pass, Harbor stops the new instance, prints its last log lines, and the old release goes on serving as if nothing happened.
  7. Switch. Caddy is pointed at the new instance and reloads gracefully: connections in flight finish on the old one. The current link moves to the new release in one atomic rename.
  8. Drain. The old slot gets SIGTERM and finishes its requests in flight (http.shutdown_grace_secs, 30 seconds) before it exits.
  9. Restart workers. The worker and scheduler processes restart on the new release. Each finishes the jobs it is running first.
  10. Prune. Releases beyond deploy.keep_releases (5) are removed.
/srv/crm-api/
  • releases/
    • 20261026-091512-3f2a9c1/
      • crm-api
      • config/
      • storage -> ../../shared/storage
    • 20261025-163003-9b17e44/
  • current -> releases/20261026-091512-3f2a9c1
  • blue -> releases/20261025-163003-9b17e44
  • green -> releases/20261026-091512-3f2a9c1
  • shared/
    • storage/
OptionWhat it does
--server <name>Deploy to one server.
--env <name>Use harbor.<name>.toml; see environments.
--no-migrateSkip the migrations.
--maintenanceRun your app's down on every web server before migrating and up after the switch (maintenance mode), for migrations the running release can't work with. If the deploy fails, Harbor runs up again.
--release <id>Deploy a release already uploaded (one navree deploy:releases lists), without building.

Zero downtime, and what it asks of you

The old release keeps serving until the new one is healthy, and requests in flight finish where they started, so users see no errors during a deploy. Two things make that work, and they are yours to keep:

  • Migrations must suit both releases. The new release migrates the database while the old one still serves. Add columns and tables freely; rename or drop one over two deploys: first stop using it, then remove it. When that isn't possible, deploy with --maintenance.
  • WebSockets reconnect. Connections held by the old instance close when it drains, and Pusher clients reconnect to the new one within seconds. Real-time clients see a reconnect, not an error.

Workers and the scheduler finish their running jobs before they restart, so no job is cut off by a deploy; give your longest jobs a timeout_secs that fits in the units' stop timeout.

Rollbacks

Shell
Text

A rollback switches to the previous release with the same start, health check, switch and drain as a deploy, without building or uploading anything. --to <release> picks an older one, and navree deploy:releases lists them:

Text

Rollbacks don't undo migrations: the database stays as the newer release left it, which is why migrations must suit both releases. To undo the last batch as well, run navree harbor:run migrate:rollback before rolling back.

Several servers and roles

As your app grows, give each role its own servers:

harbor.toml
  • Private network. Servers with a private_ip talk to each other over it: PostgreSQL and Redis listen on it, and the firewall admits only your servers' addresses. Without private addresses, PostgreSQL is reached over the public ones with TLS required (hostssl and sslmode=require), and Redis, which has no TLS here, must stay on the server that uses it: harbor.toml is refused when another server needs Redis and either side lacks a private_ip.
  • One of each data server. At most one server has the postgres role and one the redis role; they can be the same server.
  • Rolling deploys. Harbor builds once, uploads to every server in parallel and migrates once. Then it switches the web servers one at a time, each after its own health check. If one fails, Harbor switches the ones it already moved back to the previous release, so all web servers always run the same release. Workers and the scheduler restart once every web server is on the new release.
  • A load balancer in front. With more than one web server, put your provider's load balancer in front, forwarding ports 80 and 443 to each one. Caddy still terminates TLS. Turn on web.proxy_protocol = true when the balancer speaks the PROXY protocol, so Caddy, and then your app, see each client's address.
  • SQLite or PostgreSQL. SQLite suits one server. With several servers, use the postgres role and the postgres feature.

To add a server, add its table, run navree harbor:provision <name>, then navree deploy.

In a Cargo workspace of several services, each service's package directory has its own harbor.toml with its own servers or roles. Run navree deploy from that directory to release one service without touching the others.

A front end next to your API

An app with a web front end in web/, such as one made by navree new --ui next or the SaaS starter kit, deploys it in the same release. Describe it in [frontend]:

harbor.toml
  • Harbor builds it during the deploy's build step (natively, or in a node:<frontend.node_version>-bookworm container when the binary builds in one), uploads it with the binary into the release, and installs Node.js (frontend.node_version, 24) on web servers when it provisions them. Without build and command, Harbor picks npm, pnpm or Yarn from the lockfile.
  • It runs in its own blue and green slots (frontend.ports, 3001 and 3002) as crm-api-frontend@blue and @green, with its own health check, and switches together with the API, so a page and the API it calls always come from the same release. Its environment has PORT, HOSTNAME=127.0.0.1, NODE_ENV=production and NAVREE_API_URL, the address of the API slot of the same release.
  • Caddy serves domain from the front end. The API stays on loopback, where the front end's server calls it; set api_domain to also serve the API publicly, for example api.crm.example.com for mobile clients.

Custom domains

When your workspaces bring their own domains (custom domains, DomainsModule), Caddy can serve every one of them with its own certificate, got on the domain's first visit. Turn it on:

harbor.toml

harbor:provision adds Caddy's on-demand TLS to /etc/caddy/Caddyfile, asking the app through the internal entry, so the question always reaches the live slot:

/etc/caddy/Caddyfile

and a site for any other host name to the app's site file:

/etc/caddy/sites/crm-api.caddy

Caddy asks the app before it gets a certificate for a host, and the app says yes only for verified domains, so nobody can make your server request certificates for names you don't serve. Point customers' CNAME records at domain (or a host name of yours with the same address) and set the app's domains.target to it. The global options belong to the server, so one app per server serves custom domains. Run navree harbor:provision again after changing the section.

Environments

Each environment has its own file: harbor.toml for production, harbor.staging.toml for staging, with its own servers, settings, secrets and key (harbor.staging.key). Pass --env to any command:

Shell

NAVREE_HARBOR_ENV=staging does the same for a whole shell or CI job.

Working with your servers

CommandWhat it does
navree harbor:statusEach server's roles, its processes and their state, the current release, disk and memory, and whether installed updates need a reboot.
navree harbor:logsYour app's log lines from journald, merged across servers by time, newest last. --server, --process web (or frontend, worker, scheduler), --lines 200 (100 by default, per server) and --follow narrow them down.
navree harbor:run <command> [args]Runs one of your app's commands on the first web server (or --server), from the current release with its environment, on your terminal: navree harbor:run migrate:status, navree harbor:run queue:retry all. Its exit code is navree's.
navree harbor:ssh [server]Opens a shell on a server as the navree user (the first web server by default).
navree harbor:backupRuns the PostgreSQL backup now on the postgres server; --download also copies the dump to the current directory.
navree harbor:status

Container images

Harbor servers run your binary under systemd; they don't need a container runtime. For platforms that run containers, navree build:image builds an OCI image of your app with Docker or Podman:

Shell
Text

The tag defaults to <app>:<short commit> (<app>:latest outside Git) and the platform to your machine's architecture. build:image works with or without harbor.toml: with one, it builds app with build.features and ships deploy.include. In a Cargo workspace, the build context is the workspace root, so path dependencies build too.

The image builds in two stages, rust:1-bookworm then debian:bookworm-slim:

ContentsThe binary at /app/crm-api, and config/ and resources/ when present
Working directory/app, so the binary finds config/app.toml
UserAn unprivileged user, uid 10001
Writable/app/storage, declared as a volume
EnvironmentAPP_HTTP__ADDR=0.0.0.0:8080, EXPOSE 8080
Entry point/app/crm-api, with serve as the default command

The same image runs every process: pass the command.

Shell

navree harbor:secrets export > production.env writes [env] and the decrypted [secrets] as an env file; keep the file out of your repository, and prefer your platform's secret store. Point the platform's liveness and readiness probes at /health/live and /health/ready.

To change the image, navree build:image --dockerfile writes the Dockerfile and .dockerignore it uses into your app; later builds use your copy. In a workspace, where the build context is the workspace root, the ignore file is Dockerfile.dockerignore next to the Dockerfile, the name BuildKit reads for that Dockerfile. Harbor never overwrites either file.

Push to deploy with GitHub Actions

navree harbor:ci github writes a workflow that tests and deploys every push to main (--branch <name> for another branch; with --env staging, the workflow deploys that environment):

.github/workflows/deploy.yml

Set it up once:

  1. Create a key pair for CI (ssh-keygen -t ed25519 -f ci-deploy -N ""), add ci-deploy.pub to ssh.authorized_keys, and run navree harbor:provision so the servers accept it.
  2. In the repository's settings, add two secrets: NAVREE_HARBOR_KEY, the contents of harbor.key, and NAVREE_HARBOR_SSH_KEY, the contents of ci-deploy.

The runner verifies each server against the host_key in harbor.toml, so it needs no known_hosts file. The concurrency group makes deploys queue one after another instead of overlapping, and a failing test stops the deploy before anything is built. When harbor.toml sits in a package directory below the repository root, the workflow is written at the root with a working-directory for the deploy step. Harbor won't overwrite an existing workflow file.

Security

  • SSH. Keys only, host keys pinned in harbor.toml, fail2ban on the SSH port. Deploys use the navree user, whose sudo covers only the app's units and Caddy.
  • Network. A deny-by-default firewall; databases reachable from your servers only; the app itself listens on loopback, behind Caddy.
  • TLS. Certificates from Let's Encrypt, renewed automatically, HTTP redirected to HTTPS. Navree's own Strict-Transport-Security header tells browsers to stay on HTTPS.
  • Secrets. Encrypted at rest in the repository, decrypted only on the deploying machine, stored on servers in a file only root and the app's user can read.
  • The app's processes. Run as an unprivileged user, with systemd hardening: no new privileges, a read-only system, a private /tmp, and write access to shared/ alone.
  • Updates. Security updates install automatically. Harbor never reboots a server on its own; navree harbor:status says when one needs a reboot.

The library

The navree-harbor crate does the work behind these commands, and the navree CLI has it built in. Tools can use it too: the facade re-exports it as navree::harbor with the harbor feature. It is opt-in, not part of full, since an app's runtime never needs it:

Cargo.toml
Rust

Every program Harbor runs (ssh, cargo, git, docker or podman) goes through an Executor: SystemExecutor starts real processes, and FakeExecutor records each command and answers from a script, which is how Harbor's own tests check every script it sends without a server. The release state machine runs against the Host trait: SshHost over SSH, and FakeHost in memory.

Reference

Commands

CommandWhat it does
navree harbor:initWrites harbor.toml and harbor.key, and ignores the key in Git.
navree harbor:provision [server]Prepares every server, or one, for its roles. Safe to run again.
navree harbor:secrets set <NAME> [--generate] [--stdin]Encrypts and stores a secret. --generate makes an application key.
navree harbor:secrets list, reveal <NAME>, unset <NAME>Lists names, prints one value, removes one.
navree harbor:secrets exportPrints [env] and the decrypted [secrets] as an env file.
navree harbor:secrets rotate-keyRe-encrypts every secret under a new key.
navree deployBuilds, uploads, migrates, health-checks and switches to a new release.
navree deploy:rollback [--to <release>]Switches back to the previous release, or to one you name.
navree deploy:releasesLists the releases on the servers.
navree harbor:status, harbor:logs, harbor:run, harbor:ssh, harbor:backupSee working with your servers.
navree harbor:ci github [--branch <name>]Writes the GitHub Actions workflow.
navree build:image [--tag] [--push] [--platform] [--dockerfile]Builds an OCI image of the app.

Every command takes --env <name>. Harbor looks for harbor.toml in the current directory and then in the directories above it.

harbor.toml keys

KeyDefaultWhat it does
appthe package next to harbor.tomlThe package to build and run.
domainrequired with a web serverThe domain Caddy serves and gets a certificate for.
aliases[]More domains, redirected to domain.
api_domainnoneWith [frontend], a domain that serves the API publicly.
frontend.pathnoneThe front end's directory; turns the front end on.
frontend.buildnpm ci && npm run build (or the pnpm or yarn equivalent for the lockfile)How to build it.
frontend.commandnpm start (or the equivalent)How to run it.
frontend.health_check/The path that must answer 200 before a switch.
frontend.ports[3001, 3002]The ports of the front end's blue and green slots.
frontend.node_version24The Node.js major version on web servers and in container builds.
build.methodautoauto, native or container.
build.features[]Cargo features of the build.
build.targetyour servers' architectureA Rust target triple, to override it, such as x86_64-unknown-linux-musl. A container build adds it to the builder's toolchain first.
build.imageHarbor's builder imageThe image container builds run in.
deploy.path/srv/<app>Where releases live on the servers.
deploy.keep_releases5Releases kept for rollbacks.
deploy.health_check/health/readyThe path that must answer 200 before a switch.
deploy.health_timeout_secs60How long the health check may take.
deploy.migratetrueRun migrate on deploy.
deploy.include["config", "resources"]Directories shipped with the binary.
deploy.shared["storage"]Directories kept across releases.
deploy.ports[8081, 8082]The ports of the blue and green slots.
deploy.internal_port8079The port of Caddy's internal entry, always the live slot.
deploy.trusted_proxies[]Proxies trusted besides Caddy, such as a load balancer's network.
processes.<role>.commandserve, queue:work, schedule:workThe command of the web, worker and scheduler processes.
processes.worker.instances1Worker processes per worker server.
servers.<name>.hostrequiredThe server's public address or host name.
servers.<name>.private_ipnoneIts address on a private network.
servers.<name>.roles["web", "worker", "scheduler"]What it runs.
servers.<name>.host_keywritten by harbor:provisionThe pinned SSH host key.
ssh.usernavreeThe deploy user.
ssh.provision_userrootThe user harbor:provision connects as.
ssh.port22The SSH port.
ssh.key~/.ssh/id_ed25519 when it exists, else your SSH agentYour private key; NAVREE_HARBOR_SSH_KEY replaces it.
ssh.authorized_keysthe public half of ssh.keyPublic keys allowed to log in as the deploy user.
web.metrics_allow[]Addresses allowed to fetch /metrics through Caddy.
web.proxy_protocolfalseAccept the PROXY protocol from the addresses in deploy.trusted_proxies, which it needs.
web.custom_domains.enabledfalseServe workspaces' own domains with on-demand TLS (see custom domains).
web.custom_domains.ask_path/_domains/allowThe app's ask endpoint, domains.ask_path.
web.custom_domains.toappWhere the domains go: app, or frontend with a [frontend].
postgres.version17The PostgreSQL major version.
postgres.databasethe app's name with _The database and user name.
postgres.backup_days7Daily backups kept.
redis.maxmemory_mb256Redis's memory limit.
env.<NAME>A setting for every process.
secrets.<NAME>An encrypted setting for every process.

Unknown keys are errors, so a typo never goes unnoticed, and every command that works with your servers checks the whole file first, listing each problem it finds. The secrets commands only need the file to parse.

Environment variables

VariableWhat it does
NAVREE_HARBOR_KEYThe secrets key, instead of harbor.key.
NAVREE_HARBOR_SSH_KEYThe private SSH key, instead of ssh.key.
NAVREE_HARBOR_ENVThe environment, instead of --env.