Deploy an instance
This guide takes a Fountain instance from first boot to its first successful conversation with Docker Compose. You verify readiness, create the first admin account, close registration, and exercise the database, encryption, inference and sandbox provider end to end.
For a development environment on your own machine, read Setup. That is a different thing. This guide is for an instance that stays up.
Before you start
You must have Docker Engine with Compose v2, and openssl for the two key
lines below. Fountain requires Postgres 16 or newer. The compose file runs one
for you, or you can point Fountain at a Postgres you operate.
Your machine must also reach ghcr.io, the registry that holds the published
image. Does your network block it? Then comment the image: line in
docker-compose.yml, uncomment the build: . line under it, and compose
builds the image from this checkout.
You must also have a sandbox provider token. Read Self-host Fountain for what each provider needs, and for where a token comes from.
Bring it up
git clone https://github.com/managoat/fountain
cd fountain
cp .env.compose.example .env
echo "SECRET_KEY_BASE=$(openssl rand -base64 48 | tr -d '\n')" >> .env
echo "MASTER_SECRETS_KEY=$(openssl rand 32 | base64 | tr '+/' '-_' | tr -d '=\n')" >> .env
# add your SPRITES_TOKEN to .env
docker compose up -d
Do you reach this instance by a host other than localhost? Then set
PUBLIC_URL in .env as well. It defaults to http://localhost:4000, it
builds the links in verification emails, and every sandbox reads it as
FOUNTAIN_BASE_URL. Put it on the internet
covers the rest of a public deployment.
Back MASTER_SECRETS_KEY up now, before you have data. It is not in the
database, so a database backup alone does not protect you. Read
Back up and restore.
These guides explain each variable that shapes a deployment as it comes up. The configuration reference holds the complete list, and it includes the deploy-level variables that the compose file never mentions.
Verify the instance is ready
The app applies database migrations before it opens a listener, so a cold
start takes up to a minute. Wait for the app container to report healthy,
then probe it.
docker compose ps
# wait for the app to report healthy, then:
curl -sS localhost:4000/health/ready
# {"checks":{"database":"ok"},"status":"ok"}
Expect refused connections during first boot until migrations finish. If the
app still refuses connections after a minute, inspect
docker compose logs -f app.
Register the first account
Open http://localhost:4000 and create your account.
The compose defaults are EMAIL_DELIVERY=none and FIRST_USER_ADMIN=true.
Your account then self-verifies at registration, and Fountain promotes it to
admin because it is the first. The admin audit trail records the grant, like
any other role change (ADR 0011).
Register before you expose the instance to a network you do not trust. While no admin exists, the first verified account takes the role.
For the manual path, set FIRST_USER_ADMIN=false and use a release task. Read Run a release task.
docker compose exec app bin/fountain_server eval \
'Fountain.Release.promote_admin("you@example.com")'
Close registration
echo "REGISTRATION_ENABLED=false" >> .env
docker compose up -d
Registration is open by default. Disable it immediately after you create the first admin and before you expose the instance to an untrusted network.
Point the apps at it
Fountain's own UI is a console. It covers the account, its keys, and the
agents, environments and vaults that a conversation runs on. To watch a
conversation turn by turn, and to message an agent as a teammate, you use
separate single-page apps that talk to your /api.
| Conversations | Start a run, watch it, steer it, read the raw log. |
| Team | Your agents as teammates, one thread for each. |
They are static builds with no server of their own. You type your Fountain's URL in, so the hosted copies above work against your deployment as soon as it admits the origin.
echo "API_CORS_ORIGINS=https://fountain-conversations.demo.managoat.com" >> .env
To click "Sign in with Fountain" instead of a paste of an API key, register
them in OAUTH_CLIENTS. Read the
configuration reference.
The console links to whatever CONVERSATIONS_APP_URL and TEAM_APP_URL say.
Point those at your own build of either repo and it works the same way. Set
them to "" to tell the console that this deployment has neither, and the
console stops the offer.
Prove the whole path
Readiness checks Fountain's connection to Postgres. It does not check encryption, inference credentials or the sandbox provider. In the console, add a model credential under Settings, then Inference credentials. Open Conversations and start a run. A first turn confirms that Fountain can read Postgres, decrypt the credential, reach the model provider and start a sandbox through the selected provider.
Start over
docker compose down -v
The -v flag deletes the database volume, and every account and conversation
in it. Keep the same MASTER_SECRETS_KEY when you keep the volume. A new key
cannot unwrap what the old key wrapped. Every stored environment and vault
value then becomes unreadable.
If it did not work
Does the app serve while a sandbox fails to start? The sandbox provider is the usual cause. Read Sandbox errors.
If the container never opens a listener, migrations cannot reach the database. Read Pods restart or never go ready.
Related
- Put it on the internet, the next step.
- Back up and restore.
- Configuration reference.
- Architecture, for what runs, and what breaks when a dependency is down.
Verify the deployed instance
The external suite checks public HTTP and SSE through your ingress. Use Node 24 or newer from a pinned Fountain checkout. It needs no application database access. Provision two dedicated, verified test accounts and full-scope API keys through the console. Configure inference credentials on the primary account for execution checks. Keep both accounts separate from customer data; registration can remain closed. Limit provider spending on that account with the provider's own budget controls.
Load the two keys into FOUNTAIN_SUITE_KEY and FOUNTAIN_SUITE_OTHER_KEY
through your secret store, then point the suite at the instance:
scripts/verify-deployment.sh http://localhost:4000 basic
scripts/verify-deployment.sh https://fountain.example.com
The first checks a local Compose instance's API surface. The second runs the
default streaming profile against a remote one: a real conversation with two
tool-using turns, live output, reconnect, replay and history. It prints the
failing checks, how many fixtures are left behind and where the evidence is.
Plaintext HTTP is accepted only for a loopback target, and a URL carrying
credentials, a query or a fragment is refused before anything is written.
On macOS a key you have not exported is read from the keychain, under an account naming the exact target it belongs to:
security add-generic-password -U -s fountain-deployed-suite \
-a 'https://fountain.example.com|FOUNTAIN_SUITE_KEY' -w
A stored key is only ever offered to that origin, so one kept for a production instance is never sent to a local one, and pointing the command at an unrelated host finds nothing rather than disclosing a key to it.
Select coverage with the second argument: probe for identity, capability and
health checks, basic for the API surface without a sandbox, execution for
two turns without streaming conformance, canary for both, or streaming for
everything. Missing required credentials or capabilities fail the run; they
never become passing skips. The integration profiles — secrets, mcp,
webhooks and schedules — need configuration these flags do not supply, and
run from a target file through deployed/cli.mjs.
Change what the run declares with flags, which
node deployed/verify.mjs --help lists in full:
node deployed/verify.mjs https://fountain.example.com --profile execution \
--runtime codex --model openai/gpt-5.5 --sandbox e2b
For a run that needs a field those flags do not cover, write a target file and use the underlying CLI. The same two keys apply:
{
"base_url": "http://localhost:4000",
"credentials": {
"primary": "FOUNTAIN_SUITE_KEY",
"secondary": "FOUNTAIN_SUITE_OTHER_KEY"
},
"profiles": ["basic"]
}
node deployed/cli.mjs run --config /tmp/fountain-target.json --out /tmp/fountain-check-001
For a real conversation through that path, select streaming and add explicit
execution settings:
{
"execution": {
"runtime": "claude",
"model": "anthropic/claude-haiku-4-5",
"sandbox_provider": "sprites",
"provision_ms": 120000,
"turn_ms": 90000,
"max_turns": 2
},
"limits": {
"request_ms": 30000,
"run_ms": 420000,
"cleanup_ms": 90000,
"resources": 12
}
}
Merge those fields into the target file. This checks a nonce artifact, two real tool-using turns, tenant isolation, live output, reconnect, replay and history agreement, then terminates and deletes its fixtures.
Run in CI
The Deployed verification workflow accepts only staging and production
and runs code from main. Create the matching GitHub environment,
deployed-staging or deployed-production, and restrict its deployment branch
to main. Configure these environment settings:
| Setting | Purpose |
|---|---|
Variable SUITE_ENABLED=true |
Explicitly enable this target. |
Variable SUITE_TARGET_JSON |
Target JSON with approved HTTPS URL, required capabilities and execution settings. |
Secret FOUNTAIN_SUITE_KEY |
Primary dedicated test account API key. |
Secret FOUNTAIN_SUITE_OTHER_KEY |
Different verified account's API key. |
Secret SUITE_KUBECONFIG |
Optional read-only Kubernetes access for rollout mode. |
Secret SUITE_MONITOR_URL |
Sentry Crons check-in URL for the scheduled canary. |
The workflow fixes credential variable names and bounds each run to seven minutes plus 90 seconds for cleanup. It creates at most 12 resources and attempts at most two inference prompts. These limits do not cap the monetary cost of a model turn. Profiles run sequentially. All dispatches and schedules share one concurrency group per target, and an active run is never cancelled by a newer one. GitHub can replace a pending run; use a separate workflow run ID when recording which rollout received verification.
gh workflow run deployed.yml --ref main \
-f target=staging -f profile=streaming -f mode=public
Public mode needs no cluster credentials and reports deployment identity as
unverified. Every initialized suite run retains result.json, junit.xml,
redacted traces and cleanup.json as a 30-day Actions artifact. Setup failures
before runner initialization remain visible in the workflow log. Credential
files are outside the uploaded directory and are removed at job completion.
Copy unresolved cleanup manifests to durable incident storage before expiry.
Invoke after rollout completion
The deployment owner invokes the same workflow after migrations and the serving deployment have finished rolling out. Neither image publication nor manifest publication invokes this workflow. In a Flux installation, wait for the relevant Kustomization's intended source revision and Deployment rollout, then dispatch verification. Do not treat Flux receiving a webhook as completion.
For Kubernetes, add an adapter to the environment-owned SUITE_TARGET_JSON:
{
"deployment": {
"adapter": "kubernetes",
"context": "staging",
"namespace": "fountain",
"deployment": "fountain",
"service": "fountain",
"container": "fountain"
}
}
The deployment owner must verify that the configured public URL routes only
to this Service, including any ingress, CDN, tunnel or regional routing. This
binding is an explicit trust input; the adapter does not discover it from a
successful HTTP response. Give its Kubernetes identity only get and list
on Deployments, ReplicaSets, Pods, Services and EndpointSlices in this namespace.
The CI runner must reach the API server. Public-only checks remain usable
when it cannot.
Obtain the intended image digest from the release's registry manifest or build provenance, independently of the running pods. Match the kind of identity reported by your container runtime: some report the multi-architecture image index digest; others report a platform manifest digest. The registry supplies both. This adapter requires one expected digest across the serving deployment; a fleet reporting different platform digests needs a future platform-aware adapter. Keep the registry's commit-to-digest provenance with the rollout record. The suite verifies the digest and does not independently claim a source commit from a mutable image tag.
kubectl --context staging -n fountain rollout status deployment/fountain --timeout=5m
gh workflow run deployed.yml --ref main \
-f target=staging -f profile=streaming -f mode=rollout \
-f expected_digest="$EXPECTED_IMAGE_DIGEST"
The adapter checks observed generation, all desired replicas, Pod ownership, container readiness and the intended digest. It reads every EndpointSlice for the Service and rejects missing, extra or terminating backends. It repeats the observation after public checks and cleanup; changed membership, generation, image or restart counts invalidate attribution. Reports retain both observations without raw cluster objects. These observations establish identity at the boundaries; they are not a continuous audit of all routing changes during the run.
For a local invocation of rollout mode, include expected_digest in the
deployment object and run the normal CLI with your read-only kubeconfig.
For other deployment types, run public mode until an external identity adapter
covers all their serving replicas. Never relabel that result as revision-verified.
Schedule and failure ownership
After a manual canary profile passes against the intended production target,
configure its Sentry monitor and set the repository variable
DEPLOYED_CANARY_ENABLED=true. The schedule runs basic API checks and two
real execution turns at 00:23, 06:23, 12:23 and 18:23 UTC. It is disabled until
that explicit configuration exists; this repository change alone does not
activate production runs.
Reuse the Sentry Crons channel for start, success and error check-ins, correlated by workflow run and attempt. Configure the monitor on the same six-hour schedule, a 30-minute check-in margin, a 12-minute maximum duration, an issue threshold of two consecutive failures and recovery after one success. Route its issues to the deployment's existing operational owner. Sentry keeps the consecutive-failure history and catches missed schedules; no repository cache is used as an incident ledger. A monitor delivery failure is visible in Actions, and a missing monitor URL fails a scheduled run before inference. Confirm the monitor's incident and recovery behavior before enabling the schedule.
Use the existing stage and provider alert policy to investigate canary failures alongside request IDs, provider health and runtime logs. A canary is additional external evidence for the same incident, not a separate rollback trigger. Assign setup, ingress, revision and cleanup failures to the deployment owner; assign runtime/provider failures to that owner for provider triage; assign reproducible contract failures to the API maintainer. Record the actual named owners in the environment's operations record before activation.
Start with visible verification. Before making this a promotion gate, collect at least 28 consecutive scheduled successes over seven days, zero unresolved cleanup entries, tested failure/recovery notification delivery and a named responding owner. Review durations and provider spending, and separate observed provider outages from suite defects. Blocking promotion is a separate change; this workflow does not modify a release, deployment or rollback policy.
On failure, inspect the failing phase and revision evidence in result.json.
Check cleanup even when public assertions passed. Retry cleanup with the same
URL and account:
node deployed/cli.mjs cleanup --config /tmp/fountain-target.json \
--manifest /tmp/fountain-check-001/cleanup.json --out /tmp/fountain-cleanup-001
Keep the manifest until every resource is cleaned. Never delete fixtures by a broad name prefix. An interrupted creation may commit after a timeout; an unresolved intent remains an incident until reconciled. Preserve failed-run evidence when repeating verification, and use a new output directory.