Automatic reverse proxy for Docker containers - just add labels to your containers and they're instantly accessible.
Route traffic to your Docker containers without manually editing config files:
- HTTP Routing: Access containers by hostname (api.example.com, web.local)
- TCP/UDP Proxying: Expose databases, SSH, DNS, or any TCP/UDP service
- Zero Configuration: Containers auto-register when they start
- No Downtime: Config updates happen automatically without interrupting traffic
- Perfect for Cloudflare Tunnels: Route multiple services through a single tunnel
-
Create the proxy network:
docker network create --driver bridge proxy-network
-
Pull the image:
docker pull ghcr.io/moontechs/proxy:latest
-
Create a docker-compose.yml:
version: '3.8' services: proxy: image: ghcr.io/moontechs/proxy:latest restart: unless-stopped ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro networks: - proxy-network # Your application - accessed at http://app.local webapp: image: nginx:alpine labels: proxy.http.host: "app.local" proxy.http.port: "80" networks: - proxy-network networks: proxy-network: name: proxy-network external: true # Use the network created in step 1
-
Start it:
docker-compose up -d
-
Add to /etc/hosts (for local testing):
echo "127.0.0.1 app.local" | sudo tee -a /etc/hosts
-
Visit http://app.local - your container is live!
# Latest stable version
docker pull ghcr.io/moontechs/proxy:latest
# Specific version
docker pull ghcr.io/moontechs/proxy:1.0.0latest- Most recent release1.0.0- Specific version1.0- Latest patch for 1.0.x1- Latest minor for 1.x
Access containers by domain name - perfect for web services and APIs:
services:
api:
image: my-api:latest
labels:
proxy.http.host: "api.example.com"
proxy.http.port: "8080" # Optional, defaults to 80
networks:
- proxy-network
web:
image: my-web:latest
labels:
proxy.http.host: "web.example.com,www.example.com" # Multiple domains
proxy.http.port: "3000"
networks:
- proxy-networkTraffic to api.example.com → your API container
Traffic to web.example.com → your web container
Expose databases, SSH servers, or any TCP service:
services:
postgres:
image: postgres:16
labels:
proxy.tcp.ports: "5432:5432" # Host port:Container port
networks:
- proxy-network
ssh-server:
image: linuxserver/openssh-server
labels:
proxy.tcp.ports: "22:2222" # Expose container's 2222 as host's 22
networks:
- proxy-networkConnect to PostgreSQL: psql -h localhost -p 5432
Connect to SSH: ssh user@localhost -p 22
For DNS, VPN, or other UDP services:
services:
dns:
image: adguard/adguardhome
labels:
proxy.tcp.ports: "53:53" # DNS over TCP
proxy.udp.ports: "53:53" # DNS over UDP
networks:
- proxy-networkSame container can be accessed both ways:
services:
dev-server:
image: my-dev-env
labels:
proxy.tcp.ports: "22:22" # SSH access
proxy.http.host: "dev.local" # Web interface
proxy.http.port: "8080"
networks:
- proxy-networkRun multiple services through a single Cloudflare tunnel:
# Cloudflare tunnel config
ingress:
- hostname: api.example.com
service: http://proxy:80
- hostname: web.example.com
service: http://proxy:80
- service: http_status:404
# Your containers
services:
api:
labels:
proxy.http.host: "api.example.com"
proxy.http.port: "8080"
web:
labels:
proxy.http.host: "web.example.com"
proxy.http.port: "3000"The proxy inspects the Host header and routes to the correct container.
Best Practice: Services should use both an external proxy network AND their own local network for internal components.
Pattern:
- External Network (
proxy-network): For proxy communication - Local Network (
your-service-local-network): For service-specific components (databases, cache, etc.)
This keeps your databases and internal services isolated from the proxy while allowing your web application to be accessible.
Example with database isolation:
services:
# Web application - accessible via proxy
webapp:
image: my-webapp
labels:
proxy.http.host: "app.example.com"
proxy.http.port: "3000"
networks:
- proxy-network # External - proxy can reach this
- webapp-local # Local - for database access
environment:
- DATABASE_URL=postgresql://postgres:5432/myapp
# Database - NOT accessible via proxy
postgres:
image: postgres:16
networks:
- webapp-local # Only on local network
# No proxy labels - isolated from external access
networks:
proxy-network:
name: proxy-network
external: true
webapp-local:
name: webapp-local-network
driver: bridgeWhy this works:
webappis on both networks, so it can:- Receive traffic from the proxy (via
proxy-network) - Connect to the database (via
webapp-local)
- Receive traffic from the proxy (via
postgresis only onwebapp-local, so it's isolated from the proxy
Real-world example with Cloudflare Tunnel:
# Proxy with Cloudflare
services:
proxy:
image: ghcr.io/moontechs/proxy:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- "80:80"
- "443:443"
networks:
- proxy-network
- proxy-local # For cloudflared
cloudflared:
image: cloudflare/cloudflared
command: tunnel run
environment:
- TUNNEL_TOKEN=your-token-here
networks:
- proxy-local # Only needs to reach proxy, not services
networks:
proxy-network:
name: proxy-network
external: true
proxy-local:
driver: bridge
---
# Your application (separate docker-compose.yml)
services:
app:
image: my-app
labels:
proxy.http.host: "app.example.com"
proxy.http.port: "8080"
networks:
- proxy-network # External - receives traffic from proxy
- app-local # Local - for postgres
environment:
- DATABASE_URL=postgresql://postgres:5432/app
postgres:
image: postgres:16
networks:
- app-local # Isolated from proxy
networks:
proxy-network:
name: proxy-network
external: true
app-local:
driver: bridgeSecurity benefits:
- Databases are never exposed to the proxy network
- Each service has its own isolated network for internal components
- Proxy can only reach services that explicitly join
proxy-network - No need to expose database ports on the host
# Logging
LOG_LEVEL=INFO # DEBUG or INFO
LOG_CALLER=false # Show source code location in logs
# Docker
DOCKER_HOST=unix:///var/run/docker.sock # Docker socket path
PROXY_NETWORK=proxy-network # Docker network name
# Nginx
NGINX_WORKER_CONNECTIONS=1000 # Max connections per workerHTTP Routing:
labels:
proxy.http.host: "example.com" # Required: hostname(s) - comma-separated for multiple
proxy.http.port: "80" # Optional: container port (default: 80)
proxy.http.https: "false" # Optional: use port 443 (default: false)TCP Proxying:
labels:
proxy.tcp.ports: "80:8080,443:8443" # host_port:container_port - comma-separatedUDP Proxying:
labels:
proxy.udp.ports: "53:53,5353:5300" # host_port:container_port - comma-separatedPort format:
80:8080- Different ports (host 80 → container 8080)53- Same port on both sides (shorthand for53:53)
Check container labels:
docker inspect <container-name> | jq '.[0].Config.Labels'Make sure the container has proxy labels and is on the proxy-network.
Verify container is on proxy-network:
# List all containers on proxy-network
docker ps --format "{{.Names}}\t{{.Networks}}" | grep proxy-networkIf your container is missing from this list, add it:
# In your docker-compose.yml
services:
your-service:
networks:
- proxy-network # Add this!
networks:
proxy-network:
name: proxy-network
external: trueVerify container is running:
docker ps | grep <container-name>Check proxy logs:
docker logs proxyVerify Nginx config:
docker exec proxy nginx -tFor local testing, add to /etc/hosts:
sudo sh -c 'echo "127.0.0.1 app.local api.local" >> /etc/hosts'For production, configure DNS to point to your proxy server.
Enable debug logging to see what's happening:
docker-compose down
LOG_LEVEL=DEBUG docker-compose upLook for logs like:
[INFO] registered_container name=webapp tcp_ports=0 udp_ports=0 http_hosts=1
[INFO] Nginx reload successful
You'll see errors like:
ERROR: TCP port conflict: port 80 claimed by both webapp and api-server
Each TCP/UDP port can only be used by one container. For HTTP, use different hostnames instead.
See detailed guides:
- DEBUG_OUTPUT.md - Understanding debug logs
- examples/ - Working examples
Run proxy in multiple separate docker-compose projects:
# proxy/docker-compose.yml
services:
proxy:
image: ghcr.io/moontechs/proxy:latest
environment:
PROXY_NETWORK: "proxy-network"
networks:
- proxy-network
networks:
proxy-network:
name: proxy-network
external: true # Created automatically by proxy
# services/docker-compose.yml (separate directory)
services:
app:
image: my-app
labels:
proxy.http.host: "app.local"
networks:
- proxy-network
networks:
proxy-network:
name: proxy-network
external: trueThe proxy automatically creates the network on startup.
Conflict Detection:
TCP port conflict: port 80 claimed by both webapp and api-server- Two containers trying to use same TCP portUDP port conflict: port 53 claimed by both dns1 and dns2- Two containers trying to use same UDP portHTTP hostname conflict: api.local claimed by both api-v1 and api-v2- Two containers using same hostname
No conflict: HTTP + TCP on the same port is allowed (different Nginx modules):
labels:
proxy.tcp.ports: "22:22" # SSH via port 22
proxy.http.host: "admin.local" # Web via hostname- QUICKSTART.md - Detailed quick start guide
- DEBUG_OUTPUT.md - Understanding logs and debugging
- examples/ - Working example configurations
Requirements:
- Go 1.24+
- Docker
- Make
Build binary:
make build
# or
go build -o bin/proxy .Build Docker image:
make docker-build
# or
docker build -t proxy:latest .Run tests:
make test
# or
go test -v ./...make help # Show all commands
make check # Run linters and tests
make install-tools # Install development toolsProcess:
- On container startup, scans Docker for running containers
- Reads container labels for proxy configuration
- Generates Nginx config files
- Starts Nginx with generated configs
Two routing modes:
- Stream Module (Layer 4): Raw TCP/UDP port forwarding
- HTTP Module (Layer 7): Virtual host routing by hostname
Key components:
- cmd/ - CLI commands (generate.go, root.go)
- config/ - Configuration management
- docker/ - Docker client and container scanning
- nginx/ - Nginx config generation (generator.go, templates.go, reloader.go)
- examples/ - Example setups
- main.go - CLI entry point
proxy generate:
- Scans running containers and generates Nginx configs
- Used during container startup via entrypoint
Stream config (/etc/nginx/conf.d/proxy.conf):
upstream tcp_5432 {
server 172.17.0.2:5432;
}
server {
listen 5432;
proxy_pass tcp_5432;
proxy_connect_timeout 10s;
proxy_timeout 5m;
}HTTP config (/etc/nginx/conf.d/http-proxy.conf):
upstream http_api_example_com {
server 172.17.0.3:8080;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://http_api_example_com;
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 $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}MIT