navree

Deployment

Build a release binary, configure it through the environment, run the server, queue workers and the scheduler under a process manager, and put it behind a reverse proxy.

On this page

A Navree app deploys as one binary. The same executable serves HTTP, runs queue workers, runs the scheduler and migrates the database; you pick the job with its first argument. This page covers building that binary, the settings production needs, the processes to run, and putting the app behind a reverse proxy with health checks.

The examples use Linux with nginx and systemd because they are common. Navree doesn't depend on either: any reverse proxy that forwards HTTP and any process manager that runs a command and sends it a stop signal will do.

Building a release binary

Shell

The binary is target/release/<package>, for example target/release/planner (planner.exe on Windows). Build it on the platform you deploy to, or cross-compile for it. TLS uses rustls and SQLite is compiled in, so the binary doesn't need OpenSSL or a SQLite library at runtime.

A release build changes two defaults: logs are written as JSON lines instead of the pretty format, and templates are read once instead of reloaded when they change. See Configuration: environments.

If you want more speed from the binary at the cost of longer builds, tune the release profile in your Cargo.toml. The benchmark's build of Navree used these settings:

Cargo.toml

What to copy to the server

The binary reads .env and config/app.toml from its working directory, and keeps runtime data in storage/ by default. A deployment directory looks like this:

/srv/planner/
  • plannerthe release binary
  • config/
  • app.tomlcopied from the repository
  • storage/writable by the app's user

