nginx-proxy + acme-companion
Automated reverse proxy with Let's Encrypt SSL certificates.
Setup
-
Create the data directories:
sudo mkdir -p /srv/nginx-proxy-acme/{certs,vhost.d,html,conf.d,acme} -
Copy configuration files:
sudo cp conf/conf.d/* /srv/nginx-proxy-acme/conf.d/ sudo cp conf/vhost.d/* /srv/nginx-proxy-acme/vhost.d/ -
Configure environment:
cp .env.example .env # Edit .env with your email -
Start the proxy:
docker compose up -d
Architecture
- nginx-proxy: Reverse proxy with auto-discovery of Docker containers
- acme-companion: Automatic Let's Encrypt certificate management
- static-certs: Dummy container that triggers cert issuance for non-container backends
Security
All hosts receive these security features via vhost.d/default:
- HSTS: Strict-Transport-Security header (1 year)
- Security headers: X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy
- Block exploits: WAF rules against SQL injection, XSS, file injection, spam, bad user agents
Access Control
Private Hosts (IP restricted)
Private hosts include vhost.d/private which restricts access to:
- 192.168.1.0/24 (local network)
- 172.16.0.0/12 (Docker networks)
To make a new host private, create a vhost.d file:
echo 'include /etc/nginx/vhost.d/private;' | sudo tee /srv/nginx-proxy-acme/vhost.d/myapp.kolpacksoftware.com
Public Hosts
Public hosts have no vhost.d file (only default applies). They get security headers and block-exploits but no IP restrictions.
Current public hosts:
- chd.kolpacksoftware.com
- linkding.kolpacksoftware.com
- organizer.rmstsa.org
- ridge-resources.org
- rmstsa.org / www.rmstsa.org
- share.kolpacksoftware.com
- vikunja.kolpacksoftware.com
Adding a Proxied Service
Add these environment variables to any container:
services:
myapp:
image: myapp:latest
environment:
- VIRTUAL_HOST=myapp.kolpacksoftware.com
- VIRTUAL_PORT=8080
- LETSENCRYPT_HOST=myapp.kolpacksoftware.com
networks:
- npm-network
networks:
npm-network:
external: true
Then if private, add the vhost.d file as shown above.
Static IP Backends
Services running on physical hosts or VMs (not Docker) are configured in conf.d/static-upstreams.conf:
| Domain | Backend |
|---|---|
| portainer.kolpacksoftware.com | 172.17.0.1:9443 |
| btt-cb1.kolpacksoftware.com | 192.168.1.173:80 |
| hats.kolpacksoftware.com | 192.168.1.66:9999 |
| pve-nas.kolpacksoftware.com | 192.168.1.245:8006 |
| unraid.kolpacksoftware.com | 192.168.1.192:80 |
All static backends are private (IP restricted).
To add a new static backend:
- Add server block to
conf.d/static-upstreams.conf - Add domain to
static-certscontainer's VIRTUAL_HOST and LETSENCRYPT_HOST in docker-compose.yaml - Reload:
docker exec nginx-proxy nginx -s reload
Directory Structure
/srv/nginx-proxy-acme/
├── acme/ # acme.sh state (auto-managed)
├── certs/ # SSL certificates (auto-managed)
├── conf.d/
│ ├── block-exploits.conf # WAF rules
│ └── static-upstreams.conf # Static IP backend server blocks
├── html/ # ACME challenge files (auto-managed)
└── vhost.d/
├── default # Security headers + HSTS + block-exploits (all hosts)
├── private # IP allowlist (private hosts include this)
├── docker-registry.* # Special config (auth headers + private)
└── <hostname> # Per-host includes (usually just 'include private')
Reload Config
After changing vhost.d or conf.d files:
docker exec nginx-proxy nginx -s reload
Multiple Domains
For multiple domains on one cert:
environment:
- VIRTUAL_HOST=example.com,www.example.com
- LETSENCRYPT_HOST=example.com,www.example.com
Create vhost.d entries for each domain if private.