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.

default.conf is generated, not edited. It is rendered from the tracked services/nginx/default.conf.template with substituted from your .env, by install.sh and again by _render_nginx_default_conf on every wdg start. The rendered file is gitignored because the port is machine-local. Edit the template; anything you change in default.conf is overwritten on the next start.

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: