Skip to content

Docker Setup ​

Comprehensive Docker configuration for the WDG AI Development Environment, including service orchestration, networking, and container management.

Service Architecture ​

Core Services ​

Ports are interpolated from environment variables. Defaults come from .env.defaults (checked into git); user overrides go in .env (gitignored). The values shown in comments below are the defaults.

yaml
services:
  # Nginx Proxy - Routes traffic to projects
  nginx:
    image: nginx:alpine
    container_name: wdg-nginx
    ports:
      - "${NGINX_HTTP_PORT}:80"     # default host 6080
      - "${NGINX_HTTPS_PORT}:443"   # default host 6443
    volumes:
      - ./services/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./services/nginx/sites-enabled:/etc/nginx/sites-enabled:ro
      - ./ssl:/etc/nginx/ssl:ro

  # MySQL - Shared database for all projects
  mysql:
    image: mysql:8.0
    container_name: wdg-mysql
    command: --default-authentication-plugin=mysql_native_password --require_secure_transport=OFF
    ports:
      - "${MYSQL_PORT}:3306"        # default host 6306
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    volumes:
      - ./data/mysql:/var/lib/mysql  # host bind mount

  # Qdrant - Vector database for AI search
  qdrant:
    image: qdrant/qdrant:latest
    container_name: wdg-qdrant
    ports:
      - "${QDRANT_HTTP_PORT}:6333"  # REST API (default 6333)
      - "${QDRANT_GRPC_PORT}:6334"  # gRPC (default 6334)
    volumes:
      - ./data/qdrant:/qdrant/storage  # host bind mount

  # Indexer - Incremental code indexing HTTP service
  indexer:
    build: ./indexer
    container_name: wdg-indexer
    ports:
      - "${INDEXER_PORT}:8666"      # default host 6666
    volumes:
      - transformer-cache:/root/.cache/huggingface

  # MCP Server - AI assistant integration (FastMCP)
  mcp-server:
    build: ./mcp-server
    container_name: wdg-mcp
    ports:
      - "${MCP_SERVER_PORT}:8765"   # default host 6765
    volumes:
      - transformer-cache:/root/.cache/huggingface
    depends_on:
      - qdrant

  # Docs - VitePress documentation site
  docs:
    image: node:20-alpine
    container_name: wdg-docs
    ports:
      - "${DOCS_PORT}:5173"         # default host 6173

WordPress project containers are not defined here — each project gets its own compose file generated from services/docker-compose.project-template.yml.

The mcp-auth-proxy (host port 6766) and dashboard-backend (FastAPI, host port 6001) run on the host, not in Docker. The MCP auth proxy needs the host keychain to store OAuth tokens; the dashboard backend runs on the host so it can spawn the claude CLI with user credentials.

Project-Specific Services ​

Each project creates its own WordPress container:

yaml
# projects/my-site/docker-compose.yml
services:
  wdg-wp-my-site:
    build:
      context: ../../services/wordpress
      args:
        PHP_VERSION: "8.2"
    container_name: wdg-wp-my-site
    environment:
      WORDPRESS_DB_HOST: mysql
      WORDPRESS_DB_NAME: wp_my_site
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
    volumes:
      - wordpress_my_site:/var/www/html   # named volume: wordpress_{project} (underscores)
      - ./repositories/my-site:/var/www/html/wp-content
    networks:
      - wdg-network

Per-project compose files are generated from services/docker-compose.project-template.yml. WordPress core lives in a per-project named volume wordpress_{project_name} (hyphens become underscores); the project's wp-content is bind-mounted from its repository.

Docker Compose Files ​

Main docker-compose.yml ​

Located at project root, manages core services:

The top-level file declares no version: key (the Compose Specification dropped it; recent Docker Compose warns when it is present). The network is external and the only named volume is transformer-cache — MySQL and Qdrant use host bind mounts.

yaml
networks:
  wdg-network:
    external: true

volumes:
  transformer-cache:

