Preview release.These docs are a work in progress. Pages are still being written, links may break, and structure may shift without notice. Treat everything here as a draft and report issues onGitHub.
Run single-node Compose behind your reverse proxy
Use this guide when a government IT team runs a single-node Docker Compose deployment on a virtual machine behind an institution-owned reverse proxy and identity and access management (IAM) layer. The reverse proxy owns public Transport Layer Security (TLS), hostnames, IAM integration, front rate limiting, and edge logs. Registry Relay and Registry Notary still own service authentication, scopes, purpose checks, minimized source reads, posture, and audit inside the registry boundary.
X-Road and Registry Stack solve complementary parts of a secure data-exchange architecture. For 1.0, this guide stays generic: it shows where an exchange layer or equivalent institutional edge sits, but product-specific X-Road onboarding and message profiles are deferred to later guides.
When to use this
Section titled “When to use this”Use this topology when:
- the deployment runs on one virtual machine with Docker Compose;
- an existing reverse proxy or gateway already owns public TLS certificates and IAM;
- the proxy can reach the VM on a loopback or private address;
- a front rate limiter terminates before traffic reaches Relay or Notary.
Do not use this guide for Kubernetes, active-active high availability, service mesh, or native X-Road adapter setup. Those topologies are outside the 1.0 deployment profile.
Boundary map
Section titled “Boundary map”flowchart LR Consumer["Consumer organization"] --> Exchange["Exchange layer or institutional edge<br/>member onboarding, routing, inter-organization trail"] Exchange --> Edge["Reverse proxy, IAM, and front rate limiter<br/>TLS, public hostnames, request admission"] Edge --> Relay["Registry Relay<br/>auth, scopes, purpose checks, projection, audit"] Edge --> Notary["Registry Notary<br/>auth, evidence decisions, issuance, audit"] Notary --> Relay Relay --> Source["Source registry<br/>source of record"] Notary --> Source
The front rate limiter terminates at the reverse-proxy edge, before requests enter the Registry Relay or Registry Notary containers. The exchange or institutional edge can authenticate the organization and protect the inter-organization channel, while Relay and Notary still authenticate the API caller they see. Do not turn a proxy login page or a forwarded identity header into product authorization unless Relay or Notary is configured to verify the token itself.
Before you start
Section titled “Before you start”- Install Docker Compose and
registryctlon the VM. - Choose the public hostnames, such as
registry.example.govandnotary.example.gov. - Choose whether Relay and Notary use generated API keys or OpenID Connect (OIDC) bearer tokens.
- Confirm the reverse proxy can forward
Authorization,x-api-key,Data-Purpose,traceparent, and one normalized request-id header. - Configure or request a front rate limiter at the same edge that terminates TLS.
- Follow Back up and restore a deployment before moving an existing generated project.
Generate and smoke-test locally
Section titled “Generate and smoke-test locally”Generate the project and add Notary before exposing it through the proxy:
registryctl init relay my-registry --sample benefitscd my-registryregistryctl add notary --from local-relayregistryctl startregistryctl smokeregistryctl notary smokeregistryctl init and registryctl add notary print the generated Bruno collection path.
The smoke commands print PASS lines for health, readiness, denied anonymous calls, and the
configured sample reads or claim checks.
Keep this local smoke run before hardening the public deployment; later steps gate OpenAPI
and change the public base URLs.
Bind Compose ports to the proxy side only
Section titled “Bind Compose ports to the proxy side only”Edit compose.yaml so host ports are not exposed on every interface.
When the reverse proxy runs on the same VM, bind Relay and Notary to loopback:
services: registry-relay: ports: - "127.0.0.1:4242:8080" registry-notary: ports: - "127.0.0.1:4255:8080"When the reverse proxy runs on another host in a private subnet, bind the VM’s private address
instead of 127.0.0.1 and block direct public access to those ports at the firewall.
Also edit registryctl.yaml so local probes use the private address from the host where you run
registryctl restart, registryctl smoke, and registryctl notary smoke:
runtime: relay_base_url: http://10.0.0.5:4242 notary_base_url: http://10.0.0.5:4255Keep the generated loopback URLs when the proxy and registryctl run on the same VM.
Do not publish product admin, metrics, posture, or reload routes through the public edge.
If Relay uses server.admin_bind, keep that listener on loopback or a private operations
network.
Validate the edited Compose file:
docker compose -f compose.yaml configExpected output is the normalized Compose configuration.
Set public URLs and gate discovery
Section titled “Set public URLs and gate discovery”Edit relay/config.yaml so public metadata points at the reverse proxy hostname and runtime
OpenAPI is gated before the service is public:
server: bind: 0.0.0.0:8080 openapi_requires_auth: true trust_proxy: enabled: true trusted_proxies: - 127.0.0.1/32
catalog: base_url: https://registry.example.govReplace 127.0.0.1/32 with the proxy address or CIDR range that connects to Relay.
Relay uses server.trust_proxy to resolve the client address for audit and local auth-failure
throttling; an empty trusted_proxies list means X-Forwarded-For is ignored.
If the project includes Notary, edit notary/config.yaml:
server: bind: 0.0.0.0:8080 openapi_requires_auth: true
evidence: api_base_url: https://notary.example.gov source_connections: relay: base_url: http://registry-relay:8080Keep Notary’s source connection to Relay on the internal Compose network unless your deployment intentionally calls Relay through the proxy. Set every public issuer, status, and OpenID for Verifiable Credential Issuance URL to the public Notary HTTPS origin before issuing credentials.
Keep IAM and product auth separate
Section titled “Keep IAM and product auth separate”The proxy can enforce institutional IAM before forwarding a request, but Relay and Notary still need their own API authentication.
- In generated API-key mode, keep
secrets/local.envout of Git and pass the caller credential through unchanged: Relay expectsAuthorization: Bearer ..., and Notary expectsx-api-key. - In OIDC mode, configure Relay or Notary with the identity provider issuer, audience, JSON Web Key Set discovery, allowed algorithms, and scope mapping. A proxy-authenticated session is not enough unless the product validates the bearer token.
- Do not authorize product routes from
X-User,X-Org, or similar forwarded headers. Use those headers only for edge logs or upstream systems that explicitly trust that proxy.
Rotate generated demo credentials before production use. For API keys, store only fingerprints in product config and distribute raw keys through the institution’s secret process.
Configure the edge
Section titled “Configure the edge”Configure the reverse proxy or gateway with these responsibilities:
| Edge responsibility | Placement |
|---|---|
| Public TLS certificate and HTTPS redirect | Reverse proxy listener on registry.example.gov and notary.example.gov |
| IAM login or organization membership check | Before proxying to the VM |
| Front rate limiting | Same edge, before forwarding to Relay or Notary |
| Header normalization | Strip caller-supplied identity headers, then set only reviewed headers |
| Request body and header timeouts | Reverse proxy listener before product containers |
| Correlation trail | Edge access logs plus product audit records |
The rate limiter is required for this 1.0 topology.
After it is configured, Relay deployments that declare a production or evidence-grade profile can
set deployment.evidence.ingress_rate_limit: true because the gateway now enforces the control.
Do not set that flag until the gateway rule is active.
Preserve correlation and audit references
Section titled “Preserve correlation and audit references”Pick one edge request id and keep it stable across the exchange trail, proxy logs, and product
calls.
If the exchange layer or workflow engine already emits W3C traceparent, forward it only when
your logging policy allows that trace context to cross the boundary.
For request-id headers, prefer a value generated or normalized by the proxy over a raw caller
header.
Preserve these references where allowed:
- exchange message id or member id in the exchange evidence trail;
- proxy request id and trace id in edge logs;
- product audit record ids or audit hashes from Relay and Notary;
- source-system transaction id when the source registry returns one.
Do not log bearer tokens, API keys, full query strings containing personal identifiers, raw subject records, or credential bodies in the proxy. Metrics are for operations, not request-level accountability; use audit records for security review and incident reconstruction.
Restart and verify through the proxy
Section titled “Restart and verify through the proxy”Restart after config edits:
registryctl restartregistryctl doctor --profile production --format jsondoctor prints a JSON report.
Resolve any startup_fail finding before routing public traffic.
Use the deployment profile declared for the environment if it is stricter than production.
Check Relay through the proxy:
set -a. secrets/local.envset +a
curl -fsS https://registry.example.gov/healthzcurl -fsS https://registry.example.gov/readycurl -fsS -G \ -H "Authorization: Bearer $ROW_READER_RAW" \ -H "Data-Purpose: https://example.local/purpose/tutorial" \ --data-urlencode "household_id=hh-1001" \ https://registry.example.gov/v1/datasets/benefits_casework/entities/person/recordsThe health and readiness calls return HTTP 200. The protected read returns a JSON response with records from the sample dataset.
Check Notary through the proxy when the project includes it:
curl -fsS https://notary.example.gov/healthzcurl -fsS https://notary.example.gov/readycurl -fsS \ -H "x-api-key: $REGISTRY_NOTARY_TUTORIAL_EVALUATOR_RAW" \ -H "Content-Type: application/json" \ -H "Accept: application/vnd.registry-notary.claim-result+json" \ -H "Data-Purpose: https://example.local/purpose/tutorial" \ -d '{ "target": {"type": "person", "id": "per-2001"}, "claims": ["benefits-person-exists"], "disclosure": "predicate", "purpose": "https://example.local/purpose/tutorial" }' \ https://notary.example.gov/v1/evaluationsThe evaluation returns a claim-result JSON response for benefits-person-exists.
Finally, verify operational controls:
- unauthenticated protected routes return
401; GET /openapi.jsonrequires authentication unless this is a controlled tooling environment;/admin/*,/metrics, and posture routes are not reachable from the public edge;- the front rate limiter returns
429when its threshold is exceeded; - new Relay and Notary audit records can be joined to the edge request id without exposing secrets.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
| All clients appear as the proxy IP in Relay audit records | server.trust_proxy is disabled or trusted_proxies is empty | Add the proxy address or CIDR range to server.trust_proxy.trusted_proxies |
registryctl smoke fails after hardening | The generated local smoke assumes unauthenticated OpenAPI and local URLs | Use the explicit proxy verification commands on this page after setting public URLs |
| Authenticated calls work locally but fail through the proxy | The proxy stripped Authorization, x-api-key, or Data-Purpose | Allow only the required product auth and purpose headers through the proxy |
doctor reports missing ingress rate-limit evidence | Relay cannot observe the gateway control | Configure the front rate limiter, then set deployment.evidence.ingress_rate_limit: true |
| Notary cannot call Relay | evidence.source_connections.relay.base_url points at the public hostname but the container cannot reach it | Keep the source connection on http://registry-relay:8080 unless the proxy path is intentional and reachable |