Copy config/app.toml with the binary and run every command from this directory (systemd's WorkingDirectory=). Template files under resources/views/, if your app uses them, go here too. Templates generated by make:mail are compiled into the binary.

Production settings

Production settings come from environment variables. They override config/app.toml, keep secrets out of the repository, and work the same for every process.

/etc/planner/planner.env

Check these before the first deploy:

SettingWhy
APP_KEYWithout it, JWTs and signed URLs use a random key per process, so every restart signs everyone out, and encryption fails. Every process needs the same key. See The application key.
database.migrate_on_bootNew apps set it to true in config/app.toml. In production, run migrate as a deploy step instead, so a failed migration stops the deploy, not the server.
database.urlThe default is SQLite in storage/. For PostgreSQL, build with the postgres feature and set a postgres:// URL. See Database: getting started.
http.addr127.0.0.1:8080 by default. Use 0.0.0.0:8080 when the proxy runs on another host or in another container.
http.trusted_proxiesWithout it, behind a proxy every request seems to come from the proxy's address, so the per-IP rate limit (600 requests a minute by default) and the login throttle apply to all your users together.
app.urlAbsolute links in mail, such as email verification, use it. Navree doesn't read X-Forwarded-Proto or Host to build URLs.
observe.metrics_tokenGET /metrics is public by default. Set a token, or block the path at the proxy.

Keep the environment file readable only by root and the app's user: it holds the application key and the passwords. Navree never logs them; Config's debug output masks secret-looking keys and URL passwords.

The commands an app ships

The binary runs one command and exits, except for the long-running ones. Every command except check, list and help boots the app first, connecting to the database and, with migrate_on_boot, migrating it, so it needs the same configuration as the server. planner list prints them all; a new app has these:

CommandWhat it does
serve [--addr HOST:PORT]Runs the HTTP server until it gets a stop signal. Running the binary with no command does the same.
queue:work [--queue a,b] [--once] [--stop-when-empty] [--max-jobs N]Runs queued jobs until it gets a stop signal.
queue:failed, queue:retry <id or all>, queue:flushLists, retries or deletes failed jobs.
migrateRuns every pending migration.
migrate:statusShows which migrations have run.
migrate:rollbackRolls back the last batch.
migrate:fresh [--seed]Drops every table and migrates again. Never run it in production.
db:seed [name...]Runs the seeders.
down [--retry SECS] [--message TEXT], upTurns maintenance mode on and off.
cache:clearEmpties the cache store.
routes [--json]Lists the routes and their access rules.
openapi:export [path]Writes the OpenAPI document to a file.
access:role, access:grant, access:explainCreates roles, grants permissions and explains access decisions.
key:generatePrints a new application key.
check [--graph]Validates the module graph without starting anything.

Modules add their own. With ScheduleModule in the app you get schedule:work, schedule:run and schedule:list, and with AuditModule you get audit:prune. A new app includes neither. See Console commands and the CLI.

Processes to run

A typical deployment runs up to three long-lived processes from the same binary and configuration:

ProcessCommandNeeded when
Web serverplanner serveAlways.
Queue workerplanner queue:workYou queue jobs, queued mail or queued listeners. New apps use the database queue driver.
Schedulerplanner schedule:workThe app includes ScheduleModule and declares scheduled tasks.

Instead of schedule:work, you can run planner schedule:run from cron every minute. Each tick of each task runs once however many schedulers share the database, so running the scheduler on several servers is safe.

All of them stop cleanly on SIGTERM (or Ctrl-C):

  • serve stops accepting connections and waits up to http.shutdown_grace_secs (30 seconds) for requests in flight.
  • queue:work finishes the jobs it is running (up to queue.workers, 4 by default) and takes no new ones.
  • Then the app waits up to 30 seconds for background tasks, and up to 30 seconds more for shutdown hooks, before it exits.

Give your process manager a stop timeout longer than that total.

If the app broadcasts over WebSockets (RealtimeModule), only serve holds the sockets. Workers and the scheduler post their broadcasts to it over HTTP: set realtime.relay_url (auto means http.addr) and give every process the same APP_KEY. See Real-time broadcasting.

Running under systemd

One unit per process. This one runs the web server:

/etc/systemd/system/planner.service

The queue worker's unit is the same with another description and command:

/etc/systemd/system/planner-queue.service

Enable and start them:

Shell

Logs go to standard output, so journalctl -u planner shows them, as JSON lines in a release build. To also write rotating log files, set log.file; see Logging.

A deploy, step by step

  1. Build and copy

    Build the release binary and copy it and config/app.toml to the server, next to the running version.

  2. Migrate

    Run the migrations from the deployment directory, with the production environment:

    Shell

    For a migration the running version can't work with, put the app in maintenance mode first with ./planner down, and run ./planner up afterwards.

  3. Restart

    Shell

    Requests that arrive while the server restarts fail: Navree doesn't hand its listening socket over to a new process. Run two instances behind the proxy and restart them one at a time if that matters.

Behind a reverse proxy

Navree serves plain HTTP. Put a reverse proxy in front of it to terminate TLS, and tell Navree which proxies to trust so it sees each client's real address.

This nginx server block proxies everything to the app on loopback:

/etc/nginx/sites-available/planner.conf

With APP_HTTP__TRUSTED_PROXIES=127.0.0.1,::1, Navree takes the client's address from X-Forwarded-For on requests that come from nginx. It reads the header from the right and takes the first address that isn't a trusted proxy, so a client can't choose its address by sending its own X-Forwarded-For. Requests from any other peer are identified by the peer's own address. trusted_proxies accepts addresses and CIDR blocks such as 10.0.0.0/8, which suits a load balancer with changing addresses.

http.trust_proxy = true trusts the headers from every peer. Use it only when nothing can reach the app except through the proxy; trusted_proxies is safer.

Navree sends security headers by default, including Strict-Transport-Security, which tells browsers to use HTTPS only. Serve the app over HTTPS, as above. See CORS and security headers.

Health checks

ObserveModule, which new apps include, serves two probes outside the API prefix, without authentication:

EndpointAnswersUse it for
GET /health/liveAlways 200 {"status":"up"} while the process serves HTTP. It runs no checks.Liveness: restart the process when it fails.
GET /health/ready200 when every health check is up, 503 when one is down. Each check has 2 seconds.Readiness: take the instance out of the load balancer while it fails.
Shell
JSON

DatabaseModule, CacheModule and StorageModule each register a check, and your modules can add their own. Both probes and /metrics keep answering during maintenance mode (http.maintenance_except), so an orchestrator doesn't restart instances that are down on purpose. Don't send an Authorization header to the probes: a present but invalid token is a 401 on every route, public ones included. Health and metrics has the details.

Containers

The same rules apply in a container: the binary, config/ in the working directory, settings from the environment, and APP_HTTP__ADDR=0.0.0.0:8080 so the server listens outside the container. The benchmark runs Navree in a two-stage image, building with rust:1-bookworm and running on debian:bookworm-slim (137 MB); its bench/frameworks/navree/Dockerfile is a working example. Because the benchmark app depends on Navree by path, that Dockerfile copies the repository's crates/ into the build stage, and an app of yours needs the same until the crates are published.

The database in production

SQLite is the default and suits a single server: Navree opens it in WAL mode with a 5-second busy timeout, so the server, workers and scheduler on one machine can share the file. Keep storage/ on a persistent disk and back the file up.

For several servers, use PostgreSQL. Turn on the postgres feature of navree and set database.url. Be aware of the testing status in 0.1: the benchmark runs Navree on PostgreSQL (the pool, the unit of work, pagination, inserts and the auth user lookup), but the test suite runs on SQLite, and the PostgreSQL-only paths, such as the queue's FOR UPDATE SKIP LOCKED job reservation, haven't been run against a real server. See the release notes.

Popular pages

  • Installation
  • Your first app
  • Routing and controllers
  • Validation
  • Authentication
  • Benchmarks