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
Initial Configuration:
- User provides API key during onboarding
- Server saves to
/app/config/.env - Environment variable set for current process
Container Restart:
- Server loads from
/app/config/.envon startup - API key available to Claude CLI via environment
- Server loads from
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
ENOENT Errors:
- Symptom:
spawn /usr/local/bin/node ENOENT - Solution: Use execFile instead of spawn
- Symptom:
Permission Denied:
- Symptom: Cannot write to directories
- Solution: Ensure proper ownership and permissions
API Key Not Persisting:
- Symptom: Key lost after restart
- Solution: Verify volume mounting and .env location
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
- Export existing database
- Configure environment variables
- Mount existing docs directory
- Start container with volumes
- Verify Claude CLI functionality
Version Upgrades
- Backup volumes
- Pull new image
- Stop old container
- Start new container
- Verify functionality
- Remove old image
Best Practices
- Always use execFile in Alpine: More reliable than spawn
- Absolute paths only: Avoid relative path issues
- Persist configuration: Use volumes for stateful data
- Non-root execution: Run as nodejs user
- Health checks: Monitor container health
- Structured logging: Use JSON for better parsing
- Resource limits: Prevent runaway containers
- Regular backups: Automate volume backups
Future Improvements
- Kubernetes Deployment: Helm charts for orchestration
- Horizontal Scaling: Stateless architecture with external database
- Observability: Prometheus metrics and Grafana dashboards
- CI/CD Pipeline: Automated testing and deployment
- Multi-Architecture: ARM64 support for Apple Silicon
- Security Scanning: Automated vulnerability scanning
This architecture ensures reliable Docker deployment with proper Claude CLI integration, persistent storage, and Alpine Linux compatibility.