Skip to main content
Coral publishes two container images to GitHub Container Registry: Every release pushes an immutable version tag (for example 0.13.0), and the latest and major.minor tags of both images move together, so matching tags are always compatible. Pin exact versions in production. This guide covers two topologies: a loopback-only stack for trying the server deployment on one machine, and a production stack behind a TLS-terminating reverse proxy with authentication enabled.
Coral terminates no TLS and, without [auth], does not authenticate clients. Never expose any Coral or Coral UI port beyond loopback without a TLS-terminating proxy in front and authentication enabled.

How the pieces fit

The Coral server owns all state and serves up to three listeners, all cleartext, all configured in config.toml (see Configuration):
  • Native gRPC ([server].bind_addr, port 14555 in the image’s default config) — the data plane Coral UI talks to.
  • MCP Streamable HTTP ([server.mcp_http], opt-in) — serves /mcp for MCP clients, plus health-check endpoints.
  • OAuth authorization server ([auth].http_bind_addr, only with [auth]) — serves login, token, and discovery endpoints.
The first boot of the Coral container seeds config.toml from the CORAL_SEED_CONFIG environment variable if set, otherwise with a starter config that binds gRPC to 0.0.0.0:14555. The seed applies once: an existing config.toml on the volume is never rewritten, so later changes to CORAL_SEED_CONFIG have no effect (see Changing the configuration).

Local deployment

The local stack runs without authentication: the Coral server has no [auth] configured and Coral UI runs with CORAL_UI_AUTH_MODE=disabled, talking gRPC to Coral over the Compose network. Everything is published only on loopback.
CORAL_UI_AUTH_MODE=disabled serves Coral UI with no login: anyone who can reach the port can read every source and submit new source credentials. Keep the published port bound to 127.0.0.1 and do not port-forward it beyond your machine.
You need Docker Engine with the Compose plugin. Create compose.yaml:
Start the stack:
Coral UI is at http://localhost:3000. Verify readiness with:
MCP Streamable HTTP is published on loopback too — the seed config opts the unauthenticated listener into a non-loopback bind with allow_unauthenticated_non_loopback (see Configuration). Point any MCP client that supports the transport at:
The listener performs no authentication — reachability is the entire access control — which is why the compose file publishes it to 127.0.0.1 only, and why Coral prints an exposure warning at startup. It accepts requests addressed to localhost, 127.0.0.1, ::1, or the bind IP; to reach it from a sibling container by service name instead, add the name to [server.mcp_http].allowed_hosts.

Remote deployment with TLS

The production stack adds authentication and a TLS-terminating reverse proxy (Caddy here, which provisions Let’s Encrypt certificates automatically). TLS termination in front of every listener is mandatory for remote deployments: Coral serves only cleartext, and with [auth] enabled bearer tokens would otherwise cross the wire unprotected. The example uses one domain per surface:

Prerequisites

  • A host with Docker, ports 80 and 443 reachable from the internet, and DNS records for the three hostnames pointing at it.
  • An OIDC client registered with your identity provider (Google, Okta, Microsoft Entra, Keycloak, and others all work). Set its redirect URI to https://auth.coral.example.com/auth/oidc/callback and note the client ID and secret. Who can sign in to Coral is decided entirely by this provider — see Authentication.
During login, the Coral server itself fetches https://coral.example.com/.well-known/oauth-client and Coral UI fetches the issuer’s discovery and token endpoints. Both coral.example.com and auth.coral.example.com must therefore resolve to a public address from inside the containers, and certificates must chain to system roots (Let’s Encrypt is fine, a private CA is not). Split-horizon DNS that answers with private addresses breaks login: Coral rejects client-metadata hosts that resolve to private or loopback addresses.

Secrets

Generate the two secrets and put them in a .env file next to the compose file, together with your IdP client secret (replace your-idp-client-secret):
CORAL_SESSION_SIGNING_KEY signs Coral’s access tokens; losing it invalidates every session, so treat it like any other production key material.

Compose file

Create compose.yaml, replacing the example.com hostnames and the [auth.provider] issuer and client ID with yours:
Coral UI talks native gRPC straight to coral:14555 inside the Compose network. That leg is cleartext on an operator-controlled network, which Coral UI refuses by default; CORAL_UI_ALLOW_INSECURE_CORAL_ENDPOINT=1 is the explicit opt-in for exactly this wiring. To avoid the opt-in, terminate TLS in front of the gRPC listener too and point CORAL_ENDPOINT at an https:// URL. And the Caddyfile:

Start and verify

The Coral server logs a startup warning about its non-loopback cleartext listeners; that is expected here, since Caddy terminates TLS in front of them and the Compose network is not exposed. Then verify each surface:
The first returns {"coral":"reachable","status":"ok"} once Coral UI can reach a ready Coral server. The second returns 401 Unauthorized with a WWW-Authenticate header pointing at the OAuth resource metadata — that is an MCP client’s entry point into the login flow, so a 401 here means MCP is wired correctly. Open https://coral.example.com in a browser. You are redirected to your identity provider’s login and land back in Coral UI signed in. The trusted_clients entry in the seed config is Coral UI’s own client ID, so its sign-ins skip Coral’s approval page; drop that line to have every sign-in pause on the approval page instead (see Configuration).

Connect an MCP client

Point any MCP client that supports Streamable HTTP and OAuth at:
The client discovers Coral’s authorization server from the protected-resource metadata and walks the browser login, pausing on Coral’s approval page — unlike Coral UI, MCP clients are not listed in trusted_clients — before your identity provider’s sign-in. No token needs to be copied by hand.

Administering a deployed instance

Most administration happens in the Coral UI: installing, editing, importing, and removing sources, creating workspaces, browsing the schema and traces, and toggling runtime features. The coral CLI inside the container operates on the same state as the server (concurrent access is safe — state access is serialized through a file lock), which covers the remaining operations:
The installed CLI on your workstation cannot target a remote server: CLI commands always operate on the machine’s own local state, so administration runs either in the browser or inside the container.
An MCP client on the Docker host can also use the stdio transport against the containerized instance without enabling MCP HTTP: configure the client to run docker compose exec -i coral coral mcp-stdio (or docker exec -i <container> coral mcp-stdio).

Changing the configuration later

CORAL_SEED_CONFIG only seeds the first boot. Afterwards, edit the config on the volume and restart:
Writing through exec keeps the file owned by the container user; copying it back in with docker compose cp would leave it root-owned and unreadable to the server.

Persistence and backup

Everything the server owns lives on the coral-data volume: config.toml, the SQLite database, workspace and source state, and credential material. Back up that volume. To move state to Postgres instead of SQLite, see Database — workspace and source files remain on the volume either way, so the Coral server stays a single-instance deployment.

Upgrades

To upgrade, edit compose.yaml to the new version, keeping the coral and coral-ui tags identical (release tags of the two images move together), then:

Health endpoints

Environment variable reference

coral container

The server reads its networking, auth, and telemetry configuration from config.toml, not environment variables. See Configuration. The container-level variables: Variables named by config.toml keys — [database].url_env, [auth.provider].client_secret_env, [auth.session].signing_key_env — must also be provided when configured.

coral-ui container