Docker Compose Integration
The Docker adapter provides seamless integration with Docker Compose for multi-container orchestration. This enables complex application deployments with service dependencies, networking, and volume management.
Basic Compose Operations
Starting Services
Launch multi-container applications with Docker Compose:
import { DockerAdapter } from '@xec-sh/core';
const docker = new DockerAdapter();
// Start services from default docker-compose.yml
await docker.composeUp();
// Start with specific compose file
await docker.composeUp({
file: 'docker-compose.prod.yml'
});
// Start with multiple compose files
await docker.composeUp({
file: ['docker-compose.yml', 'docker-compose.override.yml', 'docker-compose.local.yml']
});
// Start with custom project name
await docker.composeUp({
projectName: 'myapp-staging',
file: 'docker-compose.staging.yml'
});
Service Management
Control individual services and complete stacks:
// Stop all services
await docker.composeDown();
// Stop with specific options
await docker.composeDown({
file: 'docker-compose.prod.yml',
projectName: 'production'
});
// Check service status
const status = await docker.composePs();
console.log('Service status:');
console.log(status);
// Get logs from all services
const logs = await docker.composeLogs();
console.log('All service logs:');
console.log(logs);
// Get logs from specific service
const webLogs = await docker.composeLogs('web');
console.log('Web service logs:');
console.log(webLogs);
Environment Configuration
Environment Variables
Pass environment variables to Compose operations:
// Set environment for compose operations
await docker.composeUp({
file: 'docker-compose.yml',
env: {
NODE_ENV: 'production',
DATABASE_URL: 'postgresql://prod-db:5432/myapp',
REDIS_URL: 'redis://cache:6379',
SECRET_KEY: process.env.SECRET_KEY
}
});
// Environment-specific compose files
const environments = {
development: {
file: 'docker-compose.dev.yml',
env: {
NODE_ENV: 'development',
DEBUG: '*',
HOT_RELOAD: 'true'
}
},
staging: {
file: 'docker-compose.staging.yml',
env: {
NODE_ENV: 'staging',
API_URL: 'https://api-staging.example.com'
}
},
production: {
file: 'docker-compose.prod.yml',
env: {
NODE_ENV: 'production',
API_URL: 'https://api.example.com'
}
}
};
const deployToEnvironment = async (env) => {
const config = environments[env];
if (!config) {
throw new Error(`Unknown environment: ${env}`);
}
console.log(`Deploying to ${env}...`);
await docker.composeUp({
file: config.file,
env: config.env,
projectName: `myapp-${env}`
});
};
await deployToEnvironment('staging');
Service Execution
Execute Commands in Services
Run commands within running Compose services:
// Execute command in specific service container
const webContainer = 'myapp_web_1'; // Compose naming convention
// Run database migration
await $({
adapterOptions: {
type: 'docker',
container: webContainer
}
})`npm run migrate`;
// Run tests in isolated container
await $({
adapterOptions: {
type: 'docker',
container: webContainer,
workdir: '/app/tests'
}
})`npm test`;
// Execute shell commands
await $({
adapterOptions: {
type: 'docker',
container: webContainer,
tty: true
}
})`/bin/sh -c "ls -la /app && ps aux"`;
Interactive Service Commands
Run interactive commands in services:
// Interactive shell in web service
const webShell = async () => {
return $({
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr,
adapterOptions: {
type: 'docker',
container: 'myapp_web_1',
tty: true
}
})`/bin/bash`;
};
// Start interactive session (when needed)
// await webShell();
// Run interactive database client
await $({
adapterOptions: {
type: 'docker',
container: 'myapp_db_1',
tty: true
}
})`psql -U postgres -d myapp`;
Multi-Service Orchestration
Complex Application Stack
Deploy and manage complex multi-service applications:
class ComposeApplication {
constructor(environment = 'development') {
this.docker = new DockerAdapter();
this.environment = environment;
this.config = this.getEnvironmentConfig();
}
getEnvironmentConfig() {
const configs = {
development: {
file: ['docker-compose.yml', 'docker-compose.dev.yml'],
projectName: 'myapp-dev',
env: {
NODE_ENV: 'development',
LOG_LEVEL: 'debug',
HOT_RELOAD: 'true'
}
},
production: {
file: ['docker-compose.yml', 'docker-compose.prod.yml'],
projectName: 'myapp-prod',
env: {
NODE_ENV: 'production',
LOG_LEVEL: 'info'
}
}
};
return configs[this.environment];
}
async deploy() {
console.log(`Deploying ${this.environment} environment...`);
try {
// Pull latest images
await this.pullImages();
// Start services
await this.docker.composeUp(this.config);
// Wait for services to be ready
await this.waitForServices();
// Run post-deployment tasks
await this.postDeploy();
console.log('Deployment completed successfully');
} catch (error) {
console.error('Deployment failed:', error.message);
throw error;
}
}
async pullImages() {
console.log('Pulling latest images...');
// Get list of services and their images
const composeConfig = await this.getComposeConfig();
const services = Object.keys(composeConfig.services || {});
for (const service of services) {
const image = composeConfig.services[service].image;
if (image) {
try {
await this.docker.pullImage(image);
console.log(`Pulled ${image}`);
} catch (error) {
console.warn(`Failed to pull ${image}:`, error.message);
}
}
}
}
async getComposeConfig() {
// Parse compose configuration
const configResult = await this.docker.executeDockerCommand([
'compose',
...this.buildComposeFileArgs(),
'config'
], {});
return JSON.parse(configResult.stdout);
}
buildComposeFileArgs() {
const args = [];
if (this.config.file) {
const files = Array.isArray(this.config.file) ? this.config.file : [this.config.file];
for (const file of files) {
args.push('-f', file);
}
}
if (this.config.projectName) {
args.push('-p', this.config.projectName);
}
return args;
}
async waitForServices() {
console.log('Waiting for services to be ready...');
const services = await this.getServiceContainers();
for (const [serviceName, containerName] of Object.entries(services)) {
console.log(`Waiting for ${serviceName} (${containerName})...`);
try {
await this.docker.waitForHealthy(containerName, 60000);
console.log(`${serviceName} is ready`);
} catch (error) {
console.warn(`${serviceName} health check timeout, continuing...`);
}
}
}
async getServiceContainers() {
const psOutput = await this.docker.composePs(this.config);
const lines = psOutput.trim().split('\n');
const services = {};
// Parse docker-compose ps output
for (let i = 1; i < lines.length; i++) { // Skip header
const parts = lines[i].trim().split(/\s+/);
if (parts.length >= 2) {
const containerName = parts[0];
const serviceName = containerName.split('_')[1]; // Extract service name
services[serviceName] = containerName;
}
}
return services;
}
async postDeploy() {
console.log('Running post-deployment tasks...');
const services = await this.getServiceContainers();
// Run database migrations if web service exists
if (services.web) {
try {
await $({
adapterOptions: {
type: 'docker',
container: services.web
}
})`npm run migrate`;
console.log('Database migration completed');
} catch (error) {
console.warn('Migration failed:', error.message);
}
}
// Seed initial data if needed
if (services.web && this.environment === 'development') {
try {
await $({
adapterOptions: {
type: 'docker',
container: services.web
}
})`npm run seed`;
console.log('Database seeding completed');
} catch (error) {
console.warn('Seeding failed:', error.message);
}
}
// Clear cache if cache service exists
if (services.cache) {
try {
await $({
adapterOptions: {
type: 'docker',
container: services.cache
}
})`redis-cli FLUSHALL`;
console.log('Cache cleared');
} catch (error) {
console.warn('Cache clear failed:', error.message);
}
}
}
async getServiceLogs(serviceName, options = {}) {
return await this.docker.composeLogs(serviceName, {
...this.config,
...options
});
}
async restart(serviceName = null) {
if (serviceName) {
// Restart specific service
await this.docker.executeDockerCommand([
'compose',
...this.buildComposeFileArgs(),
'restart',
serviceName
], {});
} else {
// Restart all services
await this.docker.composeDown(this.config);
await this.docker.composeUp(this.config);
}
}
async scale(serviceName, instances) {
await this.docker.executeDockerCommand([
'compose',
...this.buildComposeFileArgs(),
'up',
'-d',
'--scale',
`${serviceName}=${instances}`
], {});
}
async cleanup() {
console.log('Cleaning up services...');
await this.docker.composeDown(this.config);
}
}
// Usage
const app = new ComposeApplication('development');
await app.deploy();
// Get service logs
const webLogs = await app.getServiceLogs('web', { tail: 100 });
console.log('Recent web service logs:', webLogs);
// Scale service
await app.scale('worker', 3);
// Restart specific service
await app.restart('web');
// Cleanup on exit
process.on('SIGINT', async () => {
await app.cleanup();
process.exit(0);
});
Service Dependencies
Handling Service Dependencies
Manage service startup order and dependencies:
// Wait for dependent services before starting main application
const waitForDependencies = async () => {
console.log('Starting dependency services...');
// Start database first
await docker.executeDockerCommand([
'compose', 'up', '-d', 'database'
], {});
// Wait for database to be ready
await docker.waitForHealthy('myapp_database_1', 30000);
// Start cache service
await docker.executeDockerCommand([
'compose', 'up', '-d', 'cache'
], {});
// Wait for cache to be ready
await docker.waitForHealthy('myapp_cache_1', 15000);
// Finally start the web application
await docker.executeDockerCommand([
'compose', 'up', '-d', 'web'
], {});
console.log('All services started successfully');
};
await waitForDependencies();
// Check service connectivity
const testServiceConnectivity = async () => {
const services = await docker.getServiceContainers();
// Test database connection from web service
try {
await $({
adapterOptions: {
type: 'docker',
container: services.web
}
})`nc -z database 5432`;
console.log('Database connectivity: OK');
} catch (error) {
console.error('Database connectivity: FAILED');
}
// Test cache connection from web service
try {
await $({
adapterOptions: {
type: 'docker',
container: services.web
}
})`nc -z cache 6379`;
console.log('Cache connectivity: OK');
} catch (error) {
console.error('Cache connectivity: FAILED');
}
};
await testServiceConnectivity();
Advanced Compose Operations
Blue-Green Deployment
Implement zero-downtime deployments with Compose:
class BlueGreenDeployment {
constructor() {
this.docker = new DockerAdapter();
this.activeColor = 'blue';
}
async deploy(newVersion) {
const inactiveColor = this.activeColor === 'blue' ? 'green' : 'blue';
console.log(`Deploying ${newVersion} to ${inactiveColor} environment...`);
try {
// Deploy to inactive environment
await this.deployToEnvironment(inactiveColor, newVersion);
// Test inactive environment
await this.testEnvironment(inactiveColor);
// Switch traffic to new environment
await this.switchTraffic(inactiveColor);
// Clean up old environment
await this.cleanupEnvironment(this.activeColor);
this.activeColor = inactiveColor;
console.log(`Deployment complete. Active environment: ${this.activeColor}`);
} catch (error) {
console.error('Deployment failed:', error.message);
await this.cleanupEnvironment(inactiveColor);
throw error;
}
}
async deployToEnvironment(color, version) {
const config = {
file: `docker-compose.${color}.yml`,
projectName: `myapp-${color}`,
env: {
VERSION: version,
ENVIRONMENT: color
}
};
await this.docker.composeUp(config);
// Wait for services to be ready
const services = await this.getServiceContainers(color);
for (const [serviceName, containerName] of Object.entries(services)) {
await this.docker.waitForHealthy(containerName, 60000);
}
}
async testEnvironment(color) {
console.log(`Testing ${color} environment...`);
const services = await this.getServiceContainers(color);
// Run health checks
const healthTests = [
{
name: 'API Health Check',
command: `curl -f http://localhost:8080/health`,
container: services.web
},
{
name: 'Database Connection',
command: `pg_isready -h database -U postgres`,
container: services.web
}
];
for (const test of healthTests) {
try {
await $({
adapterOptions: {
type: 'docker',
container: test.container
}
})`${test.command}`;
console.log(`✓ ${test.name} passed`);
} catch (error) {
throw new Error(`${test.name} failed: ${error.message}`);
}
}
}
async switchTraffic(newColor) {
console.log(`Switching traffic to ${newColor}...`);
// Update load balancer configuration
await $({
adapterOptions: {
type: 'docker',
container: 'load-balancer'
}
})`
sed -i 's/upstream app-blue/upstream app-${newColor}/g' /etc/nginx/nginx.conf &&
nginx -s reload
`;
}
async cleanupEnvironment(color) {
console.log(`Cleaning up ${color} environment...`);
await this.docker.composeDown({
file: `docker-compose.${color}.yml`,
projectName: `myapp-${color}`
});
}
async getServiceContainers(color) {
const psOutput = await this.docker.composePs({
file: `docker-compose.${color}.yml`,
projectName: `myapp-${color}`
});
// Parse and return service containers
// Implementation similar to previous example
return {};
}
}
// Usage
const deployment = new BlueGreenDeployment();
await deployment.deploy('v2.1.0');
Service Scaling and Load Balancing
Scale services dynamically based on load:
class ServiceScaler {
constructor() {
this.docker = new DockerAdapter();
this.config = {
file: 'docker-compose.yml',
projectName: 'myapp'
};
}
async monitorAndScale() {
console.log('Starting service monitoring and auto-scaling...');
setInterval(async () => {
try {
await this.checkAndScale();
} catch (error) {
console.error('Scaling check failed:', error.message);
}
}, 30000); // Check every 30 seconds
}
async checkAndScale() {
const services = await this.getServiceContainers();
for (const [serviceName, containers] of Object.entries(services)) {
if (serviceName === 'web') { // Only scale web service
const avgCpuUsage = await this.getAverageServiceCpuUsage(containers);
const currentInstances = containers.length;
console.log(`Service ${serviceName}: ${currentInstances} instances, ${avgCpuUsage}% CPU`);
if (avgCpuUsage > 80 && currentInstances < 5) {
// Scale up
await this.scaleService(serviceName, currentInstances + 1);
console.log(`Scaled up ${serviceName} to ${currentInstances + 1} instances`);
} else if (avgCpuUsage < 20 && currentInstances > 1) {
// Scale down
await this.scaleService(serviceName, currentInstances - 1);
console.log(`Scaled down ${serviceName} to ${currentInstances - 1} instances`);
}
}
}
}
async getAverageServiceCpuUsage(containers) {
let totalCpu = 0;
let validContainers = 0;
for (const container of containers) {
try {
const stats = await this.docker.getStats(container);
const cpuPercent = this.calculateCpuPercent(stats);
totalCpu += cpuPercent;
validContainers++;
} catch (error) {
console.warn(`Failed to get stats for ${container}:`, error.message);
}
}
return validContainers > 0 ? totalCpu / validContainers : 0;
}
calculateCpuPercent(stats) {
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
if (systemDelta > 0 && cpuDelta > 0) {
return (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100;
}
return 0;
}
async scaleService(serviceName, instances) {
await this.docker.executeDockerCommand([
'compose',
'-f', this.config.file,
'-p', this.config.projectName,
'up',
'-d',
'--scale',
`${serviceName}=${instances}`,
'--no-recreate'
], {});
}
async getServiceContainers() {
const psOutput = await this.docker.composePs(this.config);
const lines = psOutput.trim().split('\n');
const services = {};
for (let i = 1; i < lines.length; i++) {
const parts = lines[i].trim().split(/\s+/);
if (parts.length >= 2) {
const containerName = parts[0];
const serviceName = containerName.split('_')[1];
if (!services[serviceName]) {
services[serviceName] = [];
}
services[serviceName].push(containerName);
}
}
return services;
}
}
// Usage
const scaler = new ServiceScaler();
await scaler.monitorAndScale();
Troubleshooting Compose
Common Issues and Solutions
Handle common Docker Compose issues:
// Debug compose configuration
const debugCompose = async () => {
try {
// Validate compose file
const configResult = await docker.executeDockerCommand([
'compose', 'config'
], {});
console.log('Compose configuration is valid');
console.log(configResult.stdout);
} catch (error) {
console.error('Compose configuration error:', error.message);
}
// Check service dependencies
const psOutput = await docker.composePs({ projectName: 'myapp' });
console.log('Current service status:');
console.log(psOutput);
// Get detailed service information
const services = await docker.getServiceContainers();
for (const [serviceName, containerName] of Object.entries(services)) {
try {
const info = await docker.inspectContainer(containerName);
console.log(`\n${serviceName} (${containerName}):`);
console.log(`- Status: ${info.State.Status}`);
console.log(`- Health: ${info.State.Health?.Status || 'N/A'}`);
console.log(`- Restart Count: ${info.RestartCount}`);
if (info.State.ExitCode !== 0) {
console.log(`- Exit Code: ${info.State.ExitCode}`);
const logs = await docker.getLogs(containerName, { tail: 50 });
console.log(`- Recent logs:\n${logs}`);
}
} catch (error) {
console.error(`Failed to inspect ${containerName}:`, error.message);
}
}
};
await debugCompose();
// Recreate problematic services
const recreateService = async (serviceName) => {
console.log(`Recreating service: ${serviceName}`);
try {
// Stop and remove the service
await docker.executeDockerCommand([
'compose', 'rm', '-f', '-s', serviceName
], {});
// Start the service again
await docker.executeDockerCommand([
'compose', 'up', '-d', serviceName
], {});
console.log(`Successfully recreated ${serviceName}`);
} catch (error) {
console.error(`Failed to recreate ${serviceName}:`, error.message);
}
};
// await recreateService('web');
This comprehensive Docker Compose integration provides the tools needed for complex multi-container orchestration, from basic service management to advanced deployment patterns like blue-green deployments and auto-scaling.