Skip to main content
The validator performs comprehensive validation of resolved service configurations and generated Docker Compose YAML to catch errors before deployment.

Overview

The validator runs multiple checks on a resolved stack configuration:
  • Port conflict detection
  • Volume name uniqueness
  • Environment variable completeness
  • Network consistency
  • Dependency graph validation (cycle detection)
  • YAML syntax validation
  • Domain format validation

validate

Validates a complete generated stack configuration.
import { validate } from '@better-openclaw/core';
import type { ValidationResult } from '@better-openclaw/core';

const result: ValidationResult = validate(
  resolved,
  composedYaml,
  {
    domain: 'example.com',
    generateSecrets: true
  }
);

if (!result.valid) {
  console.error('Validation errors:');
  result.errors.forEach(err => {
    console.error(`  [${err.type}] ${err.message}`);
  });
}

if (result.warnings.length > 0) {
  console.warn('Validation warnings:');
  result.warnings.forEach(warn => {
    console.warn(`  [${warn.type}] ${warn.message}`);
  });
}

Parameters

resolved
ResolverOutput
required
Resolved service configuration from the resolver
composedYaml
string
required
Generated Docker Compose YAML content to validate
options
object
Optional validation configuration
options.domain
string
Domain name to validate format
options.generateSecrets
boolean
default:"true"
Whether secrets are auto-generated (affects required env var checking)

Returns

valid
boolean
True if no errors were found (warnings don’t affect validity)
errors
ResolverError[]
Blocking validation errors that must be fixed
warnings
Warning[]
Non-blocking warnings that should be reviewed

Validation Checks

Port Conflict Detection

Ensures no two services expose the same host port.
// Error example:
{
  type: 'port_conflict',
  message: 'Port 8080 is used by both "grafana" and "n8n"'
}
Fix: Use portOverrides in generation input:
const input = {
  projectName: 'stack',
  services: ['grafana', 'n8n'],
  portOverrides: {
    grafana: { 3000: 3150 }  // Change host port
  }
};

Volume Uniqueness

Verifies volume names are not shared across different services (bind mounts excluded).
// Error example:
{
  type: 'volume_conflict',
  message: 'Volume name "data" is used by both "redis" and "postgresql"'
}

Environment Variable Completeness

Checks that required environment variables have defaults or will be generated.
// Error example:
{
  type: 'missing_env',
  message: 'Required environment variable "DATABASE_URL" for "n8n" has no default value'
}

// Warning example:
{
  type: 'secret_needed',
  message: 'Secret "POSTGRES_PASSWORD" for "PostgreSQL" needs to be configured manually'
}

Environment Variable Validation

Validates default values against regex patterns if specified.
// Warning example:
{
  type: 'env_validation',
  message: 'Environment variable "EMAIL" for "n8n" default value does not match validation pattern: ^[^@]+@[^@]+\.[^@]+$'
}

Network Consistency

Warns if services are not on the openclaw-network.
// Warning example:
{
  type: 'network',
  message: 'Service "custom-service" is not on openclaw-network — it may not be reachable from OpenClaw'
}

Dependency Graph Validation

Detects circular dependencies using depth-first search.
// Error example:
{
  type: 'cycle',
  message: 'Circular dependency detected involving "service-a"'
}

YAML Syntax Validation

Parses the generated YAML to ensure it’s syntactically valid.
// Error example:
{
  type: 'yaml_invalid',
  message: 'Generated YAML is not valid: Unexpected token at line 42'
}

Domain Format Validation

Validates domain name format when provided.
// Error example:
{
  type: 'invalid_domain',
  message: '"not_a_domain" is not a valid domain name'
}
Valid formats:
  • example.com
  • subdomain.example.com
  • my-site.co.uk
Invalid formats:
  • example (no TLD)
  • http://example.com (includes protocol)
  • example.com/path (includes path)
  • -example.com (starts with hyphen)

Types

ValidationResult

interface ValidationResult {
  valid: boolean;
  errors: ResolverError[];
  warnings: Warning[];
}

ResolverError

interface ResolverError {
  type: 'port_conflict' | 'volume_conflict' | 'missing_env' | 'cycle' | 'yaml_invalid' | 'invalid_domain';
  message: string;
}

Warning

interface Warning {
  type: 'secret_needed' | 'env_validation' | 'network';
  message: string;
}

Examples

Basic Validation

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

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

const yaml = compose(resolved, {
  projectName: 'my-stack',
  platform: 'linux/amd64'
});

const validation = validate(resolved, yaml);

if (!validation.valid) {
  throw new Error('Validation failed');
}

Handling Validation Errors

const validation = validate(resolved, yaml, {
  domain: 'example.com',
  generateSecrets: true
});

// Group errors by type
const errorsByType = validation.errors.reduce((acc, err) => {
  acc[err.type] = acc[err.type] || [];
  acc[err.type].push(err);
  return acc;
}, {} as Record<string, ResolverError[]>);

if (errorsByType.port_conflict) {
  console.error('Port conflicts detected:');
  errorsByType.port_conflict.forEach(err => {
    console.error(`  - ${err.message}`);
  });
}

Warning Collection

const validation = validate(resolved, yaml, { generateSecrets: false });

// Collect secrets that need manual configuration
const secretWarnings = validation.warnings.filter(w => w.type === 'secret_needed');

if (secretWarnings.length > 0) {
  console.warn('⚠️  The following secrets need manual configuration:');
  secretWarnings.forEach(w => {
    console.warn(`  - ${w.message}`);
  });
}

Integration with Generation Pipeline

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

try {
  const result = generate({
    projectName: 'my-stack',
    services: ['redis', 'postgresql', 'n8n'],
    proxy: 'caddy',
    domain: 'example.com',
    platform: 'linux/amd64',
    generateSecrets: true
  });
  
  // generate() calls validate() internally and throws ValidationError if invalid
  console.log('✅ Stack generated successfully');
  
} catch (error) {
  if (error instanceof ValidationError) {
    console.error('Validation failed:', error.message);
  }
}

Validation in CLI

The @better-openclaw/cli uses the validator to prevent generating invalid configurations:
# CLI validates before writing files
better-openclaw generate \
  --services redis,postgresql,n8n \
  --proxy caddy \
  --domain example.com

# Output on error:
# ❌ Validation failed:
#   [port_conflict] Port 8080 is used by both "grafana" and "n8n"
#   [invalid_domain] "not_a_domain" is not a valid domain name

Best Practices

  1. Always validate before deployment
    const validation = validate(resolved, yaml);
    if (!validation.valid) {
      throw new Error('Cannot deploy invalid configuration');
    }
    
  2. Don’t ignore warnings
    if (validation.warnings.length > 0) {
      console.warn('⚠️  Warnings detected — review before deploying:');
      validation.warnings.forEach(w => console.warn(`  - ${w.message}`));
    }
    
  3. Validate domain early
    // Validate domain before generating entire stack
    const validation = validate(resolved, '', { domain: userInput.domain });
    if (validation.errors.some(e => e.type === 'invalid_domain')) {
      throw new Error('Invalid domain format');
    }
    
  4. Use typed errors
    import { ValidationError } from '@better-openclaw/core';
    
    try {
      const result = generate(input);
    } catch (error) {
      if (error instanceof ValidationError) {
        // Handle validation-specific errors
      }
    }
    

See Also

  • Resolver API - Generates ResolverOutput for validation
  • Composer API - Generates Docker Compose YAML for validation
  • Schema API - Type definitions for errors and warnings

Build docs developers (and LLMs) love