Dock
Run TimescaleDB (or plain PostgreSQL), Redis, Meilisearch, Mailpit, MinIO and NATS for local development with one command, in Docker or Podman, with your app already pointed at them.
On this page
Introduction
Dock starts the services your app needs on your machine. List them in dock.toml, run navree dock up, and TimescaleDB, Redis, Meilisearch, Mailpit, MinIO or NATS start in containers, wait until they are ready, and your app's settings point at them. navree dock down stops them; your data stays for next time.
Dock is part of the navree CLI and drives Docker or Podman, whichever you have. You write no Compose file and change nothing in your app's code: Dock generates the Compose file from dock.toml and runs it with docker compose or podman compose. The same dock.toml starts the same services in CI, so your tests run against what you develop against.
A new Navree app runs on TimescaleDB, Navree's default database, and navree new writes a dock.toml that runs it: navree dock up is the first thing you do in a new app. Its mail goes to the log until you add Mailpit. Add more services when your app uses a search engine, a message broker or object storage. (An app made with navree new --database sqlite needs no services at all.)
What you need
- The CLI:
cargo install --git https://github.com/NavreeHQ/navree navree-cli. - Docker or Podman. Docker Desktop or Docker Engine with Compose v2 (Docker Desktop includes it), or Podman (rootless works) with a compose provider for
podman compose:podman-composeordocker-compose. On macOS and Windows, Podman needs a running machine (podman machine start).
Getting started
A new app's dock.toml already lists postgres, which runs TimescaleDB. In an app without one, add the services you want, then start them:
navree dock:add postgres
navree dock:add mailpit
navree dock up created dock.toml # [services.postgres]
Added postgres. Your app needs navree's `postgres` feature.
updated dock.toml # [services.mailpit]
Added mailpit.
Starting crm-api services with docker
pulling timescale/timescaledb:2.31.0-pg17
postgres timescale/timescaledb:2.31.0-pg17 127.0.0.1:5432 ready in 2.0 s
mailpit axllent/mailpit:v1.31 127.0.0.1:1025 ready in 0.5 s
Mailpit's inbox: http://127.0.0.1:8025
Wrote 2 settings to .env: APP_DATABASE__URL, APP_MAIL__TRANSPORT.The first run pulls the images it doesn't have yet, with Docker's or Podman's own progress output. navree dock:add postgres prints the feature hint only when your Cargo.toml doesn't turn on postgres yet (a new app's does), and navree dock:add minio says how to point a storage disk at its bucket (driver = "s3" and the bucket's name; the disk reads MinIO's address and keys from the AWS_* settings).
Now run your app as usual. navree dev passes the services' settings to it, so it migrates the TimescaleDB database (where its time-series tables become hypertables) and sends its mail to Mailpit, whose inbox is at http://127.0.0.1:8025:
navree migrate
navree devWhen you're done for the day:
navree dock downdock.toml
dock.toml sits next to your app's Cargo.toml and lists its services. Commit it: everyone on the team, and CI, gets the same ones.
# Local services for crm-api. `navree dock up` starts them.
[services.postgres]
version = "17"
database = "crm_api"
[services.redis]
[services.mailpit]
[services.meilisearch]
[services.minio]
bucket = "crm-uploads"
[services.nats]
jetstream = trueA service with an empty table runs with its defaults. Services start, and their settings are listed, in the file's order. Top-level keys:
| Key | Default | What it does |
|---|---|---|
project | the package name | Prefix of container and volume names: crm-api-postgres, crm-api-postgres-data. Lower-case letters, digits, - and _. |
runtime | auto | docker, podman, or auto: Docker when the docker command exists, else Podman. NAVREE_DOCK_RUNTIME, when set, overrides it. |
write_env | true | Write the services' settings into .env (see how your app finds them). |
Dock checks the whole file before it runs anything. An unknown service or key, a port used twice, or a setting two services both set fails with the table and key at fault:
error: dock.toml: [services.redis]: unknown key `bucket`; redis takes image, version, port, env, maxmemory_mbThe services
| Service | Image | Ports on 127.0.0.1 | Settings your app gets |
|---|---|---|---|
postgres | timescale/timescaledb:2.31.0-pg17: PostgreSQL 17 with TimescaleDB (below); postgres:17 with timescaledb = false (pgvector/pgvector:pg17 with extensions = ["vector"] too) | 5432 | APP_DATABASE__URL=postgres://navree:[email protected]:5432/crm_api |
redis | redis:7 | 6379 | REDIS_URL=redis://127.0.0.1:6379, for RedisModule, your own code and the Redis Streams transport |
meilisearch | getmeili/meilisearch:v1.55 | 7700 | APP_SEARCH__ENGINE=meilisearch, APP_SEARCH__URL=http://127.0.0.1:7700, APP_SEARCH__KEY |
mailpit | axllent/mailpit:v1.31 | 1025 (SMTP), 8025 (inbox) | APP_MAIL__TRANSPORT=smtp://127.0.0.1:1025 |
minio | pgsty/minio:RELEASE.2026-08-04T00-00-00Z | 9000 (S3 API), 9001 (console) | AWS_ENDPOINT_URL=http://127.0.0.1:9000, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION=us-east-1 |
nats | nats:2.14 | 4222, 8222 (monitoring) | APP_MICRO__TRANSPORT=nats, APP_MICRO__URL=nats://127.0.0.1:4222 |
Each image is pinned, so everyone on the team runs the same version, and navree dock up prints the one it uses. Set version (the image tag; for TimescaleDB, the PostgreSQL major version) or image (any image, with or without a tag) to choose another. MinIO no longer publishes container images, so the minio service runs pgsty/minio, a maintained build of the MinIO server with the same S3 API, console and mc client.
Service settings
Every service takes image, version, port (the host port of its main port) and env (below). The rest are its own:
| Service | Key | Default | What it does |
|---|---|---|---|
postgres | database | the package name with _ | The database created on first start (crm-api gives crm_api). |
user, password | navree, navree | The role it is created with. | |
timescaledb | true (false for an image that isn't TimescaleDB's) | Runs TimescaleDB and creates its extension in the database once it is ready. false runs plain PostgreSQL. See PostgreSQL and TimescaleDB. | |
extensions | [] | More extensions created in the database once it is ready, such as "vector" for vector search. With timescaledb = false, "vector" switches to the pgvector image of the same major version. | |
redis | maxmemory_mb | 256 | Redis's memory limit. Data is saved with the append-only file. |
meilisearch | master_key | dock-meilisearch-master-key | The master key, passed to your app as APP_SEARCH__KEY. |
mailpit | smtp_port, ui_port | 1025, 8025 | The SMTP port (smtp_port is another name for port) and the inbox's web port. Mailpit accepts any SMTP credentials. |
minio | bucket | the package name with - | A bucket created once the server is ready. |
access_key, secret_key | navree, navree-secret | The credentials. The secret key needs at least 8 characters. | |
console_port | 9001 | The web console's port. | |
nats | jetstream | true | Turn on JetStream, for durable events. |
monitor_port | 8222 | The monitoring port. |
Users, passwords and keys may use letters, digits, ., _, ~ and -, so they go into URLs and commands as they are.
Every port is bound to 127.0.0.1 only, so nothing on your network can reach the services, and the passwords are development defaults. Dock is for your machine and CI, never for a server: Harbor provisions the production equivalents.
PostgreSQL and TimescaleDB
[services.postgres] runs TimescaleDB, Navree's default database: TimescaleDB 2.31.0 on PostgreSQL 17, from the timescale/timescaledb image, which also has pgvector. Dock starts the server with TimescaleDB preloaded and its telemetry off, and once the server is ready creates the timescaledb extension in your database, before the ones extensions lists. Your migrations then decide which tables become hypertables.
version picks the PostgreSQL major version under TimescaleDB, "16", "17" or "18":
[services.postgres]
version = "18" # timescale/timescaledb:2.31.0-pg18For plain PostgreSQL, turn TimescaleDB off. version is then any tag of the postgres image, and migrations that declare hypertables give regular tables, with a notice:
[services.postgres]
timescaledb = false
version = "16" # postgres:16An image of your own runs as named. TimescaleDB is on for TimescaleDB's images (timescale/timescaledb, timescale/timescaledb-ha, from any registry) and off for any other, such as PostGIS's below; set timescaledb = true for an image of your own that preloads TimescaleDB, and Dock creates the extension in it.
[services.postgres]
image = "postgis/postgis:17-3.5"The data volume keeps its PostgreSQL major version. Moving from plain PostgreSQL to TimescaleDB on the same major keeps your data: Dock preloads TimescaleDB on the command line, also on a volume the plain image created. Going back, or to another major version, needs an empty volume (navree dock down postgres --volumes), since plain PostgreSQL can't read hypertables.
How your app finds the services
Each service has settings your app reads, listed above. Dock delivers them two ways:
- As environment variables.
navree dev, and every app command the CLI forwards (navree migrate,navree queue:work,navree db:seedand the rest), pass them to your app's process whendock.tomlexists. Environment variables overrideconfig/app.toml, so they win even when the file setsdatabase.url, as a new app's does. A variable you already set in your shell wins over Dock's. - In
.env.navree dock upwrites them between two marker lines, for tools that read.env, and replaces that block on every run:
APP_KEY=base64:G60q3lWREHaFECJUwNhN58ArvmrSl0sptSaOEWgeBLk=
# dock:start (written by navree dock up; edit dock.toml instead)
APP_DATABASE__URL=postgres://navree:[email protected]:5432/crm_api
APP_MAIL__TRANSPORT=smtp://127.0.0.1:1025
# dock:endThe block holds the settings of every service in dock.toml, also when you start only some of them. When .env sets one of these keys again below the block, the later line wins, and navree dock up says so.
Navree reads .env before config/app.toml, so a key that the TOML file also sets keeps the TOML value (see where settings come from). When you start the binary without the CLI, with cargo run or from your editor, either remove those keys from config/app.toml or load the variables first:
eval "$(navree dock env --export)"
cargo run -- servenavree dock env prints every setting as NAME=value lines; --export prefixes them with export and quotes values for a shell. It reads only dock.toml, so it works before anything runs.
Changing what your app gets
Each service's env table replaces its settings. Values may use {host} and {port} (the main port) for every service, plus the service's own:
| Service | Placeholders |
|---|---|
postgres | {user}, {password}, {database} |
meilisearch | {key} (the master key) |
mailpit | {ui_port} |
minio | {bucket}, {user} and {password} (the access and secret keys), {console_port} |
nats | {monitor_port} |
[services.postgres]
database = "crm_api"
env = { APP_DATABASE__URL = "postgres://{user}:{password}@{host}:{port}/{database}?application_name=crm-dev" }
[services.minio]
bucket = "crm-uploads"
env = { CRM_UPLOADS_ENDPOINT = "http://{host}:{port}", CRM_UPLOADS_BUCKET = "{bucket}" }A placeholder the service doesn't have is an error that lists the ones it has.
Commands
| Command | What it does |
|---|---|
navree dock up [service...] | Starts every service, or the ones named, and waits until each is ready. Pulls images the first time. Running it again starts only what isn't running, and recreates a service whose image or ports you changed. |
navree dock down [service...] [--volumes] | Stops and removes the containers. --volumes also deletes their data. |
navree dock status | Each service's state, image and addresses. |
navree dock logs <service> [--follow] | A service's output (-f for short). |
navree dock env [--export] | Prints the settings your app gets. |
navree dock shell <service> | Opens the service's own client: psql for PostgreSQL, redis-cli for Redis, the nats CLI for NATS (in a natsio/nats-box container on the project's network), and a shell in the container for the others (MinIO's has mcli, already pointed at the server as dock). |
navree dock:add <service> | Adds a service to dock.toml, creating the file if needed. Comments and the rest of the file stay as they are. |
navree dock:remove <service> [--volumes] | Stops a service and removes it from dock.toml. |
navree dock up checks every port first: when one is taken, it says which, and names the container that holds it when a container does, before starting anything. Then it waits for each service's own readiness check (pg_isready over TCP, a Redis PING, Meilisearch's /health, Mailpit's /readyz, MinIO's /minio/health/live and NATS's /healthz), creates the PostgreSQL extensions (TimescaleDB's first) and the MinIO bucket, and fails with the service's last 20 log lines if one isn't ready within 60 seconds. Checking over TCP matters for PostgreSQL: during its first start the image sets the database up on a server that only listens on a Unix socket, then restarts it. When Docker or Podman doesn't answer within 30 seconds (Docker Desktop isn't running, or the Podman machine is stopped), Dock says so instead of waiting forever.
Service State Image Address
postgres running timescale/timescaledb:2.31.0-pg17 127.0.0.1:5432
redis running redis:7 127.0.0.1:6379
mailpit running axllent/mailpit:v1.31 127.0.0.1:1025, 127.0.0.1:8025
meilisearch stoppedYour data
Each service keeps its data in a named volume, <project>-<service>-data, so navree dock down and a reboot lose nothing. To start a service from empty, remove its volume:
navree dock down postgres --volumes
navree dock up postgres
navree migrate:fresh --seedSeveral apps on one machine
Each app's services are named after its project, so two apps never share a container or a volume. They do share your machine's ports: when a port is taken, navree dock up stops before starting anything and names the port, the key to change and, when another container holds it, that container. Give the second app other ports:
[services.postgres]
port = 5433
[services.redis]
port = 6380The settings your app gets follow the ports you choose.
Docker or Podman
Dock runs the same containers with either. With runtime = "auto" it uses Docker when the docker command exists, otherwise Podman. NAVREE_DOCK_RUNTIME overrides dock.toml, so you can use the other one yourself without changing the team's file:
export NAVREE_DOCK_RUNTIME=podmanDock writes the Compose file it runs to .navree/dock/compose.yaml before every command. Don't edit it, edit dock.toml; the .navree/ directory carries its own .gitignore, so it never reaches git. Containers and volumes carry the label dev.navree.dock.project=<project>, so docker ps --filter label=dev.navree.dock.project lists everything Dock started.
In CI
The same dock.toml gives your CI job the same services. On GitHub Actions:
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo install --git https://github.com/NavreeHQ/navree navree-cli --locked
- run: navree dock up postgres nats
- run: navree dock env >> "$GITHUB_ENV"
- run: cargo test --lockednavree dock env >> "$GITHUB_ENV" makes the settings environment variables for the following steps. GitHub's runners have Docker with Compose, so nothing else is needed.
Tests against TimescaleDB
TestApp never reads .env or the environment, so tests run on a temporary SQLite database by default and need no service: the same migrations run there, with hypertables as regular tables. For tests that must run on TimescaleDB (a continuous aggregate, first and last, month buckets), hand the URL to the test app yourself:
use navree::prelude::*;
use navree::testing::TestApp;
/// Runs only where `navree dock up postgres` (or CI) provided a database.
#[tokio::test]
async fn pipeline_totals_on_timescaledb() {
let Ok(url) = std::env::var("APP_DATABASE__URL") else {
eprintln!("skipped: no APP_DATABASE__URL");
return;
};
let app = TestApp::builder()
.config("database.url", url)
.config("database.migrate_on_boot", true)
.module(DatabaseModule::from_config())
.module(DealsModule)
.build()
.await;
// ...
app.shutdown().await.unwrap();
}cargo test doesn't go through the CLI, so run eval "$(navree dock env --export)" first on your machine. Tests that share one database see each other's rows: create the records each test needs and assert on those, or run these tests one at a time with cargo test -- --test-threads=1.
The library
The navree-dock crate does the work behind navree dock. Tools and tests can use it too: the facade re-exports it as navree::dock with the dock feature. It is opt-in, not part of full, since an app's runtime never needs it:
[dependencies]
navree = { git = "https://github.com/NavreeHQ/navree", features = ["dock"] }use std::path::Path;
use navree::dock::DockFile;
fn main() -> Result<(), navree::dock::DockError> {
// The settings `navree dev` would pass, read from ./dock.toml.
let file = DockFile::load(Path::new("."))?;
for (name, value) in file.env() {
println!("{name}={value}");
}
Ok(())
}Dock runs the commands through an Executor: ProcessExecutor starts real processes, and FakeExecutor records them and answers from a script, which is how Dock's own tests check the exact commands it runs without Docker.