Skip to content

Deploying Nexus

Nexus is a Go binary packaged as a container image and run as a Scaleway serverless container (nexus-<env>). One HTTP/1.1 server on port 8080 serves every ConnectRPC surface and /healthz.

Environment variables

Set on the container by infra/terraform/modules/environment/main.tf.

Variable Description
NEXUS_DATABASE_DSN PostgreSQL DSN for the environment's database on the shared RDB (sslmode=require). Secret.
NEXUS_LOG_LEVEL / NEXUS_LOG_FORMAT debug/info/warn/error; the container uses json.
NEXUS_LISTEN_ADDR Bind address. Default :8080 (PORT also honoured).
NEXUS_AUTHZ_ADDR Authz base URL including scheme, e.g. https://dev.authz.digilan-edge.com. Required: nexus refuses to start without it.
NEXUS_AUTHZ_API_KEY Bearer token for authz; same value as authz's AUTHZ_API_KEY. Secret.
NEXUS_ZITADEL_ISSUER OIDC issuer; the JWKS is loaded from <issuer>/oauth/v2/keys.
NEXUS_ZITADEL_PROJECT_ID The environment's Zitadel project id; checked against token aud.
NEXUS_ZITADEL_URL Zitadel Management API base URL.
NEXUS_ZITADEL_SERVICE_TOKEN PAT of the nexus-service machine user. Secret.
NEXUS_REALTIME_URL Realtime Worker base URL. Required.
NEXUS_INTERNAL_API_KEY Shared secret with the realtime Worker (X-Internal-Key). Required. Secret.
NEXUS_OTA_S3_ENDPOINT, NEXUS_OTA_S3_REGION, NEXUS_OTA_S3_BUCKET The environment's release bucket.
NEXUS_OTA_S3_ACCESS_KEY, NEXUS_OTA_S3_SECRET_KEY Read-only object-storage identity used to presign downloads. Secret.
NEXUS_RELEASE_PUBLISHER_KEY X-Release-Key that CI presents to ReleasePublisherService. Secret.
NEXUS_RELAY_ZONE_SUFFIX Zone for per-gateway names. Set only when relay_zone_suffix is set; empty disables per-gateway names and certificates.
NEXUS_CLOUDFLARE_ZONE_ID, NEXUS_CLOUDFLARE_API_TOKEN Zone and token for per-gateway records and ACME dns-01. Token is secret.
NEXUS_ACME_DIRECTORY_URL, NEXUS_ACME_ACCOUNT_KEY ACME directory (unset means Let's Encrypt staging, with a warning) and account key. Key is secret.
NEXUS_RELAY_TUNNEL_KEY Secret from which per-assignment relay tunnel tokens are derived. Secret.
NEXUS_MRWIFI_URL, NEXUS_MRWIFI_API_KEY Monsieur Wifi API used by enrollment, when configured.
NEXUS_REDIS_ADDR Optional Redis cache.
NEXUS_DEV_AUTH true disables token validation. Local development only.

Terraform also sets NEXUS_JWT_PRIVATE_KEY; the current nexus does not read it.

What happens when something is missing:

  • Without NEXUS_AUTHZ_ADDR (and without NEXUS_DEV_AUTH=true) nexus exits.
  • Without NEXUS_REALTIME_URL or NEXUS_INTERNAL_API_KEY nexus exits.
  • If the JWKS cannot be loaded at startup nexus exits.
  • Without the OTA variables it logs OTA object store not configured and offers no agent updates.
  • Without the relay/ACME variables it logs that per-gateway names and certificate issuance are disabled.

Running locally

cd platform/nexus
docker compose up -d        # Postgres :5432, Redis :6379
make dev                    # air hot-reload on :4001

See Local dev stack for the full stack.

Releasing

Only a tag runs the pipeline; merging to main deploys nothing.

make tag:patch     # or tag:minor / tag:major: tags vX.Y.Z and pushes it

On every tag CI builds and pushes three images, rg.fr-par.scw.cloud/digilan/nexus:dev, :stg and :prd, then:

Job Container When
deploy_dev nexus-dev automatically
deploy_stg nexus-stg manual
deploy_prd nexus-prod manual

A deploy job resolves the container by name and asks Scaleway to redeploy it, which pulls the tag the container references. The prod container references :prd (not :prod); nexus_image in environments/prod/terraform.tfvars must match.

Authz (platform/authz) follows the same pattern, except that all three of its deploy jobs, including dev, are manual.

Migrations

SQL migrations live in internal/store/pgstore/migrations/. Nexus applies them on startup, before the HTTP server binds, then seeds the roles into authz. There is no separate migration binary.

Background work

Pruning (device logs, metrics, expired tickets, abandoned relays) runs on an hourly ticker inside the process. With min_scale = 0 it runs only while an instance is up.