services:
  nginx:
    image: nginx:alpine
    container_name: wdg-nginx
    ports:
      - "${NGINX_HTTP_PORT}:80"
      - "${NGINX_HTTPS_PORT}:443"
    volumes:
      - ./services/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./services/nginx/sites-enabled:/etc/nginx/sites-enabled:ro
      - ./ssl:/etc/nginx/ssl:ro
    networks:
      - wdg-network
    restart: unless-stopped

  mysql:
    image: mysql:8.0
    container_name: wdg-mysql
    command: --default-authentication-plugin=mysql_native_password --require_secure_transport=OFF
    ports:
      - "${MYSQL_PORT}:3306"
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${WP_DB_NAME}
      MYSQL_USER: ${WP_DB_USER}
      MYSQL_PASSWORD: ${WP_DB_PASSWORD}
    volumes:
      - ./data/mysql:/var/lib/mysql
    networks:
      - wdg-network
    restart: unless-stopped

  qdrant:
    image: qdrant/qdrant:latest
    container_name: wdg-qdrant
    ports:
      - "${QDRANT_HTTP_PORT}:6333"
      - "${QDRANT_GRPC_PORT}:6334"
    volumes:
      - ./data/qdrant:/qdrant/storage
    environment:
      QDRANT__LOG_LEVEL: INFO
      QDRANT__SERVICE__HTTP_PORT: 6333
      QDRANT__SERVICE__GRPC_PORT: 6334
    networks:
      - wdg-network
    restart: unless-stopped

  indexer:
    build: ./indexer
    container_name: wdg-indexer
    ports:
      - "${INDEXER_PORT}:8666"
    environment:
      EMBEDDING_MODEL: ${EMBEDDING_MODEL}
      QDRANT_HOST: qdrant
      QDRANT_PORT: 6333
      INDEXER_PORT: 8666
      PROJECT_ROOT: /workspace
    volumes:
      - transformer-cache:/root/.cache/huggingface
      # ...plus read-only mounts of the platform source under /workspace
    networks:
      - wdg-network
    depends_on:
      - qdrant
    restart: unless-stopped

  mcp-server:
    build: ./mcp-server
    container_name: wdg-mcp
    ports:
      - "${MCP_SERVER_PORT}:8765"
    environment:
      QDRANT_HOST: qdrant
      QDRANT_PORT: 6333
      MCP_PORT: 8765
      MCP_AUTH_TOKEN: ${MCP_AUTH_TOKEN}
      EMBEDDING_MODEL: ${EMBEDDING_MODEL}
    volumes:
      - ./repositories:/repositories:ro
      - ./projects:/projects:ro
      - transformer-cache:/root/.cache/huggingface
    networks:
      - wdg-network
    depends_on:
      - qdrant
      - mysql
    restart: unless-stopped

  docs:
    image: node:20-alpine
    container_name: wdg-docs
    working_dir: /app
    ports:
      - "${DOCS_PORT}:5173"
    volumes:
      - ./docs:/app
    command: sh -c "npm install && npm run docs:dev -- --host 0.0.0.0"
    networks:
      - wdg-network
    restart: unless-stopped

  dashboard-frontend:
    build: ./dashboard/frontend
    container_name: wdg-dashboard-frontend
    ports:
      - "${DASHBOARD_FRONTEND_PORT}:3000"
    networks:
      - wdg-network
    restart: unless-stopped

  phpmyadmin:
    image: phpmyadmin:latest
    container_name: wdg-phpmyadmin
    ports:
      - "${PHPMYADMIN_PORT}:80"
    environment:
      PMA_HOST: mysql
      PMA_PORT: 3306
    networks:
      - wdg-network
    depends_on:
      - mysql
    restart: unless-stopped

MCP_AUTH_TOKEN (empty by default in .env.defaults) sets a bearer token the MCP server requires when non-empty. COMPOSE_PROJECT_NAME (default wdg-ai-development-environment) prefixes the transformer-cache named volume.

Dockerfile Configurations ​

WordPress Dockerfile ​

services/wordpress/Dockerfile:

dockerfile
ARG PHP_VERSION=8.2
FROM wordpress:php${PHP_VERSION}-apache

# Install system dependencies
RUN apt-get update && apt-get install -y \
    git \
    unzip \
    libzip-dev \
    libpng-dev \
    libjpeg-dev \
    libfreetype6-dev \
    && rm -rf /var/lib/apt/lists/*

# Configure PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) \
        gd \
        mysqli \
        pdo \
        pdo_mysql \
        zip \
        opcache

# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Install WP-CLI
RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
    && chmod +x wp-cli.phar \
    && mv wp-cli.phar /usr/local/bin/wp

# Configure PHP
COPY php.ini /usr/local/etc/php/conf.d/custom.ini
COPY php-error-logging.ini /usr/local/etc/php/conf.d/error-logging.ini

# Configure Apache
RUN a2enmod rewrite headers expires

# Set proper permissions
RUN chown -R www-data:www-data /var/www/html

# Custom entrypoint
COPY docker-entrypoint-wrapper.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint-wrapper.sh

ENTRYPOINT ["docker-entrypoint-wrapper.sh"]
CMD ["apache2-foreground"]

MCP Server Dockerfile ​

mcp-server/Dockerfile:

dockerfile
FROM python:3.11-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application
COPY server.py .
COPY tools/ ./tools/

# Create non-root user
RUN useradd -m -u 1000 mcp && chown -R mcp:mcp /app
USER mcp

EXPOSE 8765

CMD ["python", "server.py"]

Volume Management ​

Host Bind Mounts (persistent data) ​

MySQL and Qdrant data persist in host directories under ./data, not Docker named volumes. Back them up by copying the directories directly:

bash
# Stop services first for a consistent MySQL copy
docker compose stop mysql qdrant

# Back up persistent data (host bind mounts)
tar czf backups/data-$(date +%Y%m%d-%H%M%S).tar.gz data/mysql data/qdrant

docker compose start mysql qdrant

For a logical MySQL backup that doesn't require stopping the container, use mysqldump (see Database Architecture).

Named Volumes ​

The only named volume in the top-level compose file is transformer-cache, which holds the downloaded embedding model and is shared by wdg-indexer and wdg-mcp:

bash
# List volumes (prefixed by COMPOSE_PROJECT_NAME)
docker volume ls | grep transformer-cache

# Inspect
docker volume inspect wdg-ai-development-environment_transformer-cache

Project Bind Mounts ​

Per-project WordPress containers bind-mount the project's wp-content from its repository:

yaml
volumes:
  - ./repositories/my-site:/var/www/html/wp-content

Networks ​

WDG Network ​

All services communicate via wdg-network, an external bridge network created before compose runs (by the installer / wdg start). It is referenced as external in compose, not defined with an inline driver/ipam block:

yaml
networks:
  wdg-network:
    external: true

Docker assigns the subnet automatically (typically 172.19.0.0/16); it is not pinned. Create it manually with docker network create wdg-network if it is missing.

Network Inspection ​

bash
# View network
docker network inspect wdg-network

# List connected containers
docker network inspect wdg-network --format='{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}'

Resource Limits ​

Container Limits ​

Set limits in project docker-compose.yml:

yaml
services:
  wordpress:
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2G
        reservations:
          cpus: '0.5'
          memory: 512M

Global Daemon Configuration ​

/etc/docker/daemon.json:

json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 64000,
      "Soft": 64000
    }
  }
}

Build Optimization ​

Multi-Stage Builds ​

For production-optimized images:

dockerfile
# Build stage
FROM node:18 AS builder
WORKDIR /build
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM nginx:alpine
COPY --from=builder /build/dist /usr/share/nginx/html

Layer Caching ​

Optimize Dockerfile layer caching:

dockerfile
# Install dependencies first (cached layer)
COPY package*.json ./
RUN npm install

# Copy source (changes frequently)
COPY . .
RUN npm run build

Container Management ​

Start/Stop Services ​

bash
# Start all services
docker-compose up -d

# Start specific service
docker-compose up -d mysql qdrant

# Stop all services
docker-compose down

# Stop and remove volumes
docker-compose down -v

View Logs ​

bash
# All services
docker-compose logs -f

# Specific service
docker-compose logs -f mysql

# Last 100 lines
docker-compose logs --tail=100 nginx

Execute Commands ​

bash
# Enter container shell
docker exec -it wdg-mysql bash

# Run command
docker exec wdg-mysql mysql -uroot -p$MYSQL_ROOT_PASSWORD -e "SHOW DATABASES;"

# WordPress WP-CLI
docker exec wdg-wp-my-site wp plugin list

Health Checks ​

Service Health Checks ​

yaml
services:
  mysql:
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  wordpress:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/"]
      interval: 30s
      timeout: 3s
      retries: 3

Check Health Status ​

bash
# View health status
docker ps --format "table {{.Names}}\t{{.Status}}"

# Wait for healthy
docker-compose up -d && docker-compose ps

# Check specific service
docker inspect --format='{{.State.Health.Status}}' wdg-mysql

Troubleshooting ​

Container Won't Start ​

bash
# View logs
docker logs wdg-wp-my-site

# Check for port conflicts (default HTTPS host port)
lsof -i :6443

# Verify configuration
docker-compose config

Permission Issues ​

bash
# Fix WordPress permissions
docker exec wdg-wp-my-site chown -R www-data:www-data /var/www/html/wp-content

# Fix from host
sudo chown -R $(id -u):$(id -g) projects/my-site

Network Issues ​

bash
# Recreate network
docker network rm wdg-network
docker network create wdg-network

# Reset DNS
docker-compose down && docker-compose up -d

See Also: