Skip to content

Deployment

What you are deploying

Eleven services. Five of them are stateful and will lose data if you treat their volumes as disposable.

ServiceImageRoleStateful
backendbuilt from Dockerfile.backendFastAPI API, port 8000no
worker-pipelinebuilt from Dockerfile.workerCelery, queues default + osintno
worker-massivebuilt from Dockerfile.workerCelery, queue massive, concurrency 1no
dbpostgres:15-alpineprimary storeyes
redisredis:7-alpineJWT revocation list, cacheyes
rabbitmqrabbitmq:3-management-alpineCelery brokeryes
elasticsearchelasticsearch:8.12.0full-text search (optional)yes
minioquay.io/minio/minioartifact storage (optional)yes
jaegerjaegertracing/all-in-one:1.53tracing (optional)no
naso-tor-1..nbuilt from infrastructure/torTor circuitsno
naso-tor-lbhaproxy:alpineload-balances the Tor clusterno

Elasticsearch and MinIO are genuinely optional: with their credentials unset, no client is constructed and /system/health reports them as disabled.

Evaluation deployment

bash
git clone https://github.com/fabriziosalmi/naso.git
cd naso
make bootstrap        # generates .secrets-mock/ and renders .env
make up               # docker compose up -d

Then wait for the API to report ready — not merely to answer:

bash
curl -s localhost:8000/system/status | jq -r .status   # want: operational

/system/status answers 200 even when degraded

It returns 200 with {"status": "degraded"} when the database probe fails, so that a monitoring system gets a body it can reason about rather than a bare connection error. Anything gating on the status line alone will call a container with a dead database healthy. The container HEALTHCHECK and the CI readiness gate both parse the body; yours should too.

Seed synthetic data if you want something to look at:

bash
make demo             # 'Operation Lazarus'

The frontend dev server runs separately:

bash
cd frontend && npm ci && npm run dev      # http://localhost:5173

Ports

Only 8000 is published by default from the application side. Jaeger publishes 16686 (UI), 4317 and 4318 (OTLP). The datastores are reachable only on the internal naso-net network.

Publishing more

If you add port mappings for Postgres, RabbitMQ management or the MinIO console, put them behind a firewall or a VPN. RabbitMQ management and the MinIO console are full administrative interfaces.

Production changes

docker-compose.yml is an evaluation and development baseline. It is not a production configuration, and it does not pretend to be. Before real data:

Secrets. Replace the .secrets-mock/ bind mount with real Docker secrets or a secret manager mounted at /run/secrets. Remember that a value in .env takes precedence over a file in /run/secrets — see Configuration.

TLS. Terminate in front of the API. Set NASO_COOKIE_SECURE=true and restrict ALLOWED_CORS_ORIGINS to the real frontend origin.

Images. Build with the default INSTALL_DEV=false. The Compose file sets INSTALL_DEV: "true" so that cli/validate.sh can run the test suite inside the running container; a production image should not ship pytest.

bash
docker build -f Dockerfile.backend -t naso-backend:1.0.0 .
docker build -f Dockerfile.worker  -t naso-worker:1.0.0  .

Tor. Rebuild with your own control-port password. The default is naso-dev and is documented, which means it is public:

bash
docker compose build --build-arg TOR_CONTROL_PASSWORD=<strong-pw>

Set NASO_DARKWEB_TOR_CONTROL_PASSWORD on the workers to the same value; they must be changed together or circuit rotation stops working.

Resource limits. The Compose file sets 1 CPU / 1 GB for the API, 1.5 / 2 GB for worker-pipeline and 1 / 1 GB for worker-massive. worker-massive runs at concurrency 1 by design — it handles jobs whose memory footprint scales with input size.

Admin provisioning. backend/init_db.py refuses to create the initial admin unless NASO_ADMIN_PASSWORD is set. There is no default and no fallback. Avoid a .local domain in NASO_ADMIN_EMAIL: email-validator treats it as a special-use TLD and the account will fail response validation on /users/me.

Non-root containers

Both application images run as uid 10001. Two things follow that are easy to trip over when you customise them:

  • The root filesystem is read-only. /tmp is a tmpfs; the workers also get /home/pwuser/.cache. Anything that needs to write elsewhere fails, by design. If you add a component that writes to disk, give it a volume explicitly rather than relaxing read_only.
  • Playwright browsers live in /opt/playwright, not ~/.cache/ms-playwright. playwright install --with-deps runs as root at build time, so the default location would be unreadable to pwuser — and /home/pwuser/.cache is a tmpfs at runtime, which would mask anything installed there anyway.

Upgrading

  1. Read CHANGELOG.md.
  2. Back up the Postgres volume. NASO is pre-1.0; migrations are forward-only.
  3. docker compose pull && docker compose build
  4. docker compose up -d
  5. Confirm with curl -s localhost:8000/system/health | jq.

Alembic migrations live in backend/alembic/.

Verifying a deployment

bash
make test        # delegates to cli/validate.sh

cli/validate.sh is the same script CI runs, in the same order: backend pytest inside the API container, frontend Vitest, and Playwright end-to-end flows. It prints a module tally and exits non-zero on any failure. If it passes, the stack works — not merely starts.