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.
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 6173WordPress 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) anddashboard-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 theclaudeCLI with user credentials.
Project-Specific Services
Each project creates its own WordPress container:
# 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-networkPer-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.
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(defaultwdg-ai-development-environment) prefixes thetransformer-cachenamed volume.
Dockerfile Configurations
WordPress Dockerfile
services/wordpress/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:
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:
# 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 qdrantFor 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:
# List volumes (prefixed by COMPOSE_PROJECT_NAME)
docker volume ls | grep transformer-cache
# Inspect
docker volume inspect wdg-ai-development-environment_transformer-cacheProject Bind Mounts
Per-project WordPress containers bind-mount the project's wp-content from its repository:
volumes:
- ./repositories/my-site:/var/www/html/wp-contentNetworks
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:
networks:
wdg-network:
external: trueDocker 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
# 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:
services:
wordpress:
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512MGlobal Daemon Configuration
/etc/docker/daemon.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:
# 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/htmlLayer Caching
Optimize Dockerfile layer caching:
# Install dependencies first (cached layer)
COPY package*.json ./
RUN npm install
# Copy source (changes frequently)
COPY . .
RUN npm run buildContainer Management
Start/Stop Services
# 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 -vView Logs
# All services
docker-compose logs -f
# Specific service
docker-compose logs -f mysql
# Last 100 lines
docker-compose logs --tail=100 nginxExecute Commands
# 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 listHealth Checks
Service Health Checks
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: 3Check Health Status
# 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-mysqlTroubleshooting
Container Won't Start
# View logs
docker logs wdg-wp-my-site
# Check for port conflicts (default HTTPS host port)
lsof -i :6443
# Verify configuration
docker-compose configPermission Issues
# 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-siteNetwork Issues
# Recreate network
docker network rm wdg-network
docker network create wdg-network
# Reset DNS
docker-compose down && docker-compose up -dSee Also: