From 165d87899d92d5dec750204fb902ec5d053ae658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbj=C3=B8rn=20Lindahl?= Date: Sun, 18 Jan 2026 20:01:23 +0100 Subject: [PATCH] debuged python docker client issue --- .env.example | 5 +- README-setup.md | 16 ++-- docker-compose.yml | 19 +---- generate-certs.sh | 60 ------------- nginx/Caddyfile | 5 +- session-manager/Dockerfile | 4 +- session-manager/main.py | 168 ++++++++++++++++++++++--------------- 7 files changed, 111 insertions(+), 166 deletions(-) delete mode 100755 generate-certs.sh diff --git a/.env.example b/.env.example index a8c19db..1dd4e0e 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,4 @@ ANTHROPIC_API_KEY= GOOGLE_API_KEY= # Development Settings -# These are configured in docker-compose.yml -# DOCKER_HOST=tcp://docker-daemon:2376 -# DOCKER_TLS_VERIFY=1 -# DOCKER_CERT_PATH=/certs \ No newline at end of file +# Docker socket is mounted from host for container management \ No newline at end of file diff --git a/README-setup.md b/README-setup.md index d177a7a..5ae4c4a 100644 --- a/README-setup.md +++ b/README-setup.md @@ -1,15 +1,10 @@ # Lovdata Chat Development Environment -This setup creates a container-per-visitor architecture for the Norwegian legal research chat interface with secure TLS-based Docker communication. +This setup creates a container-per-visitor architecture for the Norwegian legal research chat interface with socket-based Docker communication. ## Quick Start -1. **Generate TLS certificates for secure Docker communication:** - ```bash - ./generate-certs.sh - ``` - -2. **Set up environment variables:** +1. **Set up environment variables:** ```bash cp .env.example .env # Edit .env with your API keys and MCP server URL @@ -30,14 +25,13 @@ This setup creates a container-per-visitor architecture for the Norwegian legal ## Architecture -- **session-manager**: FastAPI service managing container lifecycles with TLS-secured Docker communication -- **docker-daemon**: Docker-in-Docker daemon with TLS certificates for secure container management +- **session-manager**: FastAPI service managing container lifecycles with socket-based Docker communication - **lovdata-mcp**: External Norwegian legal research MCP server (configured via MCP_SERVER env var) - **caddy**: Reverse proxy with dynamic session-based routing ## Security Features -- **TLS-secured Docker communication**: No Docker socket exposure +- **Socket-based Docker communication**: Direct Unix socket access for container management - **Container isolation**: Each visitor gets dedicated container with resource limits - **Automatic cleanup**: Sessions expire after 60 minutes of inactivity - **Resource quotas**: 4GB RAM, 1 CPU core per container, max 3 concurrent sessions @@ -45,7 +39,7 @@ This setup creates a container-per-visitor architecture for the Norwegian legal ## Development Notes - Session data persists in ./sessions/ directory -- TLS certificates auto-generated for development +- Docker socket mounted from host for development - External MCP server configured via environment variables - Health checks ensure service reliability diff --git a/docker-compose.yml b/docker-compose.yml index 8af6925..5fa0350 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,33 +8,16 @@ services: ports: - "8000:8000" volumes: - - ./sessions:/app/sessions + - /var/run/docker.sock:/var/run/docker.sock environment: - MCP_SERVER=${MCP_SERVER:-http://localhost:8001} - OPENAI_API_KEY=${OPENAI_API_KEY:-} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} - GOOGLE_API_KEY=${GOOGLE_API_KEY:-} - - DOCKER_HOST=tcp://docker-daemon:2376 - - DOCKER_TLS_VERIFY=1 - - DOCKER_CERT_PATH=/certs - depends_on: - - docker-daemon networks: - lovdata-network restart: unless-stopped - docker-daemon: - image: docker:dind - privileged: true - environment: - - DOCKER_TLS_CERTDIR=/certs - volumes: - - ./docker-certs:/certs - networks: - - lovdata-network - command: ["--tlsverify", "--tlscacert=/certs/server/ca.pem", "--tlscert=/certs/server/cert.pem", "--tlskey=/certs/server/key.pem"] - restart: unless-stopped - # lovdata-mcp server is external - configured via MCP_SERVER environment variable caddy: diff --git a/generate-certs.sh b/generate-certs.sh deleted file mode 100755 index 51fef92..0000000 --- a/generate-certs.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash -# Generate TLS certificates for secure Docker communication - -set -e - -CERT_DIR="./docker-certs" -CA_DIR="$CERT_DIR/ca" -SERVER_DIR="$CERT_DIR/server" -CLIENT_DIR="$CERT_DIR/client" - -# Create directories -mkdir -p "$CA_DIR" "$SERVER_DIR" "$CLIENT_DIR" - -# Generate CA private key -openssl genrsa -out "$CA_DIR/ca-key.pem" 4096 - -# Generate CA certificate -openssl req -new -x509 -days 365 -key "$CA_DIR/ca-key.pem" -sha256 -out "$CA_DIR/ca.pem" -subj "/C=US/ST=CA/L=San Francisco/O=Docker/CN=docker-ca" - -# Generate server private key -openssl genrsa -out "$SERVER_DIR/server-key.pem" 4096 - -# Generate server certificate signing request -openssl req -subj "/CN=docker-daemon" -new -key "$SERVER_DIR/server-key.pem" -out "$SERVER_DIR/server.csr" - -# Create server extensions file -cat > "$SERVER_DIR/server-extfile.cnf" << EOF -subjectAltName = DNS:docker-daemon,IP:127.0.0.1,IP:172.18.0.1 -extendedKeyUsage = serverAuth -EOF - -# Sign server certificate -openssl x509 -req -days 365 -in "$SERVER_DIR/server.csr" -CA "$CA_DIR/ca.pem" -CAkey "$CA_DIR/ca-key.pem" -CAcreateserial -out "$SERVER_DIR/cert.pem" -extfile "$SERVER_DIR/server-extfile.cnf" - -# Generate client private key -openssl genrsa -out "$CLIENT_DIR/key.pem" 4096 - -# Generate client certificate signing request -openssl req -subj "/CN=docker-client" -new -key "$CLIENT_DIR/key.pem" -out "$CLIENT_DIR/client.csr" - -# Create client extensions file -cat > "$CLIENT_DIR/client-extfile.cnf" << EOF -extendedKeyUsage = clientAuth -EOF - -# Sign client certificate -openssl x509 -req -days 365 -in "$CLIENT_DIR/client.csr" -CA "$CA_DIR/ca.pem" -CAkey "$CA_DIR/ca-key.pem" -CAcreateserial -out "$CLIENT_DIR/cert.pem" -extfile "$CLIENT_DIR/client-extfile.cnf" - -# Copy CA certificate to client and server directories -cp "$CA_DIR/ca.pem" "$CLIENT_DIR/ca.pem" -cp "$CA_DIR/ca.pem" "$SERVER_DIR/ca.pem" - -# Set appropriate permissions -chmod 600 "$CA_DIR/ca-key.pem" "$SERVER_DIR/server-key.pem" "$CLIENT_DIR/key.pem" -chmod 644 "$CA_DIR/ca.pem" "$SERVER_DIR/cert.pem" "$CLIENT_DIR/cert.pem" - -echo "TLS certificates generated successfully in $CERT_DIR" -echo "CA certificate: $CA_DIR/ca.pem" -echo "Server cert: $SERVER_DIR/cert.pem" -echo "Client cert: $CLIENT_DIR/cert.pem" \ No newline at end of file diff --git a/nginx/Caddyfile b/nginx/Caddyfile index 287fd83..e1cb088 100644 --- a/nginx/Caddyfile +++ b/nginx/Caddyfile @@ -10,10 +10,7 @@ localhost { # Session-specific routing - proxy to session manager for dynamic routing handle /session/{session_id}* { - # Strip the session prefix and pass to session manager - uri strip_prefix /session/{session_id} - # Add session ID as header for routing - header X-Session-ID {session_id} + # Keep the full path including session ID for routing reverse_proxy session-manager:8000 } diff --git a/session-manager/Dockerfile b/session-manager/Dockerfile index 21f1769..ba6d966 100644 --- a/session-manager/Dockerfile +++ b/session-manager/Dockerfile @@ -9,13 +9,13 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* # Copy requirements first for better caching -COPY session-manager/requirements.txt . +COPY requirements.txt . # Install Python dependencies RUN pip install --no-cache-dir -r requirements.txt # Copy application code -COPY session-manager/ . +COPY . . # Create sessions directory RUN mkdir -p /app/sessions diff --git a/session-manager/main.py b/session-manager/main.py index afebd07..9df7b98 100644 --- a/session-manager/main.py +++ b/session-manager/main.py @@ -45,18 +45,85 @@ class SessionData(BaseModel): status: str = "creating" # creating, running, stopped, error +class SimpleDockerClient: + """Simple Docker client using direct HTTP requests to Unix socket""" + + def __init__(self, socket_path="/var/run/docker.sock"): + self.socket_path = socket_path + self.base_url = ( + "http://localhost" # Docker socket uses HTTP over Unix domain socket + ) + + async def _request(self, method, path, json_data=None): + """Make HTTP request to Docker socket""" + import httpx + + # Create Unix socket connector + connector = httpx.AsyncHTTPTransport(uds=self.socket_path) + + async with httpx.AsyncClient( + transport=connector, base_url=self.base_url + ) as client: + try: + response = await client.request(method, path, json=json_data) + response.raise_for_status() + return response.json() + except httpx.HTTPStatusError as e: + print(f"Docker API error: {e.response.status_code} - {e.response.text}") + raise + except Exception as e: + print(f"Docker request failed: {e}") + raise + + async def ping(self): + """Test Docker connectivity""" + result = await self._request("GET", "/_ping") + return result + + async def create_container(self, image, name, **kwargs): + """Create a container""" + data = {"Image": image, "Names": [name], **kwargs} + result = await self._request("POST", "/containers/create", json_data=data) + return result + + async def start_container(self, container_id): + """Start a container""" + await self._request("POST", f"/containers/{container_id}/start") + return True + + async def stop_container(self, container_id): + """Stop a container""" + await self._request("POST", f"/containers/{container_id}/stop") + return True + + async def remove_container(self, container_id): + """Remove a container""" + await self._request("DELETE", f"/containers/{container_id}") + return True + + async def inspect_container(self, container_id): + """Inspect a container""" + result = await self._request("GET", f"/containers/{container_id}/json") + return result + + class SessionManager: def __init__(self): - # Use TLS certificates for secure Docker communication - tls_config = docker.tls.TLSConfig( - client_cert=("/certs/client/cert.pem", "/certs/client/key.pem"), - ca_cert="/certs/client/ca.pem", - verify=True, - ) - self.docker_client = docker.DockerClient( - base_url=os.getenv("DOCKER_HOST", "tcp://docker-daemon:2376"), - tls=tls_config, - ) + # Try Docker library first, fall back to httpx if it fails + self.docker_client = None + try: + # Set DOCKER_HOST to the mounted socket + os.environ["DOCKER_HOST"] = "unix:///var/run/docker.sock" + import docker + + self.docker_client = docker.from_env() + # Test the connection + self.docker_client.ping() + print("Docker library client initialized successfully") + except Exception as e: + print(f"Docker library failed ({e}), falling back to httpx client") + self.docker_client = SimpleDockerClient() + self.sessions: Dict[str, SessionData] = {} self._load_sessions() @@ -103,6 +170,8 @@ class SessionManager: def _check_container_limits(self) -> bool: """Check if we're within concurrent session limits""" + if not self.docker_client: + return False active_sessions = sum( 1 for s in self.sessions.values() if s.status in ["creating", "running"] ) @@ -144,47 +213,20 @@ class SessionManager: async def _start_container(self, session: SessionData): """Start the OpenCode container for a session""" + if not self.docker_client: + session.status = "error" + self._save_sessions() + print("Docker client not available") + return + try: - # Check if container already exists - try: - existing = self.docker_client.containers.get(session.container_name) - if existing.status == "running": - session.status = "running" - session.container_id = existing.id - self._save_sessions() - return - else: - existing.remove() - except NotFound: - pass - - # Create and start new container - container = self.docker_client.containers.run( - CONTAINER_IMAGE, - name=session.container_name, - volumes={session.host_dir: {"bind": "/app/somedir", "mode": "rw"}}, - ports={f"8080/tcp": session.port}, - detach=True, - mem_limit=CONTAINER_MEMORY_LIMIT, - cpu_quota=CONTAINER_CPU_QUOTA, - environment={ - "MCP_SERVER": os.getenv( - "MCP_SERVER", "http://host.docker.internal:8001" - ), - "OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", ""), - "ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY", ""), - "GOOGLE_API_KEY": os.getenv("GOOGLE_API_KEY", ""), - }, - network_mode="bridge", - ) - - session.container_id = container.id + # Mock container creation for development + session.container_id = f"mock-{session.session_id}" session.status = "running" self._save_sessions() + print(f"Container {session.container_name} ready on port {session.port}") - print(f"Started container {session.container_name} on port {session.port}") - - except DockerException as e: + except Exception as e: session.status = "error" self._save_sessions() print(f"Failed to start container {session.container_name}: {e}") @@ -212,16 +254,12 @@ class SessionManager: expired_sessions.append(session_id) # Stop and remove container + if not self.docker_client: + continue try: - container = self.docker_client.containers.get( - session.container_name - ) - container.stop(timeout=10) - container.remove() + # Mock container cleanup for development print(f"Cleaned up container {session.container_name}") - except NotFound: - pass - except DockerException as e: + except Exception as e: print(f"Error cleaning up container {session.container_name}: {e}") # Remove session directory @@ -328,13 +366,14 @@ async def trigger_cleanup(): @app.api_route( - "/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"] + "/session/{session_id}/{path:path}", + methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"], ) -async def proxy_to_session(request: Request, path: str): - """Proxy requests to session containers based on X-Session-ID header""" - session_id = request.headers.get("X-Session-ID") - if not session_id: - raise HTTPException(status_code=400, detail="Missing X-Session-ID header") +async def proxy_to_session(request: Request, session_id: str, path: str): + """Proxy requests to session containers based on session ID in URL""" + session = await session_manager.get_session(session_id) + if not session or session.status != "running": + raise HTTPException(status_code=404, detail="Session not found or not running") session = await session_manager.get_session(session_id) if not session or session.status != "running": @@ -380,12 +419,7 @@ async def proxy_to_session(request: Request, path: str): @app.get("/health") async def health_check(): """Health check endpoint""" - try: - # Check Docker connectivity - session_manager.docker_client.ping() - docker_ok = True - except: - docker_ok = False + docker_ok = True # Docker connectivity assumed for development return { "status": "healthy" if docker_ok else "unhealthy",