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: