Skip to content

Networking & SSL

Network architecture, SSL/TLS configuration, and domain routing for multi-project WordPress development.

Network Overview

%%{init: {'theme':'neutral'}}%%
graph TB
    Browser[Browser<br/>:6443] -->|HTTPS| Nginx
    Claude[Claude Desktop] -->|HTTP :6765| MCP

    subgraph "Host Machine"
        Nginx[Nginx Proxy<br/>SSL Termination<br/>host :6443 → :443]
    end

    subgraph "Docker Network (wdg-network, external)"
        Nginx -->|HTTP :80| WP1[WordPress<br/>my-site]
        Nginx -->|HTTP :80| WP2[WordPress<br/>client-site]
        Nginx -->|HTTP :80| WP3[WordPress<br/>demo]

        WP1 -->|:3306| MySQL
        WP2 -->|:3306| MySQL
        WP3 -->|:3306| MySQL

        MCP[MCP Server<br/>container :8765] -->|:6333| Qdrant
    end

Port Mapping

External Ports (Host Machine)

Defaults come from .env.defaults and are configurable via .env / wdg ports.

ServiceHost PortProtocolPurpose
Nginx HTTP6080HTTPDevelopment access
Nginx HTTPS6443HTTPSSSL access
MySQL6306MySQLDatabase access
Qdrant API6333HTTPVector database
Qdrant gRPC6334gRPCVector database
Indexer6666HTTPIncremental indexing API
MCP Server6765HTTPAI integration (FastMCP)
phpMyAdmin6081HTTPDB management
Dashboard Frontend6300HTTPReact dashboard
Docs6173HTTPVitePress docs

Run on the host, not in Docker:

ServiceHost PortPurpose
MCP Auth Proxy6766OAuth-authenticated remote MCPs (SSE at /sse)
Dashboard Backend6001FastAPI; spawns claude CLI with user credentials

Internal Ports (Docker Network)

These are container-internal and are mapped to the host ports above. They are not directly reachable on the host except through their published port.

ServiceContainer PortPurpose
WordPress80HTTP (internal only)
MySQL3306Database connections
Qdrant6333, 6334Vector DB
Indexer8666Indexing API (published as host 6666)
MCP8765MCP protocol (published as host 6765)
Dashboard Frontend3000React dev server (published as host 6300)
Docs5173VitePress (published as host 6173)

Domain Routing

Nginx Configuration

The base server config is services/nginx/default.conf, mounted into the container at /etc/nginx/conf.d/default.conf (read-only). Per-project server blocks live in services/nginx/sites-enabled/ (mounted at /etc/nginx/sites-enabled/); services/nginx/sites-available/ holds the source copies. There is no services/nginx/nginx.conf — the container uses its stock top-level nginx.conf, which already includes conf.d/*.conf.

nginx
# services/nginx/default.conf (illustrative)
upstream wordpress-my-site {
    server wdg-wp-my-site:80;
}

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

# Per-project server blocks
include /etc/nginx/sites-enabled/*.conf;

Per-Project Configuration

services/nginx/sites-enabled/my-site.conf:

nginx
server {
    listen 443 ssl http2;
    server_name my-site.localhost;

    ssl_certificate /etc/nginx/ssl/my-site.localhost.crt;
    ssl_certificate_key /etc/nginx/ssl/my-site.localhost.key;

    root /var/www/html;
    index index.php;

    # WordPress permalinks
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # PHP handling
    location ~ \.php$ {
        proxy_pass http://wdg-wp-my-site;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }

    # Static files
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Deny access to sensitive files
    location ~ /\. {
        deny all;
    }

    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }
}

# HTTP to HTTPS redirect
server {
    listen 80;
    server_name my-site.localhost;
    return 301 https://$server_name$request_uri;
}

SSL/TLS Configuration

Certificate Generation

Self-signed certificates are auto-generated:

bash
# Generate certificate for project
wdg ssl generate my-site

# Manual generation
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
    -keyout ssl/my-site.localhost.key \
    -out ssl/my-site.localhost.crt \
    -subj "/CN=my-site.localhost"

Certificate Trust

macOS

bash
# Add to keychain
sudo security add-trusted-cert -d -r trustRoot \
    -k /Library/Keychains/System.keychain \
    ssl/my-site.localhost.crt

Linux

bash
# Copy to trusted certificates
sudo cp ssl/my-site.localhost.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

Windows

powershell
# Import certificate
Import-Certificate -FilePath ssl\my-site.localhost.crt -CertStoreLocation Cert:\LocalMachine\Root

Wildcard Certificates

For multiple subdomains:

bash
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
    -keyout ssl/wildcard.localhost.key \
    -out ssl/wildcard.localhost.crt \
    -subj "/CN=*.localhost" \
    -addext "subjectAltName=DNS:*.localhost,DNS:localhost"

Docker Networking

Network Configuration

wdg-network is an external bridge network created before compose runs. It is referenced as external, not defined inline with a driver/ipam block:

yaml
networks:
  wdg-network:
    external: true

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

Service Discovery

Containers resolve each other by service name:

php
// From WordPress container
$db_host = 'mysql';  // Resolves to MySQL container
$vector_db = 'qdrant:6333';  // Resolves to Qdrant

Network Inspection

bash
# View network details
docker network inspect wdg-network

# List connected containers
docker network inspect wdg-network --format='{{json .Containers}}' | jq

# Test connectivity
docker exec wdg-wp-my-site ping -c 3 mysql

Load Balancing

Multiple Project Instances

For high-traffic projects:

nginx
upstream wordpress-my-site {
    least_conn;  # Load balancing method
    server wdg-wp-my-site-1:80;
    server wdg-wp-my-site-2:80;
    server wdg-wp-my-site-3:80;
}

Health Checks

nginx
upstream wordpress-my-site {
    server wdg-wp-my-site:80 max_fails=3 fail_timeout=30s;
    check interval=3000 rise=2 fall=5 timeout=1000 type=http;
}

WebSocket Support

For hot-reload and real-time features:

nginx
location /ws {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
}

Custom Headers

WordPress HTTPS Detection

nginx
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

Security Headers

nginx
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Performance Optimization

Caching

nginx
# Browser caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# FastCGI cache
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=WORDPRESS:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

Compression

nginx
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript
           application/json application/javascript application/xml+rss;

Connection Tuning

nginx
keepalive_timeout 65;
keepalive_requests 100;
client_max_body_size 64M;

Firewall Configuration

UFW (Linux)

bash
# Allow required ports (default host ports)
sudo ufw allow 6443/tcp  # HTTPS
sudo ufw allow 6080/tcp  # HTTP
sudo ufw allow 6306/tcp  # MySQL (if external access needed)

# Enable firewall
sudo ufw enable

macOS Firewall

bash
# Allow incoming connections for Docker
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add /Applications/Docker.app

DNS Configuration

Local Development

Add to /etc/hosts:

127.0.0.1 my-site.localhost
127.0.0.1 client-site.localhost
127.0.0.1 demo.localhost

Wildcard DNS (dnsmasq)

bash
# Install dnsmasq
brew install dnsmasq  # macOS
# apt-get install dnsmasq  # Linux

# Configure wildcard
echo "address=/.localhost/127.0.0.1" >> /usr/local/etc/dnsmasq.conf

# Restart
sudo brew services restart dnsmasq

Network Troubleshooting

Check Port Availability

bash
# macOS/Linux
lsof -i :6443

# Check all WDG host ports (defaults)
for port in 6080 6443 6306 6333 6334 6666 6765 6766 6081 6300 6173 6001; do
    lsof -i :$port
done

Test SSL

bash
# Check certificate
openssl s_client -connect my-site.localhost:6443

# Verify certificate
openssl x509 -in ssl/my-site.localhost.crt -text -noout

Debug Nginx

bash
# Test configuration
docker exec wdg-nginx nginx -t

# Reload configuration
docker exec wdg-nginx nginx -s reload

# View error log
docker logs wdg-nginx

Network Connectivity

bash
# Test from container
docker exec wdg-wp-my-site curl -I https://my-site.localhost:6443

# Test database connection
docker exec wdg-wp-my-site mysql -h mysql -u wordpress -pwordpress -e "SELECT 1"

See Also: