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

# Integrate an Evidence Gateway candidate with Docker Compose

> Mount an approved Evidence Gateway candidate unchanged in an operator-owned Docker Compose application with separate runtime, secrets, and audit storage.

import QuickstartMeta from '../../../components/QuickstartMeta.astro';

Start with the approved candidate created in
[Build and deploy an Evidence Gateway project](../build-and-deploy-evidence-project/). This guide runs that
candidate in an existing Docker Compose application. Compose is an operator-owned deployment
adapter, not output from `evidencectl package`. The package stays unchanged across host and
container deployments.

<QuickstartMeta
  outcome="An approved Evidence Gateway candidate mounted unchanged with a separate runtime, secret root, Transit socket, and persistent audit volume."
  time="About 20 minutes after the image and candidate are approved"
  level="Operator-owned Docker Compose deployment"
  prerequisites={['Docker Compose', 'An approved Evidence Gateway candidate', 'A reviewed Evidence Gateway image', 'A workload-local Transit proxy', 'Persistent storage prepared for UID and GID 65532']}
/>

## Before you start

You need an approved candidate, a container image whose provenance and digest you have reviewed,
an owner-only secret mount, a workload-local Transit proxy, and persistent storage for the audit
chain. Do not use repository development images as released production artifacts.

Prepare a container-specific runtime document. It names the same package but container paths,
a private Compose-network listener, secret root, audit path, and any required private CA files.

## Add the Transit proxy boundary

Production and evidence-grade candidates require a workload-local Vault or OpenBao Transit proxy.
Run one proxy for Evidence Gateway on the host or in an operator-owned sidecar. Give the proxy a
dedicated host directory in which to create `transit-proxy.sock`, then bind-mount that directory at
`/run/registry-evidence` in the Evidence Gateway container. Make the directory searchable but not
writable by the Evidence Gateway identity. The proxy owns the directory and creates a mode `0660`
socket whose group admits only that identity.

The proxy owns its provider auto-auth credential, provider trust file, and reviewed HCL
configuration. Do not mount those inputs into Evidence Gateway. The proxy configuration must force
its auto-auth token, require the `X-Vault-Request` header, disable request retries, and set socket
ownership for the Evidence Gateway process. Use
[Configure Transit signing for Evidence Gateway](../move-evidence-to-production-signing/)
and the maintained deployment-target templates for the exact boundary.

The Git-managed proxy HCL is nonsecret. Provider credentials and generated tokens remain outside
Git and outside the Evidence Gateway container. If Compose also owns the sidecar, replace the host
bind with a dedicated named volume shared only by the proxy and Evidence Gateway.

## Mount the deployment inputs

The Evidence Gateway service mounts five independently owned paths:

```text
candidate                 -> /etc/registry-evidence/package      read-only
runtime.docker.yaml       -> /etc/registry-evidence/runtime.yaml read-only
Evidence secret root      -> /run/secrets/registry-evidence      read-only
Evidence audit volume     -> /var/lib/registry-evidence          writable
Transit socket directory  -> /run/registry-evidence              socket access
```

Keep the package and runtime read-only. A read-only mount establishes their immutability, but does
not waive secret ownership or mode validation. The maintained Evidence Gateway image runs as UID and GID
`65532`; pin that identity in the Compose file and make every secret file acceptable to it with the
required owner-only permissions. A different reviewed image requires an explicitly reviewed UID
and matching ownership.
Prepare the persistent audit volume for that same identity before startup. Do not run Evidence Gateway as
root to compensate for an audit volume with the wrong owner.

```yaml
# Mounts and service identity only. This is not a complete service definition;
# the note below this block names what it leaves out.
services:
  evidence:
    image: <reviewed-evidence-image-by-digest>
    user: "65532:65532"
    read_only: true
    volumes:
      - <candidate>:/etc/registry-evidence/package:ro
      - ./runtime.docker.yaml:/etc/registry-evidence/runtime.yaml:ro
      - <evidence-secret-root>:/run/secrets/registry-evidence:ro
      - evidence-audit:/var/lib/registry-evidence
      - <transit-socket-directory>:/run/registry-evidence:ro
```

That snippet shows the mounts and the service identity only. The maintained adapter at
`docker/compose/docker-compose.yaml` in the Registry Stack repository carries the rest of the
service posture, including dropped capabilities, `no-new-privileges`, and a read-only `/dev/shm`
that replaces the writable one Docker adds by default. Start from that file rather than from the
mounts alone.

Bind Evidence Gateway to a private Compose-network address. Put TLS termination and public routing in an
operator-controlled service ahead of that listener.

## Validate in the container context

Start the Transit proxy, then run `evidence check` in the target execution context after mounts,
ownership, paths, and trust files are in place:

```sh
docker compose run --rm evidence \
  check --runtime-config /etc/registry-evidence/runtime.yaml --require-runtime-dependencies
```

It prints one line and exits zero:

```text
Evidence package <package-digest> passed check (<count> requirements)
```

The digest is your own package's, and the count is the number of requirements in that package, so
the line will not match anyone else's.

Both forms of the command compile the verified package, compile the source plans, refuse a mounted extract
already older than its source allows, validate the mounted secret material, and initialize the
configured signer, which signs a self-test message through the Transit proxy and verifies it
against the governed public JWK. That is why the proxy starts first: without a reachable socket
either form stops at `evidence: runtime signing initialization failed: the Transit provider did not
answer on the configured Unix socket (missing socket, refused connection, or timeout)`.

`--require-runtime-dependencies` adds the dependencies a served request would need. It opens the
configured audit writer exactly as `serve` would: creating the audit file and its lock if they are
not already there, and deleting any sealed file already past `audit.retainDays`. It fetches the
access-token issuer key set fail-closed and resolves every configured source credential. It appends
no audit event of its own, but run beside a running service it refuses instead, because the writer
is a single-writer destination and the running service already holds its lock.

Opening the audit path takes its single-writer lock, so a candidate that shares that path with a
running instance is refused with `another process holds the single-writer lock beside the audit
file`. To
check the candidate before cutover without stopping that instance, add `--without-audit-lock`. The
audit hash key is still checked, and the audit directory and files are still checked for
ownership, mode, write access, and a complete final entry. The lock stays with the running
instance.

Changing only the container runtime does not change the governed package, so it does not require the
fixture suite to run again. Run fixtures again when the package changes.

## Start the service

```sh
docker compose up -d
docker compose ps
```

The maintained image's default command is `serve --runtime-config
/etc/registry-evidence/runtime.yaml`, the path the runtime file is mounted at. No further
arguments are needed.

Evidence Gateway writes line-delimited JSON to stdout. It announces the listener only after every
listener is bound, so the announcement means the port is this deployment's and not something else's:

```sh
docker compose logs evidence | grep 'evidence service listening'
```

That record carries the package digest, the bind host, and the port. If no
such line appears, read the whole log: startup failures are reported there, and the container will
have exited.

The runtime in this guide binds a Compose-network address with no published ports, so `/health` is
reachable from another service on the same Compose network and not from your host. Publish a port
only behind the TLS-terminating service you control.

## Keep package and requirement digests distinct

The package digest covers the exact `SHA256SUMS` bytes and remains the same in host and Compose
deployments. Runtime paths, listener bindings, trust files, secrets, and audit contents remain
outside that package identity. A configuration revision is narrower than the package digest: it
covers one requirement's own configuration and artifacts. Signed assertions carry the revision of
the requirement they answer as `configurationRevision`, never the whole-package digest. Neither
digest contains secret values or audit contents.

## Stop without deleting the audit history

`docker compose down` removes the containers and the network it created. It leaves named volumes
alone, so the audit log survives the stop:

```sh
docker compose down
docker volume ls | grep evidence-audit
```

The volume is still listed. Starting the service again appends to the same audit file rather than
beginning a new one.

:::danger
`docker compose down -v` deletes that volume with everything else. Unless you shipped the audit
files to append-only storage first, the history is gone and cannot be reconstructed. Shipping and
retention beyond the volume remain operator responsibilities.
:::

## Next

- [Build and deploy an Evidence Gateway project](../build-and-deploy-evidence-project/)
- [Configure Transit signing for Evidence Gateway](../move-evidence-to-production-signing/)
- [Configure Evidence Gateway](../../configure/evidence/)