Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/dev/llms.txt
Full corpus: https://docs.registrystack.org/dev/llms-full.txt

# Deploy a registry

> Install the Base Registry Engine runtime, provision PostgreSQL, write the runtime configuration, test, sign, and activate the first package, and expose metrics for a registry that is ready to serve.

You have a project that passes `bregctl check --production`
([Build a production candidate](../../tutorials/build-a-breg-production-candidate/) gets you there)
and want it serving. At the end of this page a `breg` process serves a signed package against
PostgreSQL, answers `GET /ready`, and exposes its metrics to your scraper.

This page covers the deploy phase only: from an empty PostgreSQL server to the first activated
package. The sibling pages take over once the registry serves:
[Bind webhook receivers](../breg-webhooks/) for the destinations the project declares,
[Change an active registry](../breg-changes/) for successor packages,
[Retain, erase, and audit](../breg-retention/) for history and the audit journal, and
[Move data in bulk](../breg-data/) for imports and exports.

Three roles take part. The author prepares a package from the project. A signer approves the exact
package bytes with a key the deployment trusts. The operator, holding the migration credential,
activates the signed package and runs the server. One person may hold all three roles on a pilot;
the commands stay the same. Every command reads `--help`. Paths given to `--runtime-config`,
`--credentials`, `--output`, and `--package` must be absolute.

{/* Evidence: crates/registry-bregctl/src/lib.rs, TestArgs, PackageArgs, ApplyArgs, and DoctorArgs;
    products/breg/scripts/test-adopter-workflow.sh. */}

## Obtain the runtime

Install `breg` and `bregctl` from the release assets of the pinned tag, so the tool that packages
and the runtime that serves carry the same version, and so the binary a deployment tested is the
binary it runs:

```sh
curl -fsSL https://github.com/registrystack/registry-stack/releases/download/v0.26.1/breg-v0.26.1-install.sh | bash
bregctl --version
```

```text
bregctl 0.26.1
```

