Compare commits

...
4 Commits
Author SHA1 Message Date
poprhythm 22166f116a Merge branch 'main' of https://gitea.kolpacksoftware.com/homelab/docker-infrastructure 2026-01-25 14:45:51 +00:00
poprhythm 73d80ea2b2 uptime-kuma config 2026-01-25 14:43:24 +00:00
poprhythm d84e412532 Add nginx-proxy-acme 2026-01-25 14:39:16 +00:00
poprhythm 57590e63b1 add claude.md 2026-01-25 14:33:27 +00:00
36 changed files with 695 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview
This is a homelab Docker infrastructure repository containing ~30 self-hosted services. Each service has its own directory with a `docker-compose.yaml` (or `.yml`) file. All services use pre-built Docker images - there are no custom Dockerfiles.
## Common Commands
```bash
# Start a service
cd <service-name> && docker compose up -d
# Stop a service
cd <service-name> && docker compose down
# View logs
docker compose logs -f <service-name>
# Reload nginx proxy config after changing vhost.d or conf.d
docker exec nginx-proxy nginx -s reload
```
## Architecture
### Network Architecture
All proxied services connect to `npm-network`, an external Docker network shared with the nginx reverse proxy. Services define this in their compose file:
```yaml
networks:
npm-network:
external: true
```
Some services have additional internal networks (e.g., `mqtt-network` for mosquitto).
### Reverse Proxy (nginx-proxy-acme)
The primary ingress is `nginx-proxy-acme/`, which auto-discovers Docker containers and issues Let's Encrypt certificates. To expose a service:
```yaml
environment:
- VIRTUAL_HOST=myapp.kolpacksoftware.com
- VIRTUAL_PORT=8080
- LETSENCRYPT_HOST=myapp.kolpacksoftware.com
```
**Access control:**
- Private hosts: Include `vhost.d/private` which restricts to 192.168.1.0/24 and Docker networks
- Public hosts: No vhost.d file (only default security headers apply)
- To make a host private: `echo 'include /etc/nginx/vhost.d/private;' | sudo tee /srv/nginx-proxy-acme/vhost.d/<hostname>`
**Static backends** (non-Docker services) are configured in `conf.d/static-upstreams.conf` and require adding the domain to the `static-certs` container's environment variables.
### Storage Patterns
- **Bind mounts**: Most services use `/srv/<service-name>/` paths
- **NFS mounts**: Some services (immich, calibre, backrest, filebrowser-colleen-hd) use NFS volumes pointing to 192.168.1.4 or 192.168.1.192
### Automation
- **watchtower**: Monitors and auto-updates container images every 5 minutes
- **netdata**: Real-time monitoring with alerting at port 19999
## Key Configuration Locations
| Path | Purpose |
|------|---------|
| `/srv/nginx-proxy-acme/vhost.d/` | Per-host nginx config (access control) |
| `/srv/nginx-proxy-acme/conf.d/` | Global nginx config (WAF rules, static backends) |
| `<service>/docker-compose.yaml` | Service definition |
| `<service>/.env` | Service-specific environment (when present) |
## Environment Variables
Common patterns across services:
- `PUID=1000`, `PGID=1000` for file permissions
- `TZ=America/New_York` for timezone
- `VIRTUAL_HOST`, `VIRTUAL_PORT`, `LETSENCRYPT_HOST` for proxy integration
+2
View File
@@ -0,0 +1,2 @@
# Let's Encrypt notification email
LETSENCRYPT_EMAIL=your-email@example.com
+142
View File
@@ -0,0 +1,142 @@
# nginx-proxy + acme-companion
Automated reverse proxy with Let's Encrypt SSL certificates.
## Setup
1. Create the data directories:
```bash
sudo mkdir -p /srv/nginx-proxy-acme/{certs,vhost.d,html,conf.d,acme}
```
2. Copy configuration files:
```bash
sudo cp conf/conf.d/* /srv/nginx-proxy-acme/conf.d/
sudo cp conf/vhost.d/* /srv/nginx-proxy-acme/vhost.d/
```
3. Configure environment:
```bash
cp .env.example .env
# Edit .env with your email
```
4. Start the proxy:
```bash
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:
```bash
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:
```yaml
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:
1. Add server block to `conf.d/static-upstreams.conf`
2. Add domain to `static-certs` container's VIRTUAL_HOST and LETSENCRYPT_HOST in docker-compose.yaml
3. 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:
```bash
docker exec nginx-proxy nginx -s reload
```
## Multiple Domains
For multiple domains on one cert:
```yaml
environment:
- VIRTUAL_HOST=example.com,www.example.com
- LETSENCRYPT_HOST=example.com,www.example.com
```
Create vhost.d entries for each domain if private.
@@ -0,0 +1,134 @@
# Block SQL injections
set $block_sql_injections 0;
if ($query_string ~ "union.*select.*\(") {
set $block_sql_injections 1;
}
if ($query_string ~ "union.*all.*select.*") {
set $block_sql_injections 1;
}
if ($query_string ~ "concat.*\(") {
set $block_sql_injections 1;
}
if ($block_sql_injections = 1) {
return 403;
}
# Block file injections
set $block_file_injections 0;
if ($query_string ~ "[a-zA-Z0-9_]=http://") {
set $block_file_injections 1;
}
if ($query_string ~ "[a-zA-Z0-9_]=(\.\.//?)+") {
set $block_file_injections 1;
}
if ($query_string ~ "[a-zA-Z0-9_]=/([a-z0-9_.]//?)+") {
set $block_file_injections 1;
}
if ($block_file_injections = 1) {
return 403;
}
# Block common exploits
set $block_common_exploits 0;
if ($query_string ~ "(<|%3C).*script.*(>|%3E)") {
set $block_common_exploits 1;
}
if ($query_string ~ "GLOBALS(=|\[|\%[0-9A-Z]{0,2})") {
set $block_common_exploits 1;
}
if ($query_string ~ "_REQUEST(=|\[|\%[0-9A-Z]{0,2})") {
set $block_common_exploits 1;
}
if ($query_string ~ "proc/self/environ") {
set $block_common_exploits 1;
}
if ($query_string ~ "mosConfig_[a-zA-Z_]{1,21}(=|\%3D)") {
set $block_common_exploits 1;
}
if ($query_string ~ "base64_(en|de)code\(.*\)") {
set $block_common_exploits 1;
}
if ($block_common_exploits = 1) {
return 403;
}
# Block spam
set $block_spam 0;
if ($query_string ~ "\b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)\b") {
set $block_spam 1;
}
if ($query_string ~ "\b(erections|hoodia|huronriveracres|impotence|levitra|libido)\b") {
set $block_spam 1;
}
if ($query_string ~ "\b(ambien|blue\spill|cialis|cocaine|ejaculation|erectile)\b") {
set $block_spam 1;
}
if ($query_string ~ "\b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)\b") {
set $block_spam 1;
}
if ($block_spam = 1) {
return 403;
}
# Block user agents
set $block_user_agents 0;
if ($http_user_agent ~ "Indy Library") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "libwww-perl") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "GetRight") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "GetWeb!") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "Go!Zilla") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "Download Demon") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "Go-Ahead-Got-It") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "TurnitinBot") {
set $block_user_agents 1;
}
if ($http_user_agent ~ "GrabNet") {
set $block_user_agents 1;
}
if ($block_user_agents = 1) {
return 403;
}
@@ -0,0 +1,219 @@
# Static upstream configurations for non-container backends
# These hosts forward to IP addresses instead of Docker containers
# ------------------------------------------------------------------------------
# portainer.kolpacksoftware.com -> 172.17.0.1:9443 (HTTPS backend via Docker bridge)
# ------------------------------------------------------------------------------
upstream portainer-backend {
server 172.17.0.1:9443;
}
server {
listen 80;
server_name portainer.kolpacksoftware.com;
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name portainer.kolpacksoftware.com;
ssl_certificate /etc/nginx/certs/portainer.kolpacksoftware.com.crt;
ssl_certificate_key /etc/nginx/certs/portainer.kolpacksoftware.com.key;
include /etc/nginx/vhost.d/default;
include /etc/nginx/vhost.d/private;
location / {
proxy_pass https://portainer-backend;
proxy_ssl_verify off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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;
}
}
# ------------------------------------------------------------------------------
# btt-cb1.kolpacksoftware.com -> 192.168.1.173:80
# ------------------------------------------------------------------------------
upstream btt-cb1-backend {
server 192.168.1.173:80;
}
server {
listen 80;
server_name btt-cb1.kolpacksoftware.com;
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name btt-cb1.kolpacksoftware.com;
ssl_certificate /etc/nginx/certs/btt-cb1.kolpacksoftware.com.crt;
ssl_certificate_key /etc/nginx/certs/btt-cb1.kolpacksoftware.com.key;
include /etc/nginx/vhost.d/default;
include /etc/nginx/vhost.d/private;
location / {
proxy_pass http://btt-cb1-backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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;
}
}
# ------------------------------------------------------------------------------
# hats.kolpacksoftware.com -> 192.168.1.66:9999
# ------------------------------------------------------------------------------
upstream hats-backend {
server 192.168.1.66:9999;
}
server {
listen 80;
server_name hats.kolpacksoftware.com;
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name hats.kolpacksoftware.com;
ssl_certificate /etc/nginx/certs/hats.kolpacksoftware.com.crt;
ssl_certificate_key /etc/nginx/certs/hats.kolpacksoftware.com.key;
include /etc/nginx/vhost.d/default;
include /etc/nginx/vhost.d/private;
location / {
proxy_pass http://hats-backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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;
}
}
# ------------------------------------------------------------------------------
# pve-nas.kolpacksoftware.com -> 192.168.1.245:8006 (HTTPS backend - Proxmox)
# ------------------------------------------------------------------------------
upstream pve-nas-backend {
server 192.168.1.245:8006;
}
server {
listen 80;
server_name pve-nas.kolpacksoftware.com;
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name pve-nas.kolpacksoftware.com;
ssl_certificate /etc/nginx/certs/pve-nas.kolpacksoftware.com.crt;
ssl_certificate_key /etc/nginx/certs/pve-nas.kolpacksoftware.com.key;
include /etc/nginx/vhost.d/default;
include /etc/nginx/vhost.d/private;
# Proxmox needs larger buffers
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
location / {
proxy_pass https://pve-nas-backend;
proxy_ssl_verify off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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;
}
}
# ------------------------------------------------------------------------------
# unraid.kolpacksoftware.com -> 192.168.1.192:80
# ------------------------------------------------------------------------------
upstream unraid-backend {
server 192.168.1.192:80;
}
server {
listen 80;
server_name unraid.kolpacksoftware.com;
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name unraid.kolpacksoftware.com;
ssl_certificate /etc/nginx/certs/unraid.kolpacksoftware.com.crt;
ssl_certificate_key /etc/nginx/certs/unraid.kolpacksoftware.com.key;
include /etc/nginx/vhost.d/default;
include /etc/nginx/vhost.d/private;
location / {
proxy_pass http://unraid-backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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;
}
}
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
public
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
+12
View File
@@ -0,0 +1,12 @@
# Default configuration applied to all virtual hosts
# Security headers and WAF rules - applied to ALL hosts (public and private)
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Include block-exploits rules
include /etc/nginx/conf.d/block-exploits.conf;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1,6 @@
# Docker registry needs Authorization header passthrough
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
# Private access control
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
public
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
+1
View File
@@ -0,0 +1 @@
public
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
+7
View File
@@ -0,0 +1,7 @@
# Access control for private hosts
# Symlink this file to restrict access to local networks only
allow 192.168.1.0/24;
allow 172.16.0.0/12;
# allow 162.10.130.22; # Netskope - uncomment if needed for corporate VPN
deny all;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
+1
View File
@@ -0,0 +1 @@
public
+1
View File
@@ -0,0 +1 @@
public
@@ -0,0 +1 @@
public
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
public
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
@@ -0,0 +1 @@
include /etc/nginx/vhost.d/private;
+1
View File
@@ -0,0 +1 @@
public
+48
View File
@@ -0,0 +1,48 @@
services:
nginx-proxy:
image: nginxproxy/nginx-proxy:1.6
container_name: nginx-proxy
restart: unless-stopped
ports:
- '80:80'
- '443:443'
volumes:
- /srv/nginx-proxy-acme/certs:/etc/nginx/certs:ro
- /srv/nginx-proxy-acme/vhost.d:/etc/nginx/vhost.d
- /srv/nginx-proxy-acme/html:/usr/share/nginx/html
- /srv/nginx-proxy-acme/conf.d/static-upstreams.conf:/etc/nginx/conf.d/static-upstreams.conf:ro
- /srv/nginx-proxy-acme/conf.d/block-exploits.conf:/etc/nginx/conf.d/block-exploits.conf:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
environment:
- TRUST_DOWNSTREAM_PROXY=false
acme-companion:
image: nginxproxy/acme-companion:2.4
container_name: acme-companion
restart: unless-stopped
volumes_from:
- nginx-proxy
volumes:
- /srv/nginx-proxy-acme/certs:/etc/nginx/certs:rw
- /srv/nginx-proxy-acme/acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- DEFAULT_EMAIL=${LETSENCRYPT_EMAIL}
depends_on:
- nginx-proxy
# Dummy container to trigger certificate issuance for static IP backends
# This container does nothing but hold env vars for acme-companion to detect
static-certs:
image: alpine:3.19
container_name: static-certs
restart: unless-stopped
command: ["sleep", "infinity"]
environment:
- VIRTUAL_HOST=portainer.kolpacksoftware.com,btt-cb1.kolpacksoftware.com,hats.kolpacksoftware.com,pve-nas.kolpacksoftware.com,unraid.kolpacksoftware.com
- LETSENCRYPT_HOST=portainer.kolpacksoftware.com,btt-cb1.kolpacksoftware.com,hats.kolpacksoftware.com,pve-nas.kolpacksoftware.com,unraid.kolpacksoftware.com
networks:
default:
name: npm-network
external: true
+18
View File
@@ -0,0 +1,18 @@
services:
uptime-kuma:
image: louislam/uptime-kuma:1
container_name: uptime-kuma
restart: unless-stopped
volumes:
- /srv/uptime-kuma/data:/app/data
environment:
- TZ=America/New_York
- VIRTUAL_HOST=uptime.kolpacksoftware.com
- VIRTUAL_PORT=3001
- LETSENCRYPT_HOST=uptime.kolpacksoftware.com
networks:
- npm-network
networks:
npm-network:
external: true