Docker configuration for the Nvisy server.
Nvisy requires three external services:
PostgreSQL 18+. PostgreSQL serves as the primary data store for all
application state: accounts, workspaces, documents, connections, and file
metadata. It uses the pgcrypto and pg_trgm contrib extensions, both bundled
with the standard image. The recommended image is postgres:18.
NATS 2.10+ with JetStream enabled. NATS handles pub/sub messaging for real-time events and persistent job queues for asynchronous processing. JetStream must be enabled with sufficient storage allocation: the default configuration uses 1 GB of memory store and 10 GB of file store.
An S3-compatible object store for first-party blobs — uploaded files,
detection audits, redacted output, and avatars. The compose files use
RustFS (MinIO-compatible, Apache-2.0); AWS S3 or any
S3-compatible service (MinIO, Cloudflare R2, …) works by pointing S3_ENDPOINT
at it. The configured S3_BUCKET must exist before the server starts — the
compose files provision it with a one-shot init container.
Start PostgreSQL, NATS, and RustFS for local development:
docker compose -f docker-compose.dev.yml up -dThis starts the services with development defaults (postgres:postgres
credentials, JetStream enabled, RustFS with rustfsadmin credentials and the
nvisy-dev bucket auto-created). Then generate configuration and run the server
locally:
make generate-all # .env, keys, migrations
cargo run --features dotenv --bin nvisy-cliThe API documentation is available at:
- Scalar UI:
http://localhost:8080/api/scalar - OpenAPI JSON:
http://localhost:8080/api/openapi.json
Build and run the complete stack:
cp .env.example .env
# Edit .env with production values
docker compose up -d --buildThe production compose file starts every service on a private bridge network. The server waits for the PostgreSQL, NATS, and RustFS health checks to pass — and for the bucket-provisioning init container to finish — before starting.
| Service | Port(s) | Description |
|---|---|---|
| PostgreSQL | 5432 | Primary database |
| NATS | 4222, 8222 | Message queue (JetStream) |
| RustFS | 9000, 9001 | S3-compatible blob store |
| Server | 8080 | Nvisy API |
All configuration is provided through environment variables. See
.env.example at the repository root for a complete
reference with defaults and descriptions.
The server requires an Ed25519 keypair for JWT signing and a 32-byte key for connection credential encryption. Generate both with:
make generate-keysThis produces three files: private.pem, public.pem, and encryption.key. In
production, store these securely and reference them via the environment
variables above.
The Dockerfile uses a multi-stage build:
- Planner: generates a dependency recipe with cargo-chef
- Builder: builds dependencies from the recipe (cached), then builds the server binary and strips it
- Runtime: minimal Debian image with only the binary and runtime libraries
The runtime image runs as a non-root user (nvisy, UID 1000) and includes a
health check endpoint at /health/.
The default NATS configuration (nats/nats.conf) enables JetStream with:
- 1 GB memory store for high-throughput streams
- 10 GB file store for persistent data
- 8 MB maximum payload size
Adjust these values based on expected workload. The memory store is used for ephemeral streams; the file store is used for durable subscriptions and job queues.
The server encrypts sensitive payloads at the application layer with XChaCha20-Poly1305 under per-workspace keys before they reach storage — file bytes, redacted output, analyzed documents, webhook signing secrets, and policy/context definitions. The blob store only ever receives ciphertext; any server-side encryption it offers is redundant defense-in-depth. This protects those payloads even against a live read of the datastore.
For everything else on disk — NATS stream/consumer metadata and KV entries, Postgres rows, backups — provision the data volumes on encrypted storage. This adds no runtime overhead, keeps key management out of the datastores, and covers the whole volume.
The persistent volumes to encrypt are nats_data, postgres_data, and
rustfs_data:
- Cloud: back the volumes with an encrypted block device (e.g. an encrypted EBS volume, or a cloud disk with default SSE enabled), or bind-mount them onto an encrypted filesystem.
- Self-hosted: place the Docker data root (or a bind mount for these volumes) on a LUKS-encrypted partition.
Volume encryption guards against stolen disks, snapshots, and backups. It does not protect against a compromised running host — which is why the sensitive payloads above are additionally encrypted at the application layer.
All services expose health check endpoints:
| Service | Endpoint | Method |
|---|---|---|
| Server | /health/ |
HTTP GET |
| PostgreSQL | pg_isready |
CLI |
| NATS | /healthz on port 8222 |
HTTP GET |
| RustFS | /health/ready on port 9000 |
HTTP GET |
The compose files configure health checks with 5-second intervals. The server depends on PostgreSQL, NATS, and RustFS being healthy before it starts accepting requests.
Migrations are embedded in the server binary and applied automatically on startup. For manual control:
make generate-migrations # Apply and regenerate schema
make clear-migrations # Revert all (destructive)# Start services
docker compose up -d
# View logs
docker compose logs -f
# Stop services
docker compose down
# Reset data
docker compose down -v