This guide walks through transforming a plain docker-compose.yaml into a Composable Data Stack (CDS) profile with corresponding module definitions. It covers the complete journey from monolithic Docker Compose files to modular, reusable CDS components.
- docker-compose.yml: The source configuration that defines all services, their build contexts, environment variables, ports, volumes, and dependencies.
- Module: A reusable, self-contained component (e.g., PostgreSQL, Dagster) that abstracts docker-compose services and defines configuration schema, contracts, and implementation details.
- Profile: A composition of multiple module instances that describes a complete deployment stack, including module configurations, secrets management, and output contracts.
- Contract: An interface defining connection points between modules (e.g., SQL database contract specifies host, port, database, credentials).
name: local-dagster-postgres-superset
services:
postgres-postgres:
image: postgres:16
ports:
- 5432:5432
environment:
POSTGRES_DB: analytics
POSTGRES_USER: analytics
POSTGRES_PASSWORD: ${CDS_ANALYTICS_POSTGRES_PASSWORD}
volumes:
- postgres-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U analytics -d analytics"]
interval: 10s
timeout: 5s
retries: 10
dagster-user-code:
build:
context: .
dockerfile: images/dagster/Dockerfile-user-code
image: local/dagster-user-code:custom
restart: always
hostname: dagster-user-code
environment:
DAGSTER_HOME: /opt/dagster/dagster_home
DAGSTER_POSTGRES_HOST: postgres
DAGSTER_POSTGRES_PORT: 5432
DAGSTER_POSTGRES_DB: dagster
DAGSTER_POSTGRES_USER: dagster
DAGSTER_POSTGRES_PASSWORD: ${CDS_DAGSTER_POSTGRES_PASSWORD}
dagster-dagster-webserver:
image: local/dagster:custom
restart: always
ports:
- 3000:3000
depends_on:
dagster-user-code:
condition: service_healthy
command:
- bash
- -c
- dagster-webserver -h 0.0.0.0 -p 3000 -w workspace.yaml
environment:
DAGSTER_HOME: /opt/dagster/dagster_home
DAGSTER_POSTGRES_HOST: postgres
DAGSTER_POSTGRES_PORT: 5432
DAGSTER_POSTGRES_DB: dagster
DAGSTER_POSTGRES_USER: dagster
DAGSTER_POSTGRES_PASSWORD: ${CDS_DAGSTER_POSTGRES_PASSWORD}
dagster-dagster-daemon:
image: local/dagster:custom
restart: on-failure
depends_on:
dagster-user-code:
condition: service_healthy
command:
- bash
- -c
- dagster-daemon run
environment:
DAGSTER_HOME: /opt/dagster/dagster_home
DAGSTER_POSTGRES_HOST: postgres
DAGSTER_POSTGRES_PORT: 5432
DAGSTER_POSTGRES_DB: dagster
DAGSTER_POSTGRES_USER: dagster
DAGSTER_POSTGRES_PASSWORD: ${CDS_DAGSTER_POSTGRES_PASSWORD}
superset-superset:
image: apache/superset:6.1.0
ports:
- 8088:8088
environment:
SUPERSET_SECRET_KEY: ${CDS_SUPERSET_SECRET_KEY}
SUPERSET_ADMIN_USERNAME: admin
SUPERSET_ADMIN_PASSWORD: ${CDS_SUPERSET_ADMIN_PASSWORD}
SUPERSET_ADMIN_EMAIL: admin@example.local
SUPERSET_DATABASE_URI: postgresql://superset:${CDS_SUPERSET_POSTGRES_PASSWORD}@postgres:5432/superset
volumes:
postgres-postgres-data:Read each service and decide which platform capability it represents. Group services by their logical function.
| Docker Compose Service | Capability | CDS Module | Purpose |
|---|---|---|---|
| postgres-postgres | SQL database | warehouse/postgres | Data warehouse and storage |
| dagster-user-code | Job definitions | orchestration/dagster | Orchestration user code repository |
| dagster-dagster-webserver | Orchestration UI | orchestration/dagster | Orchestration webserver (UI + API) |
| dagster-dagster-daemon | Job scheduler | orchestration/dagster | Background scheduler and executor |
| superset-superset | Business intelligence | bi/superset | BI and visualization |
Key insight: Three docker-compose services (dagster-user-code, dagster-webserver, dagster-daemon) combine into one logical module because they work together as an orchestration system.
| Docker Compose Concept | CDS Concept | Purpose |
|---|---|---|
| A service block | Module implementation.compose.services entry |
Defines how the module's services run |
ports: host-side value |
config.<portName> in module configSchema |
Externally configurable port |
ports: container-side value |
runtime.service.ports[].containerPort |
Internal container listening port |
environment: non-secret values |
configSchema properties |
Typed, configurable parameters |
environment: secret values (${ENV_VAR}) |
spec.secrets.values.<name> |
Secret references (env variable name) |
| Hard-coded connection string | provides contract + consumes contract + contractRef |
Loose coupling via contracts |
depends_on: between different services |
spec.modules[].dependsOn in profile |
Inter-module dependencies |
depends_on: within same service group |
implementation.compose.services[].depends_on in module |
Intra-module service orchestration |
volumes: |
implementation.compose.volumes with enabledFrom |
Persistent storage with toggles |
healthcheck: |
implementation.compose.healthcheck with conditionallyEnabledFrom |
Health monitoring |
| Multi-service compose project | Profile (kind: Profile) |
Top-level composition file |
| Compose project name / network | spec.runtime.namespace |
Resource naming prefix |
| Which services are active | spec.modules[].enabled: true/false |
Enable/disable modules |
For each capability group, create a module.yaml file at modules/<layer>/<name>/module.yaml.
A module wraps the compose fragment and replaces hard-coded values with typed configuration properties.
apiVersion: cds/v1alpha1
kind: Module
metadata:
name: {module-name}
category: {layer}
version: "0.1.0"
displayName: {Human-Readable Name}
description: >
Purpose and configuration guidance.
spec:
runtime:
type: container
service:
name: {service-name}
ports:
- name: {port-name}
containerPort: {container-port}
protocol: TCP
configSchema:
type: object
additionalProperties: false
required: [required-keys]
properties:
{config-key}:
type: {json-schema-type}
description: Description
default: {default-value}
consumes:
- name: {contract-name}
contract:
kind: {contract-kind}
required: true
mappedFrom: "spec.config.{path}"
provides:
- name: {output-contract-name}
contract:
kind: {contract-kind}
spec:
{field}: "{value}"
implementation:
kind: docker-compose
compose:
services:
{service-name}:
image: {image}
environment:
VAR: "${config.key}"
# ... other config| Schema Field | Origin in docker-compose |
|---|---|
metadata.name |
Choose a short name matching the service (postgres, dagster, superset) |
metadata.category |
The capability layer (warehouse, orchestration, bi) |
metadata.version |
Start with "0.1.0"; this is the module's semver, separate from image tags |
metadata.displayName |
Human-readable label for UI tooling |
metadata.description |
Purpose and configuration guidance |
spec.runtime.type |
Always container for containerized services |
spec.runtime.service.name |
The docker-compose service key (e.g., postgres) |
spec.runtime.service.ports[].containerPort |
Right-hand side of compose ports mapping ("5432:5432" → 5432) |
spec.runtime.service.ports[].protocol |
TCP unless the service uses UDP |
spec.configSchema |
Extract values that vary between environments (passwords, db names, ports) into JSON Schema properties |
spec.consumes |
Contracts this module requires from other modules |
spec.provides |
Contracts this module exposes to other modules |
spec.implementation.kind |
docker-compose |
spec.implementation.compose |
The original compose fragment with hard-coded values replaced by ${config.*} and ${bindings.*} templates |
File: modules/warehouse/postgres/module.yaml
Original compose fragment:
postgres-postgres:
image: postgres:16
ports:
- 5432:5432
environment:
POSTGRES_DB: analytics
POSTGRES_USER: analytics
POSTGRES_PASSWORD: ${CDS_ANALYTICS_POSTGRES_PASSWORD}
volumes:
- postgres-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U analytics -d analytics"]
interval: 10s
timeout: 5s
retries: 10Transformation:
image: postgres:16→ stays as-isPOSTGRES_DB: analytics→ becomes${config.database}(value moves to profile)POSTGRES_PASSWORD: ${CDS_ANALYTICS_POSTGRES_PASSWORD}→ becomes${config.superuserPasswordFrom}(secret reference for the container's superuser password)ports: [5432:5432]→ host port becomes${config.port}; container port declared inruntime.service.portsvolumes: postgres-postgres-data:…→ captured inimplementation.compose.volumeshealthcheck:→ captured withconditionallyEnabledFromgate
Resulting module.yaml:
apiVersion: cds/v1alpha1
kind: Module
metadata:
name: postgres
category: warehouse
version: "0.1.0"
displayName: PostgreSQL
description: >
PostgreSQL database module providing sql-database contract.
spec:
runtime:
type: container
service:
name: postgres
ports:
- name: postgres
containerPort: 5432
protocol: TCP
configSchema:
type: object
additionalProperties: false
required:
- database
- username
- superuserPasswordFrom
- port
properties:
database:
type: string
minLength: 1
description: Name of the database to create
username:
type: string
minLength: 1
description: Database user for authentication
superuserPasswordFrom:
type: string
pattern: "^secrets\\.[a-zA-Z0-9_-]+$"
description: Reference to secret containing the database superuser password (POSTGRES_PASSWORD)
port:
type: integer
minimum: 1
maximum: 65535
default: 5432
description: Port the database listens on
storage:
type: object
properties:
enabled:
type: boolean
default: true
size:
type: string
default: 5Gi
healthcheck:
type: object
properties:
enabled:
type: boolean
default: true
provides:
- name: sql-database
contract:
kind: sql-database
spec:
host: postgres
port: "5432"
database: analytics
username: analytics
password: "${CDS_ANALYTICS_POSTGRES_PASSWORD}"
connectionUri: "postgresql://analytics:${CDS_ANALYTICS_POSTGRES_PASSWORD}@postgres:5432/analytics"
implementation:
kind: docker-compose
compose:
services:
postgres:
image: postgres:16
hostname: postgres
ports:
- "${config.port}:5432"
environment:
POSTGRES_DB: "${config.database}"
POSTGRES_USER: "${config.username}"
POSTGRES_PASSWORD: "${config.superuserPasswordFrom}"
volumes:
- postgres-postgres-data:/var/lib/postgresql/data
healthcheck:
conditionallyEnabledFrom: spec.config.healthcheck.enabled
test:
- CMD-SHELL
- pg_isready -U "${config.username}" -d "${config.database}"
interval: 10s
timeout: 5s
retries: 10
volumes:
postgres-postgres-data:File: modules/orchestration/dagster/module.yaml
Original compose fragments:
dagster-user-code:
image: local/dagster-user-code:custom
environment:
DAGSTER_POSTGRES_HOST: postgres
# ... other postgres vars
dagster-dagster-webserver:
image: local/dagster:custom
ports:
- 3000:3000
depends_on:
dagster-user-code:
condition: service_healthy
dagster-dagster-daemon:
image: local/dagster:custom
depends_on:
dagster-user-code:
condition: service_healthyKey transformation:
- Three services become one module because they work together as an orchestration system
- Connection strings become consumed contracts instead of hard-coded environment variables
- Internal dependencies (webserver depends on user-code) stay within the module's
implementation.compose.services - External dependencies (on postgres) become
spec.consumescontracts
Resulting module.yaml (excerpt):
apiVersion: cds/v1alpha1
kind: Module
metadata:
name: dagster
category: orchestration
version: "0.1.0"
displayName: Dagster
description: >
Dagster orchestration service with webserver and daemon.
spec:
runtime:
type: container
service:
name: dagster
ports:
- name: http
containerPort: 3000
protocol: TCP
configSchema:
type: object
additionalProperties: false
required:
- webPort
- homeDir
- storage
properties:
webPort:
type: integer
minimum: 1
maximum: 65535
default: 3000
homeDir:
type: string
default: /opt/dagster/dagster_home
daemon:
type: object
properties:
enabled:
type: boolean
default: true
storage:
type: object
required:
- runStorage
- eventLogStorage
- scheduleStorage
properties:
runStorage:
type: object
properties:
contractRef:
type: string
pattern: "^[a-z0-9-]+\\.[a-z0-9-]+$"
eventLogStorage:
type: object
properties:
contractRef:
type: string
pattern: "^[a-z0-9-]+\\.[a-z0-9-]+$"
scheduleStorage:
type: object
properties:
contractRef:
type: string
pattern: "^[a-z0-9-]+\\.[a-z0-9-]+$"
consumes:
- name: run-storage
contract:
kind: sql-database
required: true
mappedFrom: "spec.config.storage.runStorage"
- name: event-log-storage
contract:
kind: sql-database
required: true
mappedFrom: "spec.config.storage.eventLogStorage"
- name: schedule-storage
contract:
kind: sql-database
required: true
mappedFrom: "spec.config.storage.scheduleStorage"
provides:
- name: http-service
contract:
kind: http-service
spec:
protocol: http
host: dagster
port: "3000"
implementation:
kind: docker-compose
compose:
services:
user-code:
image: local/dagster-user-code:custom
environment:
DAGSTER_HOME: "${config.homeDir}"
DAGSTER_POSTGRES_HOST: "${bindings.run-storage.host}"
DAGSTER_POSTGRES_PORT: "${bindings.run-storage.port}"
DAGSTER_POSTGRES_DB: "${bindings.run-storage.database}"
DAGSTER_POSTGRES_USER: "${bindings.run-storage.username}"
DAGSTER_POSTGRES_PASSWORD: "${bindings.run-storage.password}"
dagster-webserver:
image: local/dagster:custom
ports:
- "${config.webPort}:3000"
depends_on:
user-code:
condition: service_healthy
environment:
DAGSTER_HOME: "${config.homeDir}"
DAGSTER_POSTGRES_HOST: "${bindings.run-storage.host}"
# ... other postgres bindings
dagster-daemon:
enabledFrom: spec.config.daemon.enabled
image: local/dagster:custom
depends_on:
user-code:
condition: service_healthy
environment:
DAGSTER_HOME: "${config.homeDir}"
DAGSTER_POSTGRES_HOST: "${bindings.run-storage.host}"
# ... other postgres bindingsImportant: The depends_on: user-code for webserver and daemon is an intra-module dependency (service to service within the same module). It stays in the module implementation. This is different from the profile-level dependsOn: [postgres] which is an inter-module dependency.
Contracts live at shared/contracts/<kind>.yaml. They describe the interface between a providing module and consuming modules.
Example: shared/contracts/sql-database.yaml
apiVersion: cds/v1alpha1
kind: Contract
metadata:
name: sql-database
category: shared
version: "0.1.0"
description: >
Shared contract describing values a SQL database provider exposes.
spec:
fields:
host:
type: string
required: true
description: Hostname or service name clients use to connect.
port:
type: integer
required: true
description: TCP port where the database listens.
database:
type: string
required: true
description: Database name to connect to.
username:
type: string
required: true
description: Username for authentication.
password:
type: string
required: true
description: Secret reference or runtime placeholder for password.
connectionUri:
type: string
required: true
description: Fully composed SQLAlchemy-style connection URI.
examples:
- host: postgres
port: 5432
database: analytics
username: analytics
password: ${CDS_ANALYTICS_POSTGRES_PASSWORD}
connectionUri: postgresql://analytics:${CDS_ANALYTICS_POSTGRES_PASSWORD}@postgres:5432/analyticsIn the original compose file, secrets came from shell environment variables:
${CDS_ANALYTICS_POSTGRES_PASSWORD}${CDS_DAGSTER_POSTGRES_PASSWORD}${CDS_SUPERSET_POSTGRES_PASSWORD}${CDS_SUPERSET_SECRET_KEY}${CDS_SUPERSET_ADMIN_PASSWORD}
Each becomes an entry in the profile's spec.secrets.values section.
| Secret Value Field | Purpose | Origin |
|---|---|---|
key (e.g., postgres_password) |
Logical name within CDS | Assigned for clarity |
env |
Actual environment variable name | From original docker-compose |
required |
Whether stack refuses to start without it | True for critical secrets |
Important: Inside module configs, passwordFrom: secrets.postgres_password is a pointer to the secret name, not the env-var name. The profile maps this to the actual environment variable.
The profile (profiles/<name>/profile.yaml) wires everything together, specifying:
- Which modules to instantiate
- How to configure each module
- Module dependencies
- Secret management
- Output contracts
apiVersion: cds/v1alpha1
kind: Profile
metadata:
name: {profile-name}
displayName: {Human-Readable Name}
description: >
Description of this deployment stack.
spec:
runtime:
type: docker-compose
namespace: {resource-prefix}
modules:
- id: {module-instance-id}
source: {path-to-module}
version: "{module-version}"
enabled: true
dependsOn:
- {other-module-id}
config:
{configuration-key}: {value}
secrets:
provider:
type: env
values:
{secret-name}:
env: {ENVIRONMENT_VARIABLE_NAME}
required: true
outputs:
contracts:
{output-name}:
from: {module-id}.{contract-name}| Profile Schema Field | What it Replaces in docker-compose |
|---|---|
metadata.name |
Compose project name |
spec.runtime.type |
Render target (docker-compose) |
spec.runtime.namespace |
Compose network/resource namespace |
spec.modules[].id |
Logical instance name; used in dependsOn and contractRef |
spec.modules[].source |
Path to module directory |
spec.modules[].version |
Must match metadata.version in module |
spec.modules[].enabled |
Replaces commenting services in/out |
spec.modules[].dependsOn |
Replaces depends_on: between services |
spec.modules[].config.* |
All hard-coded values from compose environment:, ports:, volumes: |
spec.secrets |
All ${ENV_VAR} references from compose, centralized |
spec.outputs.contracts |
Exports contracts for external consumption |
Instead of repeating connection strings:
# original compose (repeated 3 times!)
environment:
DAGSTER_RUN_STORAGE_POSTGRES_URL: postgresql://dagster:${CDS_DAGSTER_POSTGRES_PASSWORD}@postgres:5432/dagster
DAGSTER_EVENT_LOG_STORAGE_POSTGRES_URL: postgresql://dagster:${CDS_DAGSTER_POSTGRES_PASSWORD}@postgres:5432/dagster
DAGSTER_SCHEDULE_STORAGE_POSTGRES_URL: postgresql://dagster:${CDS_DAGSTER_POSTGRES_PASSWORD}@postgres:5432/dagsterThe profile simply wires the contract:
config:
storage:
runStorage:
contractRef: postgres.sql-database
eventLogStorage:
contractRef: postgres.sql-database
scheduleStorage:
contractRef: postgres.sql-databaseThe contractRef pattern is <module-id>.<contract-name>. During validate, the CLI checks that the producer module exists, provides the named contract, and declares the expected contract kind; failures produce E041 for an unknown module or contract and E042 for a kind mismatch. During plan, the CLI re-checks existence and merges the fields advertised by the provider into the consumer's resolved config. A validate-only run therefore confirms the binding but does not yet show the merged contract fields.
File: profiles/local-dagster-postgres-superset/profile.yaml
apiVersion: cds/v1alpha1
kind: Profile
metadata:
name: local-dagster-postgres-superset
displayName: Local Dagster + Postgres + Superset
description: >
Minimal local development stack for orchestration, warehouse, and BI.
spec:
runtime:
type: docker-compose
namespace: cds-local
modules:
# PostgreSQL storage backend
- id: postgres
source: ../../modules/warehouse/postgres
version: "0.1.0"
enabled: true
config:
database: analytics
username: analytics
passwordFrom: secrets.postgres_password
port: 5432
storage:
enabled: true
size: 5Gi
healthcheck:
enabled: true
# Dagster orchestration
- id: dagster
source: ../../modules/orchestration/dagster
version: "0.1.0"
enabled: true
dependsOn:
- postgres # ← Dagster module depends on Postgres module
config:
webPort: 3000
homeDir: /opt/dagster/dagster_home
daemon:
enabled: true
storage:
runStorage:
contractRef: postgres.sql-database
eventLogStorage:
contractRef: postgres.sql-database
scheduleStorage:
contractRef: postgres.sql-database
# Superset BI
- id: superset
source: ../../modules/bi/superset
version: "0.1.0"
enabled: true
dependsOn:
- postgres
config:
webPort: 8088
secretKeyFrom: secrets.superset_secret_key
adminUser:
username: admin
passwordFrom: secrets.superset_admin_password
email: admin@example.local
metadataDatabase:
contractRef: postgres.sql-database
secrets:
provider:
type: env
values:
postgres_password:
env: CDS_ANALYTICS_POSTGRES_PASSWORD
required: true
dagster_postgres_password:
env: CDS_DAGSTER_POSTGRES_PASSWORD
required: true
superset_postgres_password:
env: CDS_SUPERSET_POSTGRES_PASSWORD
required: true
superset_secret_key:
env: CDS_SUPERSET_SECRET_KEY
required: true
superset_admin_password:
env: CDS_SUPERSET_ADMIN_PASSWORD
required: true
outputs:
contracts:
warehouse:
from: postgres.sql-database
orchestrationApi:
from: dagster.http-service
biApi:
from: superset.http-serviceThis is a critical distinction when working with CDS modules.
Location: Defined in profile.yaml using modules[].dependsOn
Purpose: Specifies that one MODULE depends on another MODULE being healthy before starting
Example:
modules:
- id: dagster
dependsOn:
- postgres # ← dagster module depends on postgres moduleWhy: The dagster module needs postgres to be running because its services use environment variables that connect to postgres (DAGSTER_POSTGRES_HOST, etc.).
Docker-Compose Origin: Represents implicit dependencies through shared environment variables or database connections between different service groups.
Location: Defined in module.yaml under spec.implementation.compose.services[service].depends_on
Purpose: Specifies that one SERVICE within a MULTI-SERVICE MODULE depends on another service in the same module being healthy before starting
Example from dagster module:
implementation:
compose:
services:
user-code:
# ... first service (no dependencies)
dagster-webserver:
depends_on:
user-code: # ← webserver depends on user-code
condition: service_healthy
dagster-daemon:
depends_on:
user-code: # ← daemon depends on user-code
condition: service_healthyWhy: The Dagster webserver and daemon both require the user-code service to be running and healthy. The user-code service defines the Dagster assets and jobs.
Docker-Compose Origin: Maps directly from the original docker-compose depends_on clauses within service groups.
| Dependency Type | Location | Used For | Example |
|---|---|---|---|
| Inter-Module | profile.yaml modules[].dependsOn[] |
Module depends on another module | dagster depends on postgres |
| Intra-Module | module.yaml implementation.compose.services[].depends_on |
Service depends on another service within same module | dagster-webserver depends on dagster-user-code |
# In profiles/local-dagster-postgres-superset/profile.yaml (INTER-MODULE)
modules:
- id: postgres
# ... postgres module config
- id: dagster
dependsOn:
- postgres # ← INTER-MODULE: dagster module depends on postgres module
# In modules/orchestration/dagster/module.yaml (INTRA-MODULE)
spec:
implementation:
kind: docker-compose
compose:
services:
user-code:
# ... service definition (no dependencies)
dagster-webserver:
depends_on:
user-code: # ← INTRA-MODULE: webserver depends on user-code service
condition: service_healthy
dagster-daemon:
depends_on:
user-code: # ← INTRA-MODULE: daemon depends on user-code service
condition: service_healthyHow This Works:
- Profile says:
dagstermodule depends onpostgresmodule → postgres starts first (inter-module) - Dagster module starts with 3 services
- Module's implementation defines: webserver and daemon wait for user-code to be healthy (intra-module)
- Docker Compose starts user-code first, then starts webserver and daemon
Final Startup Sequence:
1. postgres-postgres (no dependencies)
2. dagster-user-code (waits for postgres via implicit env var dependency)
3. dagster-dagster-webserver (waits for dagster-user-code to be healthy)
4. dagster-dagster-daemon (waits for dagster-user-code to be healthy)
5. superset-superset (waits for postgres)
Once modules and profile are defined, use the CDS CLI to render a docker-compose.yml:
# Set required secrets
export CDS_ANALYTICS_POSTGRES_PASSWORD=your_analytics_password
export CDS_DAGSTER_POSTGRES_PASSWORD=your_dagster_password
export CDS_SUPERSET_POSTGRES_PASSWORD=your_superset_password
export CDS_SUPERSET_SECRET_KEY=your_secret
export CDS_SUPERSET_ADMIN_PASSWORD=admin_password
# Render the profile
cds render local-dagster-postgres-superset
# Or output to a specific file
cds render local-dagster-postgres-superset --output build/docker-compose.ymlThe renderer:
- Validates all modules and contracts
- Resolves dependencies
- Substitutes configuration values
- Resolves contract bindings
- Generates a complete, valid docker-compose.yml
A module should:
- Represent a single logical component or tool (e.g., "orchestration", "warehouse", "BI")
- Contain all services required for that component to function
- Have a clear configuration interface
- Define what it consumes (external contracts) and provides (output contracts)
- Configuration (
configSchema): Values that vary between deployments but are not sensitive (database names, port numbers, feature flags) - Secrets (
spec.secrets): Sensitive values that should never be committed (passwords, API keys, secret keys)
Configuration is baked into the rendered compose file or environment variables. Secrets are referenced as Docker Compose runtime placeholders (${ENV_VAR}).
Contracts should:
- Be minimal and focused (e.g.,
sql-databaseexposes connection details;http-serviceexposes protocol, host, port) - Be reusable across many modules
- Hide implementation details
- Document all required fields
Modules become more valuable as:
- Configuration options grow (more flexibility)
- Contracts are well-defined (clearer interfaces)
- Implementation details are factored out
- Common patterns emerge across deployments
| Stage | Deliverable | Purpose |
|---|---|---|
| 1. Analysis | Service inventory | Map services to capabilities |
| 2. Modules | modules/*/module.yaml |
Define reusable components |
| 3. Contracts | shared/contracts/*.yaml |
Define module interfaces |
| 4. Profile | profiles/*/profile.yaml |
Compose modules into a stack |
| 5. Render | docker-compose.yml |
Generate deployment configuration |
The result is a maintainable, reusable, and composable infrastructure-as-code that separates concerns and promotes modularity.