Skip to content

Repository files navigation

stack-installer

English · Русский

A manifest-driven setup wizard and generic update agent for Docker Compose stacks.

Ship any dockerized application with a guided first-run installer and safe, in-app updates — by writing a single setup-manifest.yaml. The engine and agent contain no knowledge of your application: everything app-specific lives in the manifest (data), never in code.


Why

  • vs. hand-rolled docker compose — your operators get a browser wizard with validation, secret handling and registry login instead of editing .env by hand.
  • vs. Helm / Ansible — no cluster and no playbooks. Just Compose plus one YAML manifest; the operator clicks through a wizard in the browser.
  • vs. a PaaS (Coolify / CapRover) — this is not a platform. It's an embeddable installer/updater you ship with your app so customers can self-host it.
  • Generic by design — the same image installs and updates any Compose stack. All steps, fields, validators, update phases and modules are described in the manifest.

Components

Service Port Role
Engine (wizard) 8000 Public-facing: serves the Vue SPA + /api/setup/*, reads the manifest, renders/validates steps
Agent 8001 Internal: /agent/* — Docker operations (pull/up/down), .env, registry login, backups, updates, modules

Both run in one image (docker-entrypoint.sh starts them). In a Compose deployment the agent can run as its own service on a private network — set AGENT_BASE_URL.


How images are delivered

stack-installer never builds images — it pulls ready-made images that you have already built and pushed to a container registry (usually a private one). Your docker-compose.yml references them by name and tag; the agent pulls them on first install and on every update.

The chain:

  1. You publish images. Your CI builds and pushes your service images to your registry, tagged with a release version. Compose references them as, e.g., image: ${REGISTRY_HOST}/you/myapp-backend:${VERSION}.
  2. The operator authenticates in the wizard. stack.registry in the manifest declares how to log in (none / basic / license / token). After the relevant step passes, the agent runs docker login <REGISTRY_HOST>.
  3. The agent pulls. prefetch runs docker compose pull in the background while the operator finishes the wizard; the pull phase re-pulls on updates. Docker fetches each service's image by tag from the registry, using the login from step 2.
  4. Updates fetch the new tags. On update the agent pins the target VERSION, pulls the new image tags, and — if configured — downloads the signed docker-compose.yml and setup-manifest.yaml for that version from the license server (see Security model) before bringing the stack up.

Private registry via a license key (kind: license)

A common pattern for commercial self-hosted apps: instead of giving customers raw registry credentials, you put a registry proxy in front of your registry that accepts the customer's license key as the Docker password. The wizard collects the license key once; the agent runs docker login <REGISTRY_HOST> -u license -p <LICENSE_KEY>, Docker performs the standard /v2/token exchange, and the proxy decides which images (and which versions/tiers) that license may pull. The customer never sees real registry credentials, and you can revoke or scope access per license. (See the license row under Registry authentication.)


Prerequisites

  • Docker Engine with Docker Compose v2 (the agent shells out to docker compose).
  • The host Docker socket mounted into the agent (/var/run/docker.sock).
  • For local development: Python 3.13, Node 20 (frontend). PyNaCl is optional and only needed for manifest-signature verification on the update path.

Quick start (first install)

  1. Write a setup-manifest.yaml describing your wizard (see below and examples/).
  2. Run the image, mounting the Docker socket and your project directory:
docker run \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /path/to/project:/host-project \
  -e HOST_PROJECT_DIR=/host-project \
  -e COMPOSE_FILE_PATH=/host-project/docker-compose.yml \
  -e COMPOSE_ENV_PATH=/host-project/.env \
  -e COMPOSE_PROJECT_NAME=myapp \
  -e MANIFEST_PATH=/host-project/setup-manifest.yaml \
  -p 8000:8000 \
  <your-registry>/stack-installer:latest

The published image name depends on your registry. Build it yourself from this repo's Dockerfile (docker build -t stack-installer .) or pull from wherever you publish it.

  1. Open http://<host>:8000, complete the wizard. On the final step the engine writes the collected values to .env, runs docker compose up -d, and calls your POST /api/setup/apply hook. See examples/ for a runnable two-step example.

Writing a manifest for your stack

Minimal structure

manifest_version: 1

stack:
  name: "My App"
  compose_file: docker-compose.yml
  project_name: myapp

steps:
  - id: config
    title: "Configuration"
    fields:
      - key: APP_URL
        label: "Public URL"
        type: url
        required: true
        env: true

finalize:
  apply_env: true
  up: true

Field types

type Notes
string Plain text input
password Masked input; set secret: true to protect in .env
number Numeric, optional min/max
email Validated email format
url Validated URL format
select Dropdown; provide options: [a, b, c] or options_from: agent.stack/profiles
totp_setup Generates a TOTP secret + QR code; see builtin.totp_setup

Field flags

flag Default Effect
env: true false Value is written to .env on finalize
secret: true false Marks value as secret (chmod 600 in .env, cleared from state after finalize)
required false User must fill this field
required_when: {OTHER_KEY: value} Conditionally required
hide_when: {OTHER_KEY: value} Hide field when condition matches
show_when: {OTHER_KEY: value} Show field only when condition matches (inverse of hide_when)

Values in required_when, hide_when, and show_when may be plain scalars (string equality) or a {contains: <val>} dict to check membership in a multiselect field, e.g.:

show_when: { extra_services: { contains: tools } }

Step validation

Attach a validate: block to run a check after the user fills a step:

validate:
  kind: builtin.http_check
  endpoint: "https://api.example.com/ping"
  expect:
    status: 200
    json_path: ok
    equals: true

Available built-in validators:

kind What it checks
builtin.http_check HTTP endpoint — status code + optional JSON path
builtin.db_connect MySQL or PostgreSQL TCP connect + auth
builtin.redis_ping Redis PING
builtin.s3_head_bucket S3-compatible bucket access (HeadBucket)
builtin.totp_setup Generate TOTP secret + QR code
webhook Custom self-contained endpoint (see CONTRACT.md)

Add validate_post_up: true to defer a check until after docker compose up:

validate:
  kind: builtin.http_check
  endpoint: "{APP_URL}/api/health"
  validate_post_up: true

Field groups (toggled sections)

Use group: with a toggle: to show/hide a set of fields:

fields:
  - group: database
    toggle:
      key: db_local
      label: "Use built-in database"
      default: true
    when_off:
      - key: DB_HOST
        type: string
        required: true
        env: true
      - key: DB_PASSWORD
        type: password
        env: true
        secret: true
    validate_when_off:
      kind: builtin.db_connect
      driver: mysql

Background image pull (prefetch)

Images are pulled in the background while the user fills later steps:

prefetch:
  trigger_after_step: auth   # pull starts after this step passes
  services: all              # or list of service names

Finalize block

finalize:
  apply_env: true         # write all env:true fields to .env
  up: true                # run docker compose up -d
  post_up_hook:
    endpoint: "internal://backend/api/setup/apply"
    method: POST
    payload: collected    # send all collected wizard values
    timeout: 120
  post_up_checks:
    - kind: builtin.http_check
      endpoint: "internal://backend/api/health"
      retries: 20

See CONTRACT.md for the full post-up hook contract.

Registry authentication

stack:
  registry:
    host: registry.example.com
    login:
      kind: basic
      username: deploy
      password_from_env: REGISTRY_PASSWORD

Supported kind values:

kind How it works
none Public registry — no login
basic docker login with username + password from password_from_env (collected field or env var)
license The collected key_field value is used as the registry password. Equivalent to docker login <host> -u license -p <LICENSE_KEY>. Docker performs the /v2/token exchange itself against the registry proxy. Typical use: a private registry proxy that accepts a license key as the password.
token (optional/legacy) Fetches a pre-obtained pull token from credential_endpoint via GET, then uses it as the password.

Updating a deployed stack

After first install the agent can update the running stack to a new release from the app's own admin UI, without SSH. Updates are declarative: you list ordered phases under runtime.apply in the manifest, and the agent runs them. The agent itself stays generic — the phases are data.

runtime:
  apply:
    phases:
      - { id: preflight,    kind: builtin.preflight, min_disk_gb: 2 }
      - { id: backup,       kind: builtin.db_backup }
      - { id: fetch-compose, kind: builtin.fetch_compose }   # pull signed compose/manifest for the target version
      - { id: pull,         kind: builtin.pull }
      - { id: maintenance-on, kind: builtin.maintenance, mode: "on" }
      - { id: up,           kind: builtin.up }
      - { id: migrate,      kind: exec, service: app, command: ["app", "migrate"] }
      - { id: health-check, kind: builtin.http_check, endpoint: "internal://nginx/healthz", retries: 15 }
      - { id: maintenance-off, kind: builtin.maintenance, mode: "off" }
      - { id: report,       kind: builtin.hook, endpoint: "{LICENSE_SERVER_URL}/api/v1/installed", method: POST }

Key properties:

  • Two-step deploy. The update can pause after the non-destructive part (backup + image pull) and wait for the operator to confirm before the destructive part (maintenance → recreate → migrate). The site stays up during preparation.
  • Progress that survives restarts. Phase progress streams over SSE; the agent also serves a standalone progress page (/agent/ui, /agent/progress/{id}) so the operator keeps seeing progress even while the app's own web tier is being recreated.
  • Maintenance mode. A flag on a shared volume; your reverse proxy returns 503 (a static maintenance page) while it is set, then resumes when the new version is healthy.
  • Forward-only on failure. The agent does not auto-rollback — premature or partial rollbacks cause more harm than good. On failure it leaves the stack in a consistent state and surfaces the error; the operator decides to retry (fix-forward) or trigger a manual rollback.
  • Resume after crash. Completed phases are recorded; a re-run skips them.

Modules (optional add-ons)

The agent can install / update / uninstall optional add-ons at runtime — extra services, or extra application code — driven by a module manifest (data, same philosophy as the setup manifest). A module install can: extract code from a module image into a named volume, write env vars, bring up sidecar containers, enable reverse-proxy routes, create storage buckets, and run migrations. Update mode does a clean re-extract of the new version and force-recreates only that module's containers; it is forward-only and never touches user data on failure. Uninstall stops the module's containers and removes its code, but never deletes data or buckets.


Security model

This tool performs privileged operations. Understand the model before exposing it.

  • The agent is privileged. It uses the host Docker socket (/var/run/docker.sock), which is effectively root on the host. Treat the agent as a trusted, internal-only component.
  • Do not expose the agent control API. /agent/apply, /agent/up, /agent/env, /agent/install_module, … are unauthenticated and internal (the application backend calls them over a private network). Your reverse proxy must expose only the two read-only progress endpoints needed by the standalone progress page (/agent/ui and /agent/progress/) — nothing else under /agent/.
  • Hook authentication. The post-up / report hooks use a shared secret (SETUP_APPLY_SECRET, sent as X-Setup-Secret), resolved at runtime; your application verifies it before acting on the hook.
  • Signed updates. Release descriptors are ed25519-signed. Before downloading or applying a release's compose/manifest the agent verifies the signature against MANIFEST_PUBKEY_HEX. If no public key is configured, verification is skipped — that is a development-only mode; always set the public key in production, otherwise a forged manifest could point the agent at an attacker's image.
  • Restricted per-client overrides. When per-deployment service overrides are used the agent renders them under a strict policy: images only from the configured registry host, named volumes only, no privileged / host bind-mounts / host network.
  • Secrets handling. Fields marked secret: true are written to .env with chmod 600 and cleared from wizard state after finalize.
  • No app knowledge, no telemetry. The engine and agent are generic. They talk only to your own endpoints and, if configured, your registry / license server.

Architecture

Browser
  └─ Vue 3 SPA (port 8000, served by engine)
       └─ GET/POST /api/setup/*
            └─ engine (FastAPI, port 8000)
                 ├─ reads setup-manifest.yaml  (engine/manifest.py)
                 ├─ renders step schemas       (engine/steps.py)
                 ├─ validates fields           (engine/validators/)
                 ├─ tracks wizard state        (engine/state.py)
                 └─ POST /agent/* (HTTP, AGENT_BASE_URL)
                      └─ agent (FastAPI, port 8001)
                           ├─ docker compose pull / up / down  (agent/compose_runner.py)
                           ├─ writes .env                      (agent/env_writer.py)
                           ├─ registry auth                    (agent/registry_auth.py)
                           ├─ module install / uninstall       (agent/module_installer.py)
                           ├─ apply phases (update flow)        (agent/apply_runner.py)
                           └─ backup drivers (mysql/postgres)  (agent/backup/)

Local development

Engine (FastAPI, port 8000)

pip install -r requirements.txt

export MANIFEST_PATH=/path/to/setup-manifest.yaml
export AGENT_BASE_URL=http://localhost:8001
export HOST_PROJECT_DIR=/path/to/your/project

uvicorn engine.api:app --reload --port 8000

Agent (FastAPI, port 8001)

export HOST_PROJECT_DIR=/path/to/your/project
export COMPOSE_FILE_PATH=$HOST_PROJECT_DIR/docker-compose.yml
export COMPOSE_ENV_PATH=$HOST_PROJECT_DIR/.env
export COMPOSE_PROJECT_NAME=myapp
export AGENT_DB_PATH=/tmp/agent-state.sqlite
export AGENT_LOCK_PATH=/tmp/agent-lock
export MANIFEST_PATH=/path/to/setup-manifest.yaml

uvicorn agent.api:app --reload --port 8001

Frontend (Vue 3 / Vite, dev server port 5173)

cd frontend
npm install
npm run dev      # proxies /api/* to the engine on port 8000
npm run build    # production build → frontend/dist/ (served by the engine)

Tests

python -m pytest -q

Extending

Adding a built-in validator

  1. Create engine/validators/builtin_<name>.py with a validate(config, values) -> dict function.
  2. Register the kind string in the run_step_validate dispatch in engine/steps.py.
  3. Document the new kind in the validator table above.

Adding a new field type

  1. Add the type string to ManifestField in engine/manifest.py.
  2. Handle rendering in engine/steps.py (_render_field) if extra schema data is needed.
  3. Add a matching input component branch in frontend/src/FieldRenderer.vue.

Adding a backup driver

  1. Create agent/backup/<engine>.py subclassing BackupDriverBase from agent/backup/base.py.
  2. Register the engine name in agent/backup/registry.py.
  3. Add tests in tests/test_backup_drivers.py.

Documentation & contributing

  • CONTRACT.md — the post-up hook and webhook-validator contracts your app implements.
  • CONTRIBUTING.md — development setup and extension points.
  • LICENSE — MIT.

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages