home blog
~/blog/vps-docker-setup.md
2026-04-27
sepioca ~/blog $ cat vps-docker-setup.md

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:

  1. Push to GitHub
  2. SSH to server
  3. cd ~/project && git pull
  4. make 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 :ro for security
  • Simple deploys - Git pull + make respawn

Resources

sepioca ~/blog $