Last updated: Aug 12, 2025, 01:09 PM UTC

Docker Deployment Architecture

Last Updated: 2025-08-09

Overview

This document describes the Docker deployment architecture for Sasha Studio, including Alpine Linux compatibility, persistent storage, and Claude CLI integration.

Container Architecture

Base Image Selection

Current: node:20-alpine

  • Pros: Small size (~50MB base), fast builds, minimal attack surface
  • Cons: musl libc compatibility issues, requires special handling for child processes

Alternative: node:20 (Debian-based)

  • Pros: Better compatibility, glibc support, fewer edge cases
  • Cons: Larger size (~300MB base), slower builds

Multi-Stage Build

# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --include=dev

# Stage 2: Builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Stage 3: Production
FROM node:20-alpine AS runner
WORKDIR /app
# Copy built assets and dependencies
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules

Alpine Linux Compatibility

Child Process Execution

Problem: child_process.spawn() fails in Alpine with ENOENT errors due to musl libc differences.

Solution: Use execFile() for reliable execution:

import { execFile } from 'child_process';

if (process.env.RUNNING_IN_DOCKER === 'true') {
  // Use execFile in Docker Alpine
  claudeProcess = execFile('/usr/local/bin/node', ['/usr/local/bin/claude', ...args], options);
} else {
  // Standard spawn for non-Docker
  claudeProcess = spawn('claude', args, options);
}

Required Alpine Packages

RUN apk add --no-cache \
    python3 \           # Build dependencies
    make \
    g++ \
    gcc \
    libc-dev \
    linux-headers \
    bash \              # Better shell support
    git \               # Version control
    openssh-client \    # SSH operations
    dumb-init \         # Proper signal handling
    curl                # HTTP operations

Persistent Storage Architecture

Volume Configuration

volumes:
  sasha-config:
    driver: local
  sasha-data:
    driver: local

services:
  sasha-studio:
    volumes:
      - sasha-config:/app/config    # API keys, settings
      - sasha-data:/app/data        # SQLite database
      - ./docs:/app/docs            # Documentation (bind mount for dev)

Directory Structure

/app/
β”œβ”€β”€ config/              # Persistent configuration
β”‚   β”œβ”€β”€ .env            # API keys and secrets
β”‚   └── .claude-config.json  # Claude CLI config
β”œβ”€β”€ data/               # Persistent data
β”‚   └── sasha.db       # SQLite database
β”œβ”€β”€ workspaces/         # Project workspaces
β”œβ”€β”€ uploads/            # Uploaded files
β”œβ”€β”€ .tmp/               # Temporary files
└── docs/               # Documentation

API Key Management

Secure Storage Flow

  1. Initial Configuration:

    • User provides API key during onboarding
    • Server saves to /app/config/.env
    • Environment variable set for current process
  2. Container Restart:

    • Server loads from /app/config/.env on startup
    • API key available to Claude CLI via environment
  3. Implementation:

// Load API key from persistent storage
const configDir = process.env.RUNNING_IN_DOCKER === 'true' 
  ? '/app/config' 
  : path.join(__dirname, '..');

const envPath = path.join(configDir, '.env');
if (fs.existsSync(envPath)) {
  dotenv.config({ path: envPath });
  console.log('πŸ”‘ API key loaded from persistent storage');
}

Claude CLI Integration

Installation

# Install Claude CLI globally
RUN --mount=type=cache,target=/root/.npm \
    npm install -g @anthropic-ai/claude-code@latest

Workspace Handling

Problem: Relative paths like default/workspace fail in Docker.

Solution: Convert to absolute paths:

let workingDir = cwd || process.cwd();

if (!workingDir.startsWith('/')) {
  if (process.env.RUNNING_IN_DOCKER === 'true') {
    workingDir = `/app/workspaces/${workingDir}`;
  } else {
    workingDir = path.resolve(workingDir);
  }
}

// Ensure directory exists
await fs.mkdir(workingDir, { recursive: true });

Security Considerations

User Permissions

# Create non-root user
RUN addgroup -g 1001 nodejs && \
    adduser -S -u 1001 -G nodejs nodejs

# Set ownership
RUN chown -R nodejs:nodejs /app

# Run as non-root
USER nodejs

Directory Permissions

# Secure permissions
RUN chmod -R 755 /app && \
    chmod 777 /app/data && \      # Database writes
    chmod 777 /app/uploads && \   # File uploads
    chmod 777 /app/.tmp && \      # Temporary files
    chmod 777 /app/config         # Configuration updates

Health Checks

Dynamic Port Configuration

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:' + process.env.PORT + '/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" || exit 1

Health Endpoint

app.get('/api/health', (req, res) => {
  res.json({
    status: 'healthy',
    timestamp: new Date().toISOString(),
    claudeCliAvailable: !!process.env.ANTHROPIC_API_KEY
  });
});

Environment Variables

Required Variables

environment:
  - NODE_ENV=production
  - RUNNING_IN_DOCKER=true
  - PORT=3005
  - CLAUDE_HOME=/home/nodejs/.claude
  - CLAUDE_PROJECTS_PATH=/app/workspaces

Optional Variables

environment:
  - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}  # Override from host
  - DEBUG=${DEBUG:-}                           # Enable debug logging

Deployment Commands

Development

# Build and start
docker compose -f docker-compose.test.yml up --build

# View logs
docker compose -f docker-compose.test.yml logs -f

# Shell access
docker compose -f docker-compose.test.yml exec sasha-studio sh

# Reset data
docker compose -f docker-compose.test.yml down -v

Production

# Start with detached mode
docker compose up -d

# Update to latest
docker compose pull
docker compose up -d --force-recreate

# Backup data
docker run --rm -v sasha-data:/data -v $(pwd):/backup alpine tar czf /backup/sasha-backup.tar.gz /data

Troubleshooting

Common Issues

  1. ENOENT Errors:

    • Symptom: spawn /usr/local/bin/node ENOENT
    • Solution: Use execFile instead of spawn
  2. Permission Denied:

    • Symptom: Cannot write to directories
    • Solution: Ensure proper ownership and permissions
  3. API Key Not Persisting:

    • Symptom: Key lost after restart
    • Solution: Verify volume mounting and .env location
  4. Working Directory Issues:

    • Symptom: Claude CLI fails with path errors
    • Solution: Use absolute paths in Docker

Debug Tools

# Test Claude CLI
docker compose exec sasha-studio /usr/local/bin/node /usr/local/bin/claude --version

# Check API key
docker compose exec sasha-studio printenv | grep ANTHROPIC

# Verify volumes
docker volume inspect sasha-config

# Check file permissions
docker compose exec sasha-studio ls -la /app/config

Performance Optimization

Build Cache

# Cache npm dependencies
RUN --mount=type=cache,target=/root/.npm \
    npm ci --include=dev

Layer Optimization

  • Group related RUN commands
  • Order from least to most frequently changed
  • Use multi-stage builds to reduce final image size

Resource Limits

services:
  sasha-studio:
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2G
        reservations:
          cpus: '0.5'
          memory: 512M

Monitoring

Logging

// Structured logging for Docker
console.log(JSON.stringify({
  timestamp: new Date().toISOString(),
  level: 'info',
  message: 'Claude CLI started',
  dockerMode: true,
  workingDir: workingDir
}));

Metrics

  • Container health status
  • API key configuration status
  • Claude CLI execution success rate
  • Volume usage statistics

Migration Guide

From Local to Docker

  1. Export existing database
  2. Configure environment variables
  3. Mount existing docs directory
  4. Start container with volumes
  5. Verify Claude CLI functionality

Version Upgrades

  1. Backup volumes
  2. Pull new image
  3. Stop old container
  4. Start new container
  5. Verify functionality
  6. Remove old image

Best Practices

  1. Always use execFile in Alpine: More reliable than spawn
  2. Absolute paths only: Avoid relative path issues
  3. Persist configuration: Use volumes for stateful data
  4. Non-root execution: Run as nodejs user
  5. Health checks: Monitor container health
  6. Structured logging: Use JSON for better parsing
  7. Resource limits: Prevent runaway containers
  8. Regular backups: Automate volume backups

Future Improvements

  1. Kubernetes Deployment: Helm charts for orchestration
  2. Horizontal Scaling: Stateless architecture with external database
  3. Observability: Prometheus metrics and Grafana dashboards
  4. CI/CD Pipeline: Automated testing and deployment
  5. Multi-Architecture: ARM64 support for Apple Silicon
  6. Security Scanning: Automated vulnerability scanning

This architecture ensures reliable Docker deployment with proper Claude CLI integration, persistent storage, and Alpine Linux compatibility.