Skip to main content
The composer generates Docker Compose YAML files from resolved service configurations, with support for single-file or multi-file layouts, security hardening, and platform-specific options.

Overview

The composer takes a ResolverOutput and generates complete Docker Compose configurations including:
  • Service definitions with proper dependency ordering
  • Volume and network declarations
  • Environment variable substitution
  • Health checks and restart policies
  • Security hardening (capability dropping, no-new-privileges)
  • GPU device reservations
  • Reverse proxy labels (Traefik)
  • Multi-file layouts with profiles

compose

Generates a single Docker Compose YAML string with all services.
import { compose } from '@better-openclaw/core';
import type { ResolverOutput, ComposeOptions } from '@better-openclaw/core';

const yaml: string = compose(resolved, {
  projectName: 'my-stack',
  proxy: 'caddy',
  domain: 'example.com',
  platform: 'linux/amd64',
  openclawVersion: 'latest',
  hardened: true
});

console.log(yaml);

Parameters

resolved
ResolverOutput
required
Output from the resolver containing validated services
options
ComposeOptions
required
Configuration options for Compose generation
options.projectName
string
required
Docker Compose project name
options.proxy
ProxyType
default:"none"
Reverse proxy type (“caddy”, “traefik”, “none”)
options.proxyHttpPort
number
Custom HTTP port for proxy (default: 80)
options.proxyHttpsPort
number
Custom HTTPS port for proxy (default: 443)
options.domain
string
Domain for reverse proxy routing
options.gpu
boolean
default:"false"
Enable NVIDIA GPU device reservations
options.platform
Platform
default:"linux/amd64"
Target platform (linux/amd64 or linux/arm64)
options.deployment
DeploymentTarget
default:"local"
Deployment target environment
options.openclawVersion
string
default:"latest"
OpenClaw gateway image version
options.bareMetalNativeHost
boolean
Add extra_hosts for native services on host
options.openclawImage
OpenclawImageVariant
default:"official"
OpenClaw image variant (“official”, “coolify”, “alpine”)
options.hardened
boolean
default:"true"
Apply security hardening (cap_drop, no-new-privileges)
options.openclawInstallMethod
OpenclawInstallMethod
default:"docker"
How to install OpenClaw (“docker” or “direct”)

Returns

Returns a complete Docker Compose YAML string ready to write to docker-compose.yml.

composeMultiFile

Generates multiple Docker Compose files with profile-based overrides by service category.
import { composeMultiFile } from '@better-openclaw/core';
import type { ComposeResult } from '@better-openclaw/core';

const result: ComposeResult = composeMultiFile(resolved, options);

console.log('Main file:', result.mainFile);
console.log('Files:', Object.keys(result.files));
console.log('Profiles:', result.profiles);

// Write files
for (const [filename, content] of Object.entries(result.files)) {
  await writeFile(filename, content);
}

Parameters

Same as compose() function.

Returns

files
Record<string, string>
Map of filename to YAML content
  • docker-compose.yml - Base file with core services
  • docker-compose.*.yml - Profile-specific override files
mainFile
string
Name of the main Compose file (always “docker-compose.yml”)
profiles
string[]
List of Docker Compose profiles used (e.g., [“ai”, “media”, “monitoring”])

Profile Mapping

Services are split into files based on category:
CategoryFileProfile
ai, ai-platformdocker-compose.ai.ymlai
mediadocker-compose.media.ymlmedia
monitoring, analyticsdocker-compose.monitoring.ymlmonitoring
dev-tools, coding-agentdocker-compose.tools.ymltools
social-mediadocker-compose.social.ymlsocial
knowledgedocker-compose.knowledge.ymlknowledge
communicationdocker-compose.communication.ymlcommunication
othersdocker-compose.yml(none)

Generated Structure

Service Definition

Each service gets:
services:
  redis:
    image: redis:7-alpine
    environment:
      REDIS_PASSWORD: ${REDIS_PASSWORD}
    ports:
      - "${REDIS_EXTERNAL_PORT:-6379}:6379"
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping"]
      interval: 30s
      timeout: 10s
      retries: 3
    restart: unless-stopped
    networks:
      - openclaw-network
    cap_drop:
      - ALL
    security_opt:
      - "no-new-privileges:true"

Gateway Services

When openclawInstallMethod: "docker":
services:
  openclaw-gateway:
    image: ${OPENCLAW_IMAGE:-ghcr.io/openclaw/openclaw:latest}
    environment:
      OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN}
      # ... all AI provider keys
    volumes:
      - ${OPENCLAW_CONFIG_DIR:-./openclaw/config}:/home/node/.openclaw
      - ${OPENCLAW_WORKSPACE_DIR:-./openclaw/workspace}:/home/node/.openclaw/workspace
    ports:
      - "${OPENCLAW_GATEWAY_PORT:-18789}:18789"
      - "${OPENCLAW_BRIDGE_PORT:-18790}:18790"
    networks:
      - openclaw-network
    restart: unless-stopped
    depends_on:
      postgresql: { condition: service_healthy }
      redis: { condition: service_started }
    command:
      - node
      - dist/index.js
      - gateway
      - --bind
      - ${OPENCLAW_GATEWAY_BIND:-lan}
      - --port
      - "18789"

  openclaw-cli:
    image: ${OPENCLAW_IMAGE:-ghcr.io/openclaw/openclaw:latest}
    environment:
      OPENCLAW_GATEWAY_HOST: openclaw-gateway
      OPENCLAW_GATEWAY_PORT: "18789"
      OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN}
    stdin_open: true
    tty: true
    networks:
      - openclaw-network
    depends_on:
      openclaw-gateway: { condition: service_started }
    entrypoint: ["node", "dist/index.js"]
    restart: "no"

PostgreSQL Init Container

Automatically added when services need dedicated databases:
services:
  postgres-setup:
    image: postgres:17-alpine
    depends_on:
      postgresql: { condition: service_healthy }
    environment:
      PGHOST: postgresql
      PGUSER: ${POSTGRES_USER:-openclaw}
      PGDATABASE: ${POSTGRES_DB:-openclaw}
      PGPASSWORD: ${POSTGRES_PASSWORD}
      N8N_DB_PASSWORD: ${N8N_DB_PASSWORD}
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        echo '=== PostgreSQL database setup ==='
        psql -c "CREATE ROLE n8n_user WITH LOGIN PASSWORD '$N8N_DB_PASSWORD'"
        psql -c "CREATE DATABASE n8n OWNER n8n_user"
        psql -c "GRANT ALL PRIVILEGES ON DATABASE n8n TO n8n_user"
        echo '=== All databases ready ==='
    restart: "no"
    networks:
      - openclaw-network

Security Hardening

When hardened: true (default):
  • cap_drop: ["ALL"] - Drop all Linux capabilities
  • security_opt: ["no-new-privileges:true"] - Prevent privilege escalation
  • cap_add - Re-add specific capabilities only when needed:
    • caddy, traefik: ["NET_BIND_SERVICE"]
    • crowdsec: ["NET_BIND_SERVICE", "DAC_READ_SEARCH"]
  • Memory limits set to 2x minMemoryMB as safe limit

GPU Support

When gpu: true and service has gpuRequired: true:
services:
  ollama:
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]

Port Overrides

Customize host ports via portOverrides:
const yaml = compose(resolved, {
  projectName: 'stack',
  portOverrides: {
    grafana: {
      3000: 3150  // Map host 3150 -> container 3000
    }
  }
});

Environment Variables

All service environment variables use ${VAR_NAME} substitution from .env file:
environment:
  POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
  POSTGRES_USER: ${POSTGRES_USER:-openclaw}
Port mappings use _EXTERNAL_PORT suffix to avoid conflicts:
ports:
  - "${GRAFANA_EXTERNAL_PORT:-3000}:3000"

Examples

Single File Generation

import { resolve, compose } from '@better-openclaw/core';

const resolved = resolve({
  services: ['redis', 'postgresql'],
  proxy: 'caddy',
  platform: 'linux/amd64'
});

const yaml = compose(resolved, {
  projectName: 'my-stack',
  proxy: 'caddy',
  domain: 'example.com',
  platform: 'linux/amd64',
  hardened: true
});

await writeFile('docker-compose.yml', yaml);

Multi-File with Profiles

import { composeMultiFile } from '@better-openclaw/core';

const result = composeMultiFile(resolved, options);

// Write all files
for (const [filename, content] of Object.entries(result.files)) {
  await writeFile(filename, content);
}

// Generate .env with profiles
const envContent = [
  `COMPOSE_FILE=${Object.keys(result.files).join(':')}`,
  `COMPOSE_PROFILES=${result.profiles.join(',')}`,
  // ... other vars
].join('\n');

await writeFile('.env', envContent);

Direct Install (No Docker Gateway)

const yaml = compose(resolved, {
  projectName: 'stack',
  openclawInstallMethod: 'direct',  // No gateway/CLI containers
  platform: 'linux/amd64'
});

// Result: only companion services (redis, postgresql, etc.)
// OpenClaw runs directly on host

Bare-Metal with Native Services

const yaml = compose(resolved, {
  projectName: 'stack',
  bareMetalNativeHost: true,  // Gateway can reach host services
  platform: 'linux/amd64'
});

// Gateway gets extra_hosts: ["host.docker.internal:host-gateway"]

Types

ComposeOptions

interface ComposeOptions {
  projectName: string;
  proxy?: ProxyType;
  proxyHttpPort?: number;
  proxyHttpsPort?: number;
  portOverrides?: Record<string, Record<string, number>>;
  domain?: string;
  gpu?: boolean;
  platform?: Platform;
  deployment?: DeploymentTarget;
  openclawVersion?: string;
  bareMetalNativeHost?: boolean;
  openclawImage?: OpenclawImageVariant;
  hardened?: boolean;
  openclawInstallMethod?: OpenclawInstallMethod;
}

ComposeResult

interface ComposeResult {
  files: Record<string, string>;  // filename -> YAML content
  mainFile: string;                // "docker-compose.yml"
  profiles: string[];              // ["ai", "media", ...]
}

See Also

Build docs developers (and LLMs) love