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

Docker Container Workspace Initialization Plan

Generated: 2025-01-07 20:15 UTC
Status: Analysis Complete

Problem Analysis

When running Sasha Studio in a Docker container, workspace/project creation fails due to:

1. JWT Token Mismatch

  • Issue: Container uses default secrets (change-me-in-production) while browser has tokens from different secrets
  • Impact: All authenticated API calls fail with "invalid signature"
  • Solution: Need consistent JWT secrets between sessions

2. Missing Claude Directory Structure

  • Issue: Container's /home/nodejs/.claude/ is empty
  • Impact: No projects directory exists for workspace creation
  • Solution: Initialize Claude directory structure on container startup

3. Claude CLI Not Installed

  • Issue: Only placeholder Claude CLI exists in container
  • Impact: Cannot execute actual Claude commands
  • Solution: Multiple approaches available (see below)

Solution Architecture

Option 1: Host Mount Strategy (Recommended for Development)

Mount host's Claude directory into container:

volumes:
  # Mount host Claude directory
  - ~/.claude:/home/nodejs/.claude:rw

Pros:

  • Access to existing projects
  • Real Claude CLI available if installed on host
  • Seamless integration with host system

Cons:

  • Requires Claude installed on host
  • Path permissions may need adjustment
  • Not portable across different hosts

Option 2: Container-Isolated Strategy (Recommended for Production)

Create self-contained workspace environment:

# In Dockerfile
RUN mkdir -p /home/nodejs/.claude/projects \
    && echo '{"claudeProjects": {}}' > /home/nodejs/.claude/claude.json \
    && chown -R nodejs:nodejs /home/nodejs/.claude

Pros:

  • Fully isolated and portable
  • Consistent across deployments
  • No host dependencies

Cons:

  • No access to host projects
  • Requires Claude CLI in container

Option 3: Hybrid Approach (Best of Both)

Support both modes via environment variable:

// In server/projects.js
const CLAUDE_HOME = process.env.CLAUDE_HOME || os.homedir();
const PROJECTS_PATH = process.env.CLAUDE_PROJECTS_PATH || 
                      path.join(CLAUDE_HOME, '.claude', 'projects');
const USE_DOCKER_WORKSPACE = process.env.USE_DOCKER_WORKSPACE === 'true';

if (USE_DOCKER_WORKSPACE) {
    // Initialize container workspace
    await initializeDockerWorkspace();
}

Implementation Steps

Step 1: Create Docker Initialization Script

// server/docker-init.js
import fs from 'fs/promises';
import path from 'path';

export async function initializeDockerEnvironment() {
    const claudeHome = process.env.CLAUDE_HOME || '/home/nodejs/.claude';
    
    // Create directory structure
    const dirs = [
        claudeHome,
        path.join(claudeHome, 'projects'),
        path.join(claudeHome, 'sessions'),
        '/app/workspaces'  // Alternative workspace location
    ];
    
    for (const dir of dirs) {
        await fs.mkdir(dir, { recursive: true });
        console.log(`βœ… Created directory: ${dir}`);
    }
    
    // Create default Claude config if not exists
    const configPath = path.join(claudeHome, 'claude.json');
    try {
        await fs.access(configPath);
    } catch {
        const defaultConfig = {
            claudeProjects: {},
            mcpServers: {},
            dockerMode: true
        };
        await fs.writeFile(configPath, JSON.stringify(defaultConfig, null, 2));
        console.log('βœ… Created default Claude configuration');
    }
    
    // Create a default workspace
    const defaultWorkspace = path.join(claudeHome, 'projects', 'default-workspace');
    await fs.mkdir(defaultWorkspace, { recursive: true });
    
    // Create workspace metadata
    const metadata = {
        name: 'Default Workspace',
        created: new Date().toISOString(),
        type: 'docker-workspace',
        path: '/app/workspaces/default'
    };
    
    await fs.writeFile(
        path.join(defaultWorkspace, '.claude-workspace'),
        JSON.stringify(metadata, null, 2)
    );
    
    console.log('βœ… Docker workspace initialized');
}

Step 2: Update Docker Compose Configuration

# docker-compose.yml updates
services:
  sasha-studio:
    environment:
      # Add Docker-specific environment variables
      USE_DOCKER_WORKSPACE: "true"
      CLAUDE_HOME: /home/nodejs/.claude
      CLAUDE_PROJECTS_PATH: /app/workspaces
      
      # Use consistent secrets (generate these!)
      SESSION_SECRET: ${SESSION_SECRET:-your-generated-secret-here}
      JWT_SECRET: ${JWT_SECRET:-your-generated-secret-here}
    
    volumes:
      # Option A: Mount host Claude directory
      # - ~/.claude:/home/nodejs/.claude:rw
      
      # Option B: Use container workspace
      - claude-workspace:/home/nodejs/.claude
      - app-workspaces:/app/workspaces

volumes:
  claude-workspace:
    driver: local
  app-workspaces:
    driver: local

Step 3: Create Workspace Manager for Docker

// server/workspace-manager.js
export class DockerWorkspaceManager {
    constructor() {
        this.workspacesPath = process.env.CLAUDE_PROJECTS_PATH || '/app/workspaces';
        this.claudeHome = process.env.CLAUDE_HOME || '/home/nodejs/.claude';
    }
    
    async createWorkspace(name, options = {}) {
        const workspaceId = `workspace-${Date.now()}`;
        const workspacePath = path.join(this.workspacesPath, workspaceId);
        
        // Create workspace directory
        await fs.mkdir(workspacePath, { recursive: true });
        
        // Create subdirectories
        const dirs = ['src', 'docs', 'tests', '.claude'];
        for (const dir of dirs) {
            await fs.mkdir(path.join(workspacePath, dir), { recursive: true });
        }
        
        // Create workspace config
        const config = {
            id: workspaceId,
            name: name || 'Untitled Workspace',
            created: new Date().toISOString(),
            path: workspacePath,
            type: 'docker-workspace',
            ...options
        };
        
        await fs.writeFile(
            path.join(workspacePath, '.claude', 'workspace.json'),
            JSON.stringify(config, null, 2)
        );
        
        // Register in Claude config
        await this.registerWorkspace(workspaceId, config);
        
        return config;
    }
    
    async registerWorkspace(id, config) {
        const claudeConfigPath = path.join(this.claudeHome, 'claude.json');
        
        try {
            const configData = await fs.readFile(claudeConfigPath, 'utf8');
            const claudeConfig = JSON.parse(configData);
            
            if (!claudeConfig.claudeProjects) {
                claudeConfig.claudeProjects = {};
            }
            
            claudeConfig.claudeProjects[config.path] = {
                id: id,
                name: config.name,
                created: config.created
            };
            
            await fs.writeFile(claudeConfigPath, JSON.stringify(claudeConfig, null, 2));
        } catch (error) {
            console.error('Failed to register workspace:', error);
        }
    }
    
    async listWorkspaces() {
        const workspaces = [];
        
        try {
            const entries = await fs.readdir(this.workspacesPath);
            
            for (const entry of entries) {
                const workspacePath = path.join(this.workspacesPath, entry);
                const configPath = path.join(workspacePath, '.claude', 'workspace.json');
                
                try {
                    const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
                    workspaces.push(config);
                } catch {
                    // Skip invalid workspaces
                }
            }
        } catch (error) {
            console.error('Failed to list workspaces:', error);
        }
        
        return workspaces;
    }
}

Step 4: Update Server Initialization

// server/index.js additions
import { initializeDockerEnvironment } from './docker-init.js';
import { DockerWorkspaceManager } from './workspace-manager.js';

// At server startup
async function initializeServer() {
    // ... existing initialization ...
    
    // Initialize Docker workspace if needed
    if (process.env.USE_DOCKER_WORKSPACE === 'true') {
        console.log('🐳 Initializing Docker workspace environment...');
        await initializeDockerEnvironment();
        
        // Use Docker workspace manager
        global.workspaceManager = new DockerWorkspaceManager();
    }
    
    // ... rest of initialization ...
}

Step 5: Add Workspace API Endpoints

// server/routes/workspace.js
import express from 'express';
const router = express.Router();

// Create new workspace
router.post('/create', async (req, res) => {
    try {
        const { name, description } = req.body;
        
        if (global.workspaceManager) {
            const workspace = await global.workspaceManager.createWorkspace(name, {
                description,
                userId: req.user.id
            });
            
            res.json({ success: true, workspace });
        } else {
            // Fallback to traditional Claude project creation
            res.status(501).json({ 
                error: 'Workspace creation not available' 
            });
        }
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// List workspaces
router.get('/list', async (req, res) => {
    try {
        if (global.workspaceManager) {
            const workspaces = await global.workspaceManager.listWorkspaces();
            res.json({ workspaces });
        } else {
            // Fallback to Claude projects
            const projects = await getProjects();
            res.json({ workspaces: projects });
        }
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

export default router;

Quick Fix for Current Issue

For immediate resolution, restart the container with proper secrets:

# Stop current container
docker compose down

# Create .env file with consistent secrets
cat > .env << EOF
SESSION_SECRET=$(openssl rand -base64 32)
JWT_SECRET=$(openssl rand -base64 32)
EOF

# Restart with new configuration
docker compose up -d

Then create initialization directory:

# Initialize Claude directory in container
docker exec sasha-studio mkdir -p /home/nodejs/.claude/projects

# Create a test project
docker exec sasha-studio mkdir -p /home/nodejs/.claude/projects/test-project

# Set permissions
docker exec sasha-studio chown -R nodejs:nodejs /home/nodejs/.claude

Environment Variables Summary

# Docker-specific
USE_DOCKER_WORKSPACE=true        # Enable Docker workspace mode
CLAUDE_HOME=/home/nodejs/.claude # Claude configuration directory
CLAUDE_PROJECTS_PATH=/app/workspaces # Workspace storage location

# Security (must be consistent!)
SESSION_SECRET=<generate-strong-secret>
JWT_SECRET=<generate-strong-secret>

# Optional
DOCKER_WORKSPACE_MOUNT=host      # 'host' or 'container'
AUTO_CREATE_WORKSPACE=true       # Auto-create default workspace

Testing Workspace Creation

After implementing:

  1. Test workspace creation:
curl -X POST http://localhost:3005/api/workspace/create \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"name": "Test Workspace"}'
  1. Verify workspace exists:
docker exec sasha-studio ls -la /app/workspaces/
  1. Check Claude configuration:
docker exec sasha-studio cat /home/nodejs/.claude/claude.json

Conclusion

The Docker workspace initialization issue stems from missing directory structures and JWT token mismatches. The solution involves:

  1. Creating proper workspace directories on container startup
  2. Using consistent JWT secrets across sessions
  3. Implementing a Docker-aware workspace manager
  4. Providing flexibility between host-mounted and container-isolated modes

This approach ensures workspaces can be created and managed effectively within Docker containers while maintaining compatibility with the standard Claude CLI workflow when available.