Self-Hosting with Docker & Nginx Reverse Proxy
This is a documentation of how I set up my VPS to host multiple services using Docker containers with a single Nginx reverse proxy handling SSL termination and routing.
The Architecture
The setup follows a simple but effective pattern: one main Nginx container acts as the entry point, handling all incoming traffic on ports 80 and 443. It terminates SSL and proxies requests to internal containers based on the domain.
INTERNET
|
ports 80, 443
v
+--------------------------------------------+
| main_nginx (reverse proxy) |
| - SSL termination (Let's Encrypt) |
| - HTTP -> HTTPS redirect |
| - Routes to backend containers |
+--------------------+-----------------------+
|
+------------+------------+
| |
v v
+---------------+ +---------------+
| sepioca-site | | waiz_api |
| (nginx:80) | | (nginx:80) |
| | | |
| sepioca.com | api.waiz.sepioca.com |
+---------------+ +---------------+
Docker Compose Setup
Each service has its own docker-compose.yml. The key is the
shared Docker network that allows containers to communicate by name.
Main Reverse Proxy
# ~/sepioca-vpc/docker-compose.yml
services:
nginx:
image: nginx
container_name: main_nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- /etc/letsencrypt:/etc/letsencrypt:ro
- /var/www/certbot:/var/www/certbot
networks:
- main_network
networks:
main_network:
name: main_network
Application Containers
# ~/sepioca-site/docker-compose.yml
services:
sepioca:
image: nginx
container_name: sepioca-site
networks:
- main_network # joins shared network
- internal # own internal network
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./src:/usr/share/nginx/html/mysite
networks:
main_network:
external: true # already created by main compose
internal:
driver: bridge
Nginx Reverse Proxy Config
The main nginx config handles SSL and proxies to containers by their Docker network hostname:
http {
include mime.types;
# HTTP -> HTTPS redirect
server {
listen 80;
server_name sepioca.com *.sepioca.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
# Main site
server {
listen 443 ssl;
server_name sepioca.com www.sepioca.com;
ssl_certificate /etc/letsencrypt/live/sepioca.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sepioca.com/privkey.pem;
location / {
proxy_pass http://sepioca:80; # container name!
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
# API subdomain
server {
listen 443 ssl;
server_name api.waiz.sepioca.com;
ssl_certificate /etc/letsencrypt/live/api.waiz.sepioca.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.waiz.sepioca.com/privkey.pem;
location / {
proxy_pass http://waiz_api:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
SSL with Let's Encrypt
Certificates are managed via certbot on the host system and mounted
read-only into the container. The /.well-known/acme-challenge/
location allows cert renewal without downtime.
# Initial cert setup
sudo certbot certonly --webroot \
-w /var/www/certbot \
-d sepioca.com \
-d www.sepioca.com
# Auto-renewal via cron
0 0 * * * certbot renew --quiet
Deployment Workflow
Each project has a simple Makefile:
spawn:
docker compose up -d
nuke:
docker compose down -v
respawn:
docker compose up -d --force-recreate
Deploying changes:
- Push to GitHub
- SSH to server
cd ~/project && git pullmake respawn
Directory Structure
/home/sepioca/
├── sepioca-vpc/ # Main reverse proxy
│ ├── docker-compose.yml
│ ├── nginx.conf
│ └── Makefile
├── sepioca-site/ # This website
│ ├── docker-compose.yml
│ ├── nginx.conf
│ ├── src/
│ └── Makefile
└── waiz_api/ # API service
├── docker-compose.yml
├── nginx.conf
├── src/
└── Makefile
Key Takeaways
- Shared network - Containers communicate via Docker network hostnames
- External: true - Lets app containers join the proxy's network
- SSL termination - Only the proxy handles HTTPS; internal traffic is HTTP
- Read-only mounts - Certs mounted as
:rofor security - Simple deploys - Git pull + make respawn