Home / Docs / Self-hosting & first steps

Self-hosting & first steps

From zero to a control plane running on your infra — in one command. yggdrasil init brings up Postgres, the core engine and adapters, creates the first admin and saves a CLI context. You walk away with a live, already-authenticated stack, ready to apply manifests and run workflows.

Requirements

Standalone mode packages everything into containers, so the list is short:

Docker
The engine must be installed and on your PATH. init aborts early with a clear message if it can't find the docker binary.
Compose v2
The docker compose plugin (v2, with a space — not the legacy docker-compose). init validates it by running docker compose version before anything else.
The yggdrasil CLI
A single static binary, no runtime dependencies. Installation in the next section.
(optional) a kubeconfig
The Kubernetes adapter mounts ~/.kube/config read-only. You only need it if you'll run workflows that apply K8s objects.

For existing cluster mode (--server), you need no local Docker at all — just network access to a core engine that's already running.

Install the CLI

The CLI ships as a pre-built binary on GitHub Releases. There's no container image: it's a single static binary per OS/architecture. The files follow the yggdrasil_<version>_<os>_<arch>.tar.gz pattern (Windows ships as .zip), with os and arch lowercase.

install the CLI
# pick the release and your OS/arch (lowercase)
$ VERSION=0.1.0          # the release you want
$ OS=darwin            # darwin | linux | windows
$ ARCH=arm64            # amd64 | arm64

$ curl -L -o yggdrasil.tar.gz \
    "https://github.com/dakasa-yggdrasil/yggdrasil/releases/download/v${VERSION}/yggdrasil_${VERSION}_${OS}_${ARCH}.tar.gz"
$ tar xzf yggdrasil.tar.gz
$ sudo mv yggdrasil /usr/local/bin/
$ yggdrasil version
 yggdrasil v0.1.0

Prefer to build from source? go install github.com/dakasa-yggdrasil/yggdrasil/cmd/yggdrasil@latest (Go 1.25+) works too. And a release binary knows how to update itself: yggdrasil update downloads, verifies the goreleaser checksum and swaps the binary in place; yggdrasil update --check only reports whether a new version is available.

Bring up standalone

With Docker and the CLI in place, one command stands up the entire control plane:

yggdrasil init
$ yggdrasil init
  writing docker-compose.yml + .env to ./yggdrasil
  docker compose up -d …
  waiting for /readyz …

 yggdrasil is ready

  directory: /abs/path/yggdrasil
  server:    http://localhost:9080
  context:   local
  username:  admin
  password:  k3p…q9  (generated — save it now)

Under the hood, init writes a docker-compose.yml and a .env (with random passwords) to the target directory — ./yggdrasil by default, adjustable with --dir — runs docker compose up -d, polls /readyz (which only responds once the database is reachable and bootstrap has finished), logs in as admin and persists the context to ~/.yggdrasil/config.yaml. The admin password is randomly generated and printed exactly once at the end — write it down. (Use --admin-username / --admin-password to pin credentials.)

The stack it brings up:

ServiceRolePort
postgresCore state (versioned manifests, runs, identity)internal :5432
coreThe core engine — server with authority, HTTP API /api/v1:9080 (host)
integration-kubernetesK8s adapter (applies objects to your cluster)adapter :8081 · health :8080
integration-schema-migrationsSQL migrations adapter (goose-postgres provider)adapter :8082 · health :8080

Only the core port (9080) is published to the host by default; it's configurable with --port. Confirm everything with yggdrasil status.

Connect to an existing control plane

If you already brought up the core engine yourself — Helm, Kustomize or any other path — skip the whole compose flow and just register the context. init --server only logs in and persists the configuration:

existing control plane
# attach to an already-running control plane (no local Docker)
$ yggdrasil init --server https://cp.example.com

# or authenticate an account at any time (supports MFA)
$ yggdrasil login --server https://cp.example.com --username ops@example.com
  Authenticator code (TOTP): ••••••
 logged in as ops@example.com → context "cp-example-com" saved

The context name is derived from the server host (so it won't overwrite the standalone local one), but you can pin it with --context. For accounts with MFA, login prompts for the authenticator code automatically in an interactive terminal; in CI, pass --totp <code> or --recovery-code <code>. The config.yaml is multi-context, kubectl-style: switch the active one with the YGGDRASIL_CONTEXT variable.

Transport: HTTP by default, AMQP opt-in

The core and the adapters talk over a transport. HTTP is the default and is always available — there's nothing to configure. RabbitMQ/AMQP is opt-in: the compose setup declares RabbitMQ under the amqp profile, so it only comes up when you explicitly ask for it.

enable AMQP
# 1. in the generated .env, uncomment the credentials and broker URL
#    BROKER_URL=amqp://yggdrasil:…@rabbitmq:5672/

# 2. bring it up with the amqp profile active
$ docker compose --profile amqp up -d

With BROKER_URL empty, the core comes up in HTTP-only mode and the RabbitMQ service is never started. You only turn AMQP on when your dispatch volume justifies the broker.

First workflow

With the stack up and the context saved, the cycle is always apply (creates a new version of the manifest) followed by run (dispatches the execution). run is synchronous by default and prints progress step by step:

apply + run
$ yggdrasil apply -f my-workflow.yaml
 created workflow default/my-workflow (version 1)

$ yggdrasil run my-workflow -n default --input target=prod
workflow status: succeeded
   resolve-config                       succeeded
   apply-objects                        succeeded  (kubernetes.apply)
   verify                               succeeded

Pass --input k=v as many times as you need; the values go through as strings and the workflow schema coerces the type on the server. Want to just see the diff before applying? yggdrasil apply -f … --dry-run. Want async dispatch? yggdrasil run … --async returns a run id to follow with yggdrasil logs and yggdrasil ops get.

Deploy the control plane

There's a dedicated shortcut to promote the control plane itself: yggdrasil deploy control-plane applies a control_plane manifest and then dispatches the seeded yggdrasil-deploy-control-plane workflow against it — syntactic sugar over apply + run that already encodes the Kubernetes and schema-migrations instances of the bootstrap topology.

deploy control-plane
$ yggdrasil deploy control-plane -f cp.yaml
 applied control_plane global/yggdrasil
→ dispatching workflow yggdrasil-deploy-control-plane
workflow status: succeeded

Use --no-run to apply the manifest without firing the workflow, and -f - to read from stdin. The instances used by the deploy can be overridden with --kubernetes-instance and --schema-migrations-instance.

From here, dig into the declarative model in Manifests, plug in your systems via Integrations and expose everything internally with Surfaces. It all keeps running on your infra.