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:
- 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"}'
- Verify workspace exists:
docker exec sasha-studio ls -la /app/workspaces/
- 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:
- Creating proper workspace directories on container startup
- Using consistent JWT secrets across sessions
- Implementing a Docker-aware workspace manager
- 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.