The installer accepts the platforms in
[platform support](../../explanation/known-limitations/#platform-support), and refuses any other
platform rather than guessing. It verifies both downloaded binaries against the release `SHA256SUMS`
before anything reaches the install directory, and installs both or neither. It does not verify
release authenticity. The signed checksum chain that does, and the checks behind it, are recorded in
[OpenSSF and release trust](../../security/openssf-evidence/). Replace `| bash` with `| less` to read
the installer before you run it on a host you operate. For a higher-assurance installation, follow
[release verification](https://github.com/registrystack/registry-stack/blob/v0.26.1/release/VERIFY.md)
for the pinned tag, then rerun the installer with `BREG_ASSET_DIR` pointing at the verified
directory. `BREG_INSTALL_DIR` selects the install directory; the default is `~/.local/bin`.

Testing, packaging, activation, and serving must use one release: `--version` on each binary
prints it, and the installer keeps the pair together, so reinstalling replaces both. The release
also publishes `breg-install.sh` as a movable alias of the pinned installer.

The container image is `ghcr.io/registrystack/breg:v0.26.1`, built on distroless nonroot for
`linux/amd64` only; see [platform support](../../explanation/known-limitations/#platform-support)
for the rest of the artifact matrix. Its
entrypoint is `/usr/local/bin/breg`, its default arguments are `--config /etc/breg/runtime.yaml`,
and it exposes port 8080. The image carries no shell, no writable directory, no healthcheck
subcommand, and no `bregctl`: the audit journal lives in PostgreSQL, the runtime configuration and
the files it names are mounted read-only under `/etc/breg`, and the orchestrator probes
`GET /health` or `GET /healthz` over HTTP for liveness while readiness stays with `GET /ready`, as
[Activate and serve](#activate-and-serve) records. Because the image carries no `bregctl`,
activation always happens from a separate host, and the container's own runtime document differs
from that host's; see [Serve the container image](#serve-the-container-image). The release
manifest published beside the binaries, `registry-stack-v0.26.1-release-manifest.json`, records
the promoted digest of each image; pin that digest instead of the movable tag.

{/* Evidence: crates/registry-breg/install.sh; release/docker/Dockerfile.breg;
    release/scripts/build-release-image.sh;
    crates/registry-breg/src/api/mod.rs; release/VERIFY.md. */}

## Provision PostgreSQL

Base Registry Engine needs PostgreSQL 15 or newer, with TLS between the server and the database. A
project with a `crs84-point` field needs PostgreSQL 16 or newer and PostGIS. Create two login
roles: a migration role that owns the schema and applies packages, and a runtime role the serving
process uses. Keep the migration credential off the serving host when no activation is in
progress.

```sql
CREATE ROLE registry_migration LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE
  NOINHERIT NOBYPASSRLS PASSWORD '<migration-password>';
CREATE ROLE registry_runtime LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE
  NOINHERIT NOBYPASSRLS PASSWORD '<runtime-password>';
```

Create two databases with the same shape: the serving database, and a schema-test database for
`test`. The five managed schemas in the schema-test database must be empty when `test` starts, and
`test` fills them without cleaning up after itself, so drop and recreate that database before every
run; a run against a database that still holds the previous run's objects refuses with
`schema-test database is not clean`. In each database, as an administrator:

```sql
CREATE EXTENSION IF NOT EXISTS btree_gist;
REVOKE ALL ON DATABASE registry FROM PUBLIC;
GRANT CONNECT ON DATABASE registry TO registry_migration, registry_runtime;
CREATE SCHEMA registry_internal AUTHORIZATION registry_migration;
CREATE SCHEMA registry_data AUTHORIZATION registry_migration;
CREATE SCHEMA registry_source AUTHORIZATION registry_migration;
CREATE SCHEMA registry_derived AUTHORIZATION registry_migration;
CREATE SCHEMA registry_context AUTHORIZATION registry_migration;
REVOKE ALL ON SCHEMA registry_internal, registry_data, registry_source,
  registry_derived, registry_context FROM PUBLIC;
```

Base Registry Engine creates every table, function, and policy inside those five schemas from the
package. It never creates a schema, an extension, or a role, so a missing prerequisite surfaces
at `test` or `apply` rather than at runtime.

For a spatial project, add a bounding-box role named after the runtime role with the suffix
`__spatial_bbox`, and install PostGIS in a dedicated schema that neither registry role owns:

```sql
CREATE ROLE registry_runtime__spatial_bbox NOLOGIN NOSUPERUSER NOCREATEDB
  NOCREATEROLE NOINHERIT NOBYPASSRLS;
GRANT registry_runtime__spatial_bbox TO registry_migration
  WITH INHERIT FALSE, SET TRUE, ADMIN FALSE;
-- in each database
CREATE SCHEMA registry_spatial_ext AUTHORIZATION postgres;
CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA registry_spatial_ext;
REVOKE CREATE ON DATABASE registry FROM PUBLIC, registry_migration, registry_runtime;
REVOKE ALL ON SCHEMA registry_spatial_ext FROM PUBLIC;
GRANT USAGE ON SCHEMA registry_spatial_ext
  TO registry_migration, registry_runtime, registry_runtime__spatial_bbox;
```

The launcher behind [Create and query your first registry](../../tutorials/first-breg/) writes
exactly these statements for its local container, so a quickstart run's generated `database/`
directory is a working reference.

{/* Evidence: products/breg/quickstart/support/quickstart.py, _initialize_sql();
    crates/registry-breg/src/postgres/schema.rs, refuse_existing_managed_objects() and
    prepare_schema_test_database_with_connections();
    crates/registry-breg/src/postgres/roles.rs. */}

## Write the runtime configuration

The runtime file binds one package to one database, one token issuer, and one listener, plus an
optional private metrics listener. It is a deployment artifact: keep it with the operator, outside
the authoring project, and never put a credential in it. Values such as `secret:file/<name>`
resolve to owner-only files under the file provider root; `secret:env/<NAME>` reads an environment
variable when `secretProviders.environment` is declared.

```yaml
apiVersion: registry.registrystack.org/breg-runtime/v1alpha1
kind: BRegRuntimeConfig
listener:
  bind: 127.0.0.1:8080
  publicOrigin: https://registry.example.org
identity:
  environment: production
  instanceId: civil-registry-1
  databaseId: civil-registry-db-1
  databaseInitializationEnvironment: production
secretProviders:
  file:
    root: /etc/breg/secrets
database:
  runtimeUrlRef: secret:file/runtime-database-url
  migrationUrlRef: secret:file/migration-database-url
  pool:
    maxSize: 8
  roles:
    migration: registry_migration
    runtime: registry_runtime
package:
  root: /var/lib/breg/packages/build-1/package
  trustAnchorPath: /etc/breg/package-trust-anchor.json
  compilerSourceRevision: civil-registry-0.1.0
  activeRevision: sha256:<package revision reported by package>
  activeSequence: 1
authentication:
  oidc:
    issuer: https://issuer.example.org
    audience: breg
    allowedAlgorithm: ES256
    accessTokenType: at+jwt
    scopeClaim: scope
    scopeSeparator: " "
    allowedClients: [registry-console]
    deniedKids: []
    maxTokenLifetimeSeconds: 300
    leewayMilliseconds: 30000
    jwksSource:
      kind: discovery
  authorityClaims:
    principal: registry_principal
    purpose: registry_purpose
audit:
  hashKeyRef: secret:file/audit-key
cursor:
  secretRef: secret:file/cursor-key
eventDestinations: {}
```

`publicOrigin` may include a deployment path prefix, such as
`https://registry.example.org/registry-a`. Discovery, paging, and schema links preserve the
configured prefix and never derive their authority from request headers.

| Section | What it binds |
| --- | --- |
| `listener` | The bind address and the optional public origin that generated documents cite. The runtime reads neither peer addresses nor forwarded headers, so client addresses and TLS termination are enforced at your proxy. |
| `identity` | The environment, instance, and database this file may serve. `environment` and `instanceId` must equal the `package` block in `registry.yaml`; `databaseId` must equal the `--database-id` given to `test` and `package`. |
| `secretProviders` | The file root (owner-only files) and, when declared, the environment provider. |
| `database` | Secret references for the runtime and migration connection URLs, pool bounds, and the two role names the package's policies are written for. |
| `package` | The activated package directory, the trust anchor, the source revision the project declares as `package.sourceRevision`, and the active revision and sequence. |
| `authentication` | The OpenID Connect verifier and the names of the claims that carry the caller's principal and purpose. |
| `audit` and `cursor` | The key that chains the audit journal and the secret that signs pagination cursors. Loss of either key invalidates existing chains or cursors. |
| `eventDestinations` and `eventDelivery` | One binding per webhook destination the package declares, and payload retention; see [Bind webhook receivers](../breg-webhooks/). |
| `operationalTimeouts` | Optional HTTP request, record lock, migration lock, migration statement, and shutdown grace bounds. |
| `metricsListener` | Optional second binding, private to the operator, that serves `GET /metrics`; see [Scrape metrics](#scrape-metrics). |

`leewayMilliseconds` must be a whole number of seconds and at most 300000. Static keys load at
startup; rotate them through a configuration change and restart:

```yaml
jwksSource:
  kind: static
  documentRef: secret:file/<jwks-document>
```

With `kind: discovery`, the runtime reads the issuer's discovery document and caches its JWKS
under the optional `jwksCache` bounds.

{/* Evidence: crates/registry-breg/src/runtime_config.rs, RuntimeConfig;
    crates/registry-breg/tests/runtime_config.rs;
    crates/registry-platform-config/src/secrets.rs, SecretReference;
    products/breg/generated/runtime/runtime.schema.json. */}

### Create the secret files

Create the files the example references as the user that runs `breg` and the operator commands,
because the resolver refuses a file owned by anyone else. Each must be a regular file with mode
`0400` or `0600` and no other link or symbolic link to it, and its name starts with a lowercase
letter and uses lowercase letters, digits, `.`, `_`, and `-`. The bytes are used exactly as
written, neither trimmed nor decoded, so write them without a trailing newline:

```sh
install -d -m 0700 /etc/breg/secrets
(umask 077; openssl rand -hex 32 | tr -d '\n' > /etc/breg/secrets/audit-key)
(umask 077; openssl rand -hex 32 | tr -d '\n' > /etc/breg/secrets/cursor-key)
(umask 077; printf '%s' 'postgresql://registry_runtime:<runtime-password>@db.example.org:5432/registry' \
  > /etc/breg/secrets/runtime-database-url)
(umask 077; printf '%s' 'postgresql://registry_migration:<migration-password>@db.example.org:5432/registry' \
  > /etc/breg/secrets/migration-database-url)
```

The audit key and the cursor secret each need at least 32 bytes; 64 hexadecimal characters
satisfy both. Percent-encode a password that carries reserved characters. The user in each URL
must equal the role named under `database.roles`, and the runtime requires TLS on both connections
itself, so the URL needs no `sslmode` parameter and a server without TLS refuses the connection.

{/* Evidence: crates/registry-platform-config/src/secrets.rs, read_secret_file(),
    validate_file_metadata(), and valid_file_name();
    crates/registry-platform-audit/src/lib.rs, MIN_AUDIT_SECRET_BYTES;
    crates/registry-breg/src/cursor.rs, ROOT_SECRET_MIN_BYTES;
    crates/registry-breg/src/runtime_config.rs, database_connection_config_for();
    crates/registry-breg/src/postgres/config.rs, require_tls_config(). */}

## What a token must carry

Base Registry Engine verifies a bearer access token before it reads any claim. Configure the issuer
so that every token for this deployment satisfies the table. Fixture claims from `journeys.yaml`
are never accepted by a running server.

| Token part | Requirement |
| --- | --- |
| `alg`, `typ`, `kid` headers | `alg` equals `allowedAlgorithm`; `typ` equals `accessTokenType`; `kid` is present in the JWKS and absent from `deniedKids`. |
| `iss` | Equals `issuer`. |
| `aud` | A string equal to `audience`, or an array of 1 to 16 distinct nonempty strings containing that exact resource audience. |
| `exp`, `iat`, `nbf` | The lifetime is at most `maxTokenLifetimeSeconds`; clock skew up to `leewayMilliseconds` is tolerated. |
| `azp` or `client_id` | Listed in `allowedClients` when that list is not empty. |
| `scope` | The single claim named by `scopeClaim` covers every required permission. It is a string split on `scopeSeparator`, or a JSON array of permission strings. Other shapes are refused; permissions from several claims are not merged. |
| principal claim | The claim named by `authorityClaims.principal` carries the identity recorded in audit and workflow decisions. It must match each authenticated profile's `principalClaim`. |
| purpose claim | The claim named by `authorityClaims.purpose` is required when the profile lists `requiredPurposes`. |
| assignment claims | Each row boundary or claim-backed lookup names its own claim. An `equals` boundary needs a scalar; an `in` boundary needs a JSON array. An identity claim does not imply district or team assignments. |

Set `maxTokenLifetimeSeconds` to your deployment's chosen limit, up to 7200 seconds.
The limit bounds the accepted difference between issuance and expiry; increasing it extends how long an already issued token may carry old permissions.
Match the issuer's token settings and test both accepted and refused lifetimes.
Use a resource audience dedicated to this registry API, distinct from your application's login client identifier.
Obtain access tokens for that resource; an ID token issued to the login client is not an API credential.

### Choose identities that survive operations

Select the principal claim explicitly. `sub` is supported when selected in both the runtime and profiles;
the server never falls back to another identity claim if the selected claim is missing.
An issuer's `sub` usually belongs to that issuer's identity namespace.
For an issuer migration, prefer an institutional identifier carried in a custom claim by both issuers,
and document who guarantees that its values remain unique, stable, and never reassigned.
Base Registry Engine treats the selected value as an opaque identity and does not link accounts across issuers.
Changing it can change whether someone is recognized as a submitter or a previous reviewer.

Use the same principal claim in an ownership row boundary when the record's owner field stores that identifier.
Use separate assignment claims for district, tenant, or team membership.
Configure the issuer to derive these claims from trusted assignments rather than caller-supplied values.

Deployments without an identity provider can run [Registry Mint](../../configure/mint/) as the issuer for registered machine clients.
Mint's client-credentials flow represents a service, not an individual signing in.
Human login, account lifecycle, and multifactor authentication belong to your identity provider and application.
When replacing an issuer, verify the same permitted and refused requests with real tokens before changing the serving configuration.
Compare issuer, resource audience, token headers, permission source, principal identity, purpose, and assignment claim shapes.

{/* Evidence: crates/registry-breg/src/auth.rs;
    crates/registry-breg/tests/http_auth.rs;
    crates/registry-breg/src/runtime_config.rs;
    crates/registry-mint/src/lib.rs;
    crates/registry-platform-oidc/src/lib.rs. */}

### Rotate signing keys and handle issuer outages

With discovery, `jwksCache.cacheTtlSeconds` defaults to 600 seconds.
An unfamiliar `kid` can trigger a refresh, subject to `refreshCooldownSeconds` (30 seconds by default) and the bounded negative cache.
Publish a new public key before issuing tokens with it, then verify a token using that key against the registry.
Keep the previous public key available while its legitimate tokens remain valid.
Removing a key from the issuer does not immediately remove an already cached key from the registry.

During a fetch failure, a known cached key may remain usable until its age reaches the cache TTL plus `outageToleranceSeconds`, which defaults to 900 seconds.
The default total allowance is therefore 1500 seconds from the last successful key-set fetch, not from the start of the outage.
Unknown keys cannot use this allowance, and the server cannot obtain keys for a cold start from an unavailable issuer.
After the allowance, verification requiring those stale keys fails until fetching succeeds.
Token expiry and every other verification rule continue to apply during the allowance.

Short token lifetimes bound how long issued permission and assignment claims remain usable.
Disabling an account or changing a group at the issuer does not update claims in an already issued token.
For a compromised signing key, add its identifier to `deniedKids` and restart every serving instance with that configuration.
That rejects all tokens signed with the denied key, including otherwise legitimate tokens.
Static JWKS documents also require a configuration change and restart to rotate.
[Rotate credentials, keys, certificates, and trust](../advanced/rotate-credentials-and-trust/) covers the same rotation discipline for Relay, Evidence Gateway, and Mint.

### Change the issuer without losing request context

The runtime trusts one configured issuer and reads its configuration at startup.
Plan a coordinated restart and client token change; one process does not provide an overlap period accepting both issuers.
Keep the selected principal values stable if existing ownership and review decisions should continue to recognize the same actors.
Test new tokens before the serving cutover, including refused permissions and assignment shapes.

Resolve uncertain writes before changing issuer mappings, profiles, or packages.
After a timeout, retry the same request with its original idempotency key and the same effective principal, profile, purpose, and row context.
A replacement token can satisfy those conditions; changing identity or authority context can produce `idempotency.conflict` instead of a replay.
The binding also covers the request and package revision, so issuing a fresh key blindly can duplicate a write whose first response was lost.
[Mutation retry rules](../../reference/breg-api/#idempotency) explain the request contract.

Pagination cursors bind the principal, profile, purpose, row scope, query, projection, and compiled registry identity.
They also expire, with `cursor.maxAgeSeconds` defaulting to 300 seconds.
A token renewal alone does not require starting over when the effective context is unchanged, but changed mappings, policy, package, or cursor secret can invalidate an existing cursor.
Restart the query from its first page when that happens; do not treat a cursor as portable authority between deployments.

{/* Evidence: crates/registry-platform-oidc/src/lib.rs, JwksFetcher and tolerated_age();
    crates/registry-breg/src/runtime_config.rs, JwksCacheConfig and DEFAULT_CURSOR_MAX_AGE_SECONDS;
    crates/registry-breg/src/startup.rs, prepare();
    crates/registry-breg/src/auth.rs;
    crates/registry-breg/src/idempotency.rs, canonical_claim_context();
    crates/registry-breg/src/cursor.rs, CursorBinding;
    crates/registry-breg/src/api/mod.rs, cursor_binding(). */}

## Test the candidate

`test` compiles the project, applies it to the empty schema-test database, measures the resulting
schema, runs every journey in `tests/journeys.yaml` over real HTTP with real tokens, and writes a
receipt that `package` later binds to. Prepare three inputs:

1. A test runtime file: the same document as the serving one, with `database` pointing at the
   schema-test database, `package.root` an empty directory, and `package.activeRevision` a
   placeholder of `sha256:` followed by 64 hexadecimal digits.
2. Real access tokens from your issuer for each journey principal, stored as owner-only files under
   the secret root.
3. A credentials document binding every journey step to a token or to anonymous access:

```yaml
apiVersion: registry.registrystack.org/breg-schema-test-credentials/v1
kind: SchemaTestCredentials
bindings:
  - journeyId: record-lifecycle
    stepId: create-record
    credential:
      type: bearer
      tokenRef: secret:file/schema-test-token
  - journeyId: record-lifecycle
    stepId: get-record
    credential:
      type: bearer
      tokenRef: secret:file/schema-test-token
  - journeyId: record-lifecycle
    stepId: list-records
    credential:
      type: anonymous
```

Then run the test with the signature policy the package will carry:

```sh
bregctl --format json test ./my-registry \
  --runtime-config /srv/registry/runtime-test.yaml \
  --credentials /srv/registry/schema-test-credentials.yaml \
  --database-id civil-registry-db-1 \
  --signature-threshold 1 --signature-key-id registry-signer-2026 \
  --output /srv/registry/schema-test-receipt.json
```

The report names the candidate `packageRevision`, the measured `schemaFingerprint`, and the
journeys that passed. Keep the receipt and the fingerprint with the candidate: `package` refuses a
receipt taken for different sources, a different baseline, or a different signature policy.
A journey failure reports the journey and step without record values.

{/* Evidence: crates/registry-bregctl/src/test_lifecycle.rs;
    crates/registry-bregctl/src/lib.rs, TestArgs;
    crates/registry-breg/src/fixtures.rs. */}

## Package and sign

`package` writes the deployable package directory and stops at the signing boundary:

```sh
bregctl --format json package ./my-registry \
  --database-id civil-registry-db-1 \
  --test-receipt /srv/registry/schema-test-receipt.json \
  --signature-threshold 1 --signature-key-id registry-signer-2026 \
  --output /srv/registry/build-1
```

The schema fingerprint comes from the test receipt; `--schema-fingerprint` states it explicitly
when you want the command to fail on any other value. The report's `state` is
`awaiting_signatures` and `build-1/signing-input.json` holds the exact bytes to sign. The command
never accepts a private key. Your signer produces a detached Ed25519 signature over those bytes,
for example with OpenSSL, and returns a signature document:

```sh
openssl pkeyutl -sign -rawin -inkey registry-signer-2026.pem \
  -in /srv/registry/build-1/signing-input.json -out signature.bin
```

```json
{"signatures": [{"keyId": "registry-signer-2026", "signatureHex": "<hex-encoded signature.bin>"}]}
```

Rerun the same `package` command with `--signatures <document>` and the unchanged inputs and
output directory. The report's `state` becomes published and `packageRevision` names the package.
The package directory is `build-1/package`.

The trust anchor the runtime file names lists the public keys and the threshold the deployment
accepts, bound to the same identity:

```json
{
  "apiVersion": "registry.registrystack.org/package-trust/v1",
  "environment": "production",
  "instanceId": "civil-registry-1",
  "databaseId": "civil-registry-db-1",
  "threshold": 1,
  "keys": [
    {
      "keyId": "registry-signer-2026",
      "jwk": {"kty": "OKP", "crv": "Ed25519", "alg": "EdDSA",
              "kid": "registry-signer-2026", "x": "<base64url public key>"}
    }
  ]
}
```

`--signature-threshold 0` builds an unsigned package. Production activation refuses it; the
quickstart uses it for local development only.

{/* Evidence: crates/registry-bregctl/src/package_lifecycle.rs;
    crates/registry-bregctl/src/lib.rs, PackageArgs;
    crates/registry-breg/src/package.rs;
    products/breg/scripts/test-adopter-workflow.sh. */}

## Activate and serve

Write the serving runtime file with `package.root` pointing at `build-1/package`,
`activeRevision` equal to the reported `packageRevision`, and `activeSequence: 1`. Then activate
with the migration credential:

```sh
bregctl --format json apply \
  --runtime-config /etc/breg/runtime.yaml \
  --package /srv/registry/build-1/package --initial
```

`--initial` activates sequence one in an uninitialized database and requires the runtime file to
already name that package. The report carries the activated revision, sequence, and schema
fingerprint. Confirm the configuration before starting the process:

```sh
bregctl verify --runtime-config /etc/breg/runtime.yaml
bregctl doctor --runtime-config /etc/breg/runtime.yaml
breg --config /etc/breg/runtime.yaml
```

`verify` opens no runtime dependency. It proves that the configured package verifies against the
trust anchor and identity, and reports the registry id, version, and revision plus inventory
counts. It does not say whether the database is on that package; the `apply` report and readiness
do. `doctor` opens every startup dependency without binding the listener: the runtime file, the
package, the database connection and its readiness, the audit key, the cursor key, the OIDC
verifier's key material, the event destination bindings, and the authentication profile, the claim
mapping, the accepted algorithms, and the audience against the package this runtime serves. It
names the first dependency that refuses and stops there; a run that reaches the end lists every
dependency it checked:

```text
doctor succeeded
checked runtimeConfig: pass
checked package: pass
checked database: pass
checked audit: pass
checked cursor: pass
checked authentication.oidc: pass
checked eventDestinations: pass
checked authentication: pass
```

```json
{
  "ok": true,
  "command": "doctor",
  "checked": [
    "runtimeConfig",
    "package",
    "database",
    "audit",
    "cursor",
    "authentication.oidc",
    "eventDestinations",
    "authentication"
  ]
}
```

Start the process and check
`GET /ready` before admitting traffic; `GET /health` reports liveness only.

Retain the project sources, module locks, generated files, test receipt, signature documents, and
runtime file for each activation. Store secrets separately.

{/* Evidence: crates/registry-bregctl/src/apply_lifecycle.rs;
    crates/registry-bregctl/src/doctor.rs, startup_diagnostic();
    crates/registry-bregctl/src/lib.rs, VerifyArgs, verify(), write_doctor_success(), and
    doctor_success_output_is_stable_in_human_and_machine_formats();
    crates/registry-breg/src/api/mod.rs;
    crates/registry-breg/src/startup.rs. */}

## Serve the container image

The release image carries the `breg` binary alone, so the runtime document you activated with is
not the one the container serves: activation runs on the operator host that holds the migration
credential and `bregctl`, and the container only serves the package that host already activated.
Write a second runtime document for the container, changing exactly five things from the host's:
`listener.bind`, `secretProviders.file.root`, `package.root`, `package.trustAnchorPath`, and the
database hostname the resolved connection URL names, because the container mounts its own paths
and reaches the database over its own network.

```yaml
listener:
  bind: 0.0.0.0:8080
secretProviders:
  file:
    root: /etc/breg/secrets
package:
  root: /var/lib/breg/package
  trustAnchorPath: /etc/breg/package-trust-anchor.json
```

Nothing restricts `listener.bind` to a loopback or private address the way `metricsListener.bind`
is restricted, so the container can bind `0.0.0.0` and accept traffic from outside its network
namespace; a host process that only ever serves `127.0.0.1` has no reason to. Mount the four
inputs the image needs, all read-only, and publish the listener port:

```sh
docker run --rm \
  -p 8080:8080 \
  -v /srv/registry/container/runtime.yaml:/etc/breg/runtime.yaml:ro \
  -v /var/lib/breg/build-1/package:/var/lib/breg/package:ro \
  -v /etc/breg/package-trust-anchor.json:/etc/breg/package-trust-anchor.json:ro \
  -v /etc/breg/container-secrets:/etc/breg/secrets:ro \
  ghcr.io/registrystack/breg:v0.26.1
```

Run `bregctl verify` and `bregctl doctor` against this exact runtime document from a host that
reaches the same database and the same mounted package before you rely on the container's
`GET /ready`.

{/* Evidence: crates/registry-breg/src/runtime_config.rs, ListenerConfig and MetricsListenerConfig;
    release/docker/Dockerfile.breg. */}

## Scrape metrics

The listener that serves the registry API never serves metrics. To expose them, add a second
listener that only the operator's network can reach:

```yaml
metricsListener:
  bind: 127.0.0.1:9464
```

The address must be a loopback or private address (an IPv4 private range, or IPv6 unique-local)
with a non-zero port, and it must differ from `listener.bind`; the runtime file is otherwise
refused with `runtime_config.invalid_metrics_listener`. The listener answers `GET /metrics` in the
Prometheus text format and nothing else, and it carries no authentication, so keep it behind the
scraper's network boundary rather than a public one.

| Series | Type | Labels |
| --- | --- | --- |
| `breg_http_requests_total` | counter | `route`, `method`, `status` |
| `breg_http_request_duration_seconds` | histogram | `route`, `method`, `status` |
| `breg_anonymous_refusals_total` | counter | `route`, `method`, `reason` |
| `breg_pool_connections` | gauge | `state`: `max_size`, `size`, `available`, `waiting` |

`route` is the matched route template, such as `/v1/records/placements/{record_id}`, or
`unmatched`; it never carries a record identifier. `status` is `success`, `client_error`, or
`server_error` rather than the exact code. The label sets are closed, so a scrape cannot grow
without bound, and no label carries a principal, a token, or a record value. Pool states are read
from the connection pool at scrape time.

`breg_anonymous_refusals_total` counts requests that presented no credential and were refused
before admission: a profile the caller cannot hold, a route it cannot see, an absent scope or
purpose, or a query the route cannot parse. `reason` is one of nine fixed values,
`read_request_invalid`, `read_concealed`, `read_refused`, `revision_request_invalid`,
`revision_concealed`, `revision_refused`, `mutation_concealed`, `mutation_refused`, and
`action_refused`, derived from the refusal itself rather than from anything the request carried.
These refusals are counted rather than journaled, because a caller with no principal names nobody
the journal could hold accountable; refusals of an authenticated principal are journaled, and so
is every admitted request. Alert on a sharp rise in this counter the way you would on a rise in
`client_error` responses: it is the signal the journal no longer carries.

An admitted anonymous read writes the same pre-I/O audit envelope as any admitted request, and the
runtime enforces no request-rate limit of its own, so put a registry that admits anonymous reads
behind an upstream rate limit.

{/* Evidence: crates/registry-breg/src/metrics.rs, AnonymousRefusalReason;
    crates/registry-breg/src/api/mod.rs, anonymous_refusal();
    crates/registry-breg/src/postgres/read.rs, record_pre_io_audit() call in execute();
    crates/registry-breg/tests/postgres_anonymous_refusals.rs;
    crates/registry-breg/src/runtime_config.rs, MetricsListenerConfig;
    crates/registry-breg/src/startup.rs. */}

## Troubleshooting

| Symptom | Next move |
| --- | --- |
| `test` refuses the runtime file | Compare it with the serving file: same identity, schema-test database references, an empty package root, and a placeholder revision. |
| A secret reference is refused | The file under the secret root must be a regular file owned by the running user, with mode `0400` or `0600`, one link, and no symbolic link, and its name must follow the naming rule in [Create the secret files](#create-the-secret-files). |
| `package` refuses the receipt | The receipt binds sources, baseline, fingerprint, and signature policy. Rerun `test` for the exact candidate. |
| `apply --initial` reports a binding error | The runtime file must already name the target package's revision and sequence one. |
| `doctor` reports `startup.oidc.refused` | Check the issuer, discovery reachability or the static JWKS document, the algorithm, and the leeway bound. |
| The runtime file is refused with `runtime_config.invalid_metrics_listener` | `metricsListener.bind` must be a loopback or private address with a non-zero port, distinct from `listener.bind`. |
| Every token is refused | Compare the token's header and claims with the token table; a list-valued `aud` or a missing principal claim refuses the token. |
| An operator command cannot reach PostgreSQL | Check the migration URL secret, the role names, and `SSL_CERT_FILE` for a private authority. |

## Next

- [Bind webhook receivers](../breg-webhooks/) when the project declares events, because the
  package refuses to activate until every destination is bound.
- [Change an active registry](../breg-changes/) for the successor package, its diff, and its
  migration evidence.
- [Retain, erase, and audit](../breg-retention/) for the maintenance commands that erase history
  and prove the audit journal.
- [Harden a production deployment](../../security/hardening-checklist/) for the controls around
  the listener, the secret root, and the migration credential.
- [Evidence deployment targets](https://github.com/registrystack/registry-stack/tree/v0.26.1/products/evidence/reference/deployment-targets)
  and the
  [Evidence Compose adapter](https://github.com/registrystack/registry-stack/blob/v0.26.1/docker/compose/docker-compose.yaml)
  cover Evidence and Registry Mint deployment shapes, not Base Registry Engine; read them when this
  deployment also runs Evidence or Mint alongside the registry.