Skip to content
SkyKeephelp

Startup sequence

In order, from an empty machine to a signed-in vault. Steps with a Check line are verified against every real deployment by the test gate.

  1. Configure the environment

    Copy the checked-in template to `.env` and fill every value — the stack refuses to start on any unset required variable (that refusal is fail-closed configuration, not a bug). The settings reference explains each key; secrets like SKYKEEP_ENGKEY must be generated fresh per deployment and never reused across engagements.

    cp env.example .env
    python3 -c 'import secrets; print(secrets.token_hex(32))' # a fresh SKYKEEP_ENGKEY
  2. Start the services and wait for health

    Compose starts seven services — vecstore (Postgres+pgvector), auditstore (the physically separate audit instance), ollama (the model runtime), the webserver (publishes NO host port), the site (the sales pages, ADR-0057; publishes NO host port either), pagerender (PDF page images, ADR-0091; on the compose-internal network only), and the tls-proxy (the deployment's ONLY HTTP entry, with a second listener for the site). `--wait` returns only when every container's own healthcheck passes; a service stuck 'starting' is the first thing to read logs for. An eighth service, worker, is the webserver's own image running `python -m skykeep.workflow` (ADR-0092): the same configuration with no HTTP server and no published port, running the ingest loop and nothing else, so document processing stops competing with the request path. How many of them start is SKYKEEP_WORKFLOW_WORKERS (shipped as 0 — it has no default in the compose file, so it must be set in the env file), and SKYKEEP_WEBSERVER_INGESTS (true/false, shipped true) says whether the webserver ALSO drains the queue itself. Setting it false with zero workers would leave a vault that accepts uploads and processes none of them, so that pair is refused at startup and names both settings; worker containers have no healthcheck because they answer nothing — what watches them is the queue-health check, which stays in the webserver. The request path's own concurrency (ADR-0100) is two more settings on the webserver itself: SKYKEEP_REQUEST_THREADS (shipped 40) sizes the thread limiter every sync route runs under, and SKYKEEP_WEB_WORKERS (shipped 1) is how many uvicorn processes this one container runs — more of them costs memory per process, not configuration. Both refuse at startup by name outside their accepted range.

    docker compose up --build --detach --wait
    docker compose ps # every service 'healthy'
  3. Apply the database migrations

    Two forward-only series, one per instance: the primary schema and the audit instance's own. Re-running is a checksummed no-op, so applying at every deploy is safe and expected.

    python -m skykeep.db.migrate
    python -m skykeep.db.migrate --audit
  4. Check the front door

    Liveness through the TLS proxy — the same path users take. Dev/CI stacks self-sign, so a certificate warning there is expected; production mounts real material at /etc/skykeep/tls.

    Check: GET /healthz → HTTP 200

  5. Check the dependency legs

    The status endpoint probes the database and model runtime from inside the deployment; both legs must report ok before the vault is usable. A red database leg usually means migrations have not been applied; a red model leg means the configured model runtime is unreachable or its models are not pulled.

    Check: GET /api/status → HTTP 200; database.ok true; model_runtime.ok true

  6. Open the portal and sign in

    The portal page must render, and it opens as a plain username/password sign-in page. A FRESH vault has exactly one account: the bootstrap administrator, identifier admin001 with the documented initial password initpass001.33a (ADR-0012). That first login needs no mail and no one-time code (ADR-0013): the password alone signs in, straight into a forced password change that also collects the account's email address — the vault refuses everything else until both are set. The next sign-in after that asks for a one-time code ONCE, because consuming a code is what enrols the account; after that the account's own preference decides whether it is asked again, and the shipped default is not to be (ADR-0025). An administrator who wants the code back for everybody sets require_all_mfa, which overrides every account preference. Users (each with their own email) are managed in the admin console.

    Check: GET / → HTTP 200