How generation works
ignition-stack create resolves a project configuration into a writable tree on disk. The compose generation engine sits at the heart of that pipeline.
The pipeline
- The CLI builds a
ProjectConfig(name, gateways, database, services, network topology, modules). - The dependency resolver expands implicit needs (see below) into a fully-resolved config.
- The writer copies the static asset tree (
scripts/docker-bootstrap.sh, per-gatewayservices/<name>/resource directories) and overlays each service's seeds. - The compose engine renders
docker-compose.yamlfrom a static anchor header, per-service Jinja2 fragments, and a footer that declares volumes (and networks when the split is opted into). - The writer renders
.envfrom the resolved config, including per-gateway HTTP-port keys for multi-gateway projects and each service's preset credentials.
The service catalog
Every supported service is a self-contained directory under templates/services/<name>/:
manifest.yamldeclares the image (and the.envkey that overrides it), the capabilities the serviceprovidesandrequires, its preset.envcredentials, and which connections it cannot file-seed and defers toPOST-SETUP.md.compose.yaml.j2is the service's compose fragment, rendered with a small context (image reference, container name, networks, dependencies).seed/service/is copied intoservices/<name>/and mounted into the service's own container (a Postgres initdb script, a Keycloak realm export, a broker config).seed/gateway-resources/is overlaid onto every gateway'sconfig/resources/tree, so a service can pre-seed a file-seedable gateway connection. Postgres uses this to ship a workingdb-connectionplus theinternal-secret-providerthat holds its password. What is file-seedable on Ignition 8.3 is recorded in the seeding matrix.
Adding a service is a data change: drop a new directory in, and the engine, the .env writer, and the seed copier pick it up with no code change.
The dependency resolver
services.resolver.resolve() is a pure transformation run before anything renders. It expands two kinds of rule:
- Declarative capability chains from each manifest's
requires. Keycloakrequires: [sql-database], so selecting Keycloak with no database auto-adds Postgres (and a dedicatedkeycloaklogical database created on first init). - Imperative couplings: a MySQL database attaches the
mysql-jdbcdriver to every gateway so the connector.jarlands inuser-lib/jdbc/.
The engine renders the resolved config verbatim and never adds or re-resolves services at render time, which keeps the resolution rules testable in isolation.
Why a hybrid engine
A pure Jinja2 approach makes nested YAML indentation a debugging nightmare. A pure dict-builder loses the hand-written feel maintainers need to read and tweak the generated compose file. The hybrid engine reads a static header that holds the YAML anchors (x-ignition-common, x-ignition-environment), renders each service as a Jinja2 fragment, concatenates them under services:, then round-trips the whole document through ruamel.yaml.
Anchors survive because the entire document is parsed in one context: the alias *ignition-common in a gateway fragment resolves to the anchor &ignition-common defined earlier in the same string.
Network topology
By default every service shares the implicit project bridge (no per-service networks: block). Opting into network_split=True declares two top-level networks - frontend and backend - and routes services:
dband brokers:backendonly.- Ignition gateways: both networks (they need DB reach AND user-facing UI exposure).
bootstrap-*init containers: no network membership; they only write into the data volume.
Per-gateway env overrides
The x-ignition-environment anchor carries the project-wide defaults (ACCEPT_IGNITION_EULA: "Y", IGNITION_EDITION: standard, admin credentials, TZ). A gateway that needs a different value (e.g. Edge for a frontend role) inserts an override key after the merge-key reference:
environment:
<<: *ignition-environment
IGNITION_EDITION: edge
Modules attached to a gateway append ACCEPT_MODULE_LICENSES and ACCEPT_MODULE_CERTS as comma-separated fully-qualified module identifiers. GATEWAY_MODULES_ENABLED is a strict whitelist emitted only when a built-in is disabled (see disable built-in modules), so stacks without disable_builtins never carry it. The engine looks each module up in modules.yaml and rejects unknown slugs at render time.
Golden tests
Every supported combination has a golden snapshot under tests/golden/. Engine changes that affect output text fail the test with a unified diff; an intentional change is committed by re-running the suite with UPDATE_GOLDENS=1.
The goldens cover:
architectures/<slug>/- one snapshot per architecture (basic,scale-out,hub-and-spoke,scale-out-redundant).services/<name>/- one minimal snapshot per catalog service (and per database kind).combos/- key combinations: the smoke stack (Postgres + HiveMQ + OPC-UA-sim) and a network-split stack.
The multi-service docker compose up smoke lives behind a smoke pytest marker (pytest -m smoke); the default run stays fast.