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 service configuration from the resolver
Generated Docker Compose YAML content to validate
Optional validation configuration
Domain name to validate format
Whether secrets are auto-generated (affects required env var checking)
Returns
True if no errors were found (warnings don’t affect validity)
Blocking validation errors that must be fixed
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
-
Always validate before deployment
const validation = validate(resolved, yaml);
if (!validation.valid) {
throw new Error('Cannot deploy invalid configuration');
}
-
Don’t ignore warnings
if (validation.warnings.length > 0) {
console.warn('⚠️ Warnings detected — review before deploying:');
validation.warnings.forEach(w => console.warn(` - ${w.message}`));
}
-
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');
}
-
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