Quickstart · Docker Compose
Fifteen minutes to a verified CA.
At the end of this page you have a running goca, a changed admin password, a real CA hierarchy with a test certificate issued, and a verified audit chain. Time required: about fifteen minutes.
Prerequisites
- Docker with Compose v2 — the only supported way to run the local
bundle. (Building the binary yourself instead needs Go ≥ 1.24
plus a C toolchain for the PKCS#11 bridge:
CGO_ENABLED=1 go build -tags pkcs11 ./cmd/goca.) - ~1 GB disk for images and the Postgres volume.
- Nothing else. No external services, no internet access after the image build.
Step 1 — bring the stack up
Your approved download link delivers the evaluation bundle. Unpack it, then:
cd goca
docker compose up --build
This starts two containers (profile all-in-one, preset in
.env): goca-db (PostgreSQL 16) and
goca running every role in one process. A third short-lived
container, goca-db-migrate, applies the schema first —
services wait for it. When the log line
starting role api-admin (management plane) on :8081
appears, the system is up.
No download link yet? Request one
What is now listening, and where
Every one of these ports serves HTTPS, from a certificate goca issues to itself (step 2).
| Address | What |
|---|---|
127.0.0.1:8085 | Admin console — start here |
127.0.0.1:8081 | Management API (the console proxies into this) |
:8080 | Issuance plane — the REST API is served here (also the ACME head's storage API) |
:8087 | ACME directory endpoint(s) |
:8083 | CRL distribution — GET /crl/{ca}/{generation} |
:8082, :8084 | CMP / OCSP listeners — both answer health and their role menu; the CMP head has no message handler yet, and OCSP responses are not served in this release Roadmap |
The scep, est, rest and
emul-rest heads are designed and not built, so nothing binds
:8088–:8091.
Management stays on loopback
The management surfaces (8081, 8085) are published on
loopback only, on purpose. Reach the console from
another machine with an SSH tunnel
(ssh -L 8085:127.0.0.1:8085 <host>) or by publishing
it on a management network — never by rebinding them to
0.0.0.0. goca terminates its own TLS, so a
reverse proxy in front is optional, not required.
Step 2 — the browser warning you should expect
At first boot goca stands up its own trust before anything else: it
creates goca-infra-root → ica-infra and the
goca-service-tls template, then issues every listener a real
certificate and hot-swaps it in — usually within a second of the database
being up. Until that first swap the listeners serve an ephemeral
self-signed bootstrap certificate, and your browser will
warn. The warning is expected in exactly two situations: this
bootstrap moment, and a fault the console's alert banner and
Status → Components → Edge TLS will name. Certificates renew and
hot-swap automatically from then on — no cron job, no restart, no proxy.
To make the warning go away for good, trust the infra root. Fetch it from the open endpoint:
curl -sk https://127.0.0.1:8081/infra/trust -o goca-infra.pem
Import it into your OS/browser trust store, or point tools at it
(curl --cacert goca-infra.pem …). The endpoint serves the
self-signed root (CN=GOCA Infrastructure
Root) — that is the file to import, not the issuing CA. On
Windows it belongs in Trusted Root Certification Authorities,
not Enterprise Trust. Restart the browser afterwards; the store
is read at start.
The commands below use curl -k (skip verification) for
brevity, as the manual does; substitute
--cacert goca-infra.pem once you have fetched the root.
Step 3 — first login: do these three things immediately
-
Change the bootstrap password. Open
https://127.0.0.1:8085and sign in asadmin/admin. This account exists so a fresh install is usable; the server logs a loud warning until its password changes. Change it now (console: Users → your identity, orPOST /v1/identities/admin/password). - Label the system. The console shows a configurable heatline — a colored bar with a label (PROD, TEST, ROOT CA, …) above every screen, including login, where it matters most. On a fresh instance the top bar shows a muted "Set environment…" badge; click it. Presets cover the common convention (red = production, blue = root, amber = staging, green = test). Set it on day one — it is the cheapest wrong-instance protection you will ever deploy.
-
Set a real master secret before creating anything you
intend to keep.
GOCA_MASTER_SECRETis the root of software key protection: the KEK that wraps every CA private key is derived from it. The shipped compose file sets a well-known development value — fine for evaluation, unacceptable for anything real. A hierarchy created under the dev value is protected by a public string; treat every key and token created under it as public.
The master secret, in four rules
Set it via your secret mechanism, never committed. Set it before creating your hierarchy — changing it later makes every wrapped key unusable (that is the design: a stolen database without the secret is inert). Escrow it — losing it means losing every software-custody CA key. Give it only to the containers that must unwrap a CA key: the all-in-one, the API service and the key service; everything else runs without it.
Step 4 — create a real hierarchy and issue a test certificate
In the console: CAs → New CA — create a self-signed
root (pick an algorithm; slh-dsa-sha2-192s is the
conservative PQC root choice, ECDSA P-384 the classical one), then an
issuing CA with issuerRef pointing at the root. For
production hierarchies, prefer running CA creation as a
ceremony (Ceremonies → plan → quorum approval →
execute — the four-eyes flow with a hash-chained transcript); the direct
API path below creates immediately and is fine for evaluation. Then
issue a test leaf from the issuing CA (Certificates → Issue) and sign a
first CRL for it.
The same via API (token from POST /v1/auth/login):
A="Authorization: Bearer $TOKEN"; API=https://127.0.0.1:8081
curl -sk -H "$A" -d '{"name":"root-g1","commonName":"Example Root CA G1","organization":"Example","algorithm":"ecdsa-p384","validityYears":20}' $API/v1/cas
curl -sk -H "$A" -d '{"name":"ica-tls","commonName":"Example TLS CA G1","organization":"Example","algorithm":"ecdsa-p256","validityYears":10,"issuerRef":"root-g1"}' $API/v1/cas
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
-keyout test.key -subj "/CN=test.example.internal" -out test.csr
curl -sk -H "$A" -d "$(jq -n --rawfile csr test.csr \
'{csrPem:$csr, profile:"leaf", validityDays:90, dnsNames:["test.example.internal"]}')" \
$API/v1/cas/ica-tls/issue
curl -sk -H "$A" -X POST $API/v1/cas/ica-tls/generations/1/crl
curl -sk https://127.0.0.1:8083/crl/ica-tls/1 -o ica-tls.crl # relying-party view
The CSR is required for this workflow: it carries the subject key and
goca never receives the private key. profile is
leaf — the default — or sub-ca; a certificate
template controls the result. The response returns
certificatePem. (This sequence uses jq to
build the JSON body.)
Step 5 — five green checks
A fresh install should pass all five:
docker compose ps # both services healthy
curl -sk -H "$A" $API/v1/status/components # every role heartbeating
curl -sk -H "$A" "$API/v1/audit/verify?limit=0" # → "status":"intact"
curl -sk -H "$A" $API/v1/audit/checkpoints # ≥1 signed head after ~5 min
curl -sk -H "$A" $API/v1/status/edge-tls # every listener "source":"issued"- Both services healthy in
docker compose ps - Every role heartbeating on
/v1/status/components - Audit chain verifies
"status":"intact" - At least one signed audit head (or force one with
POST /v1/audit/checkpoints) - Every listener serving an
issuedcertificate on/v1/status/edge-tls
In the console, Status → Audit & Logs should show intact plus a green signed heads badge.
Sidebar — restarting the stack: three shapes, three costs
Every long-running service in the bundle carries
restart: unless-stopped — the database
included. Measured on a laptop running the distributed profile;
your numbers will vary, the ordering will not:
1 · Host reboot
Automatic, seconds. The unless-stopped
policy restarts the existing containers directly — no dependency
chain, no init jobs, no seed. Nothing to type. (Docker Desktop must
itself start with the machine: Settings → General → "Start Docker
Desktop when you sign in".)
2 · stop / start
~25 s to fully green. Same containers, clean 15 s drain on the way down. This is the way to pause and resume a stack. Services answer sooner than the status flips — the healthcheck probes every 10 s.
3 · up -d
~65 s to fully green. Recreation — the only path that runs the dependency chain (database healthcheck, migrations, role passwords, TLS init), and the only one you need after changing images or compose configuration.
What never to do
A blanket docker start across all containers (or
a desktop-UI "start everything"). That re-runs every one-shot
container — including the demo seed, which will
happily load its fixtures into your live database — and revives
retired containers, neither of which docker compose would
ever do. If the goal is "the stack I had before", shape 2 is the whole
answer.
Next steps
- Go distributed.
COMPOSE_PROFILES=distributed docker compose up --build— one container per role, same image, one network per edge of the call graph. This is the shape that makes thera/issuer/keysvcsplit real. - Mount an HSM. Two read-only mounts on the keysvc
container and a
Pkcs11Moduledocument — goca ships no vendor library and no vendor configuration, ever. - Learn the model. The operator training covers custody, the issuance split, and the audit chain in depth.
- Evaluation only: seed a fake estate with
docker compose run --rm goca-db-seed— all key material in the seed is fake digest bytes; nothing can sign. Never seed an installation you intend to use for real.