Compare commits
10 Commits
31cc759cd8
...
6f953a090d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f953a090d | ||
|
|
96e3c80ffb | ||
|
|
9ac7fefb3d | ||
|
|
a5cd34d9ab | ||
|
|
58cc1b1e92 | ||
|
|
f818bd3eca | ||
|
|
a924adee27 | ||
|
|
47e640b969 | ||
|
|
4b23c27bc1 | ||
|
|
166641be57 |
53
.env.example
53
.env.example
@@ -1,20 +1,13 @@
|
||||
# .env.example
|
||||
# Copy this file to .env and update the values below
|
||||
|
||||
## Domain / Timezone
|
||||
DOMAIN=your-domain.com
|
||||
TZ=Your/Timezone
|
||||
DOMAIN_PREFIX=gate
|
||||
|
||||
TZ=Africa/Cairo
|
||||
|
||||
## ACME (Let's Encrypt)
|
||||
# Your email address for Let's Encrypt certificate notifications
|
||||
ACME_EMAIL=admin@your-domain.com
|
||||
|
||||
## Basic Authentication
|
||||
# Generate password hash with: echo $(htpasswd -nB user) | sed -e s/\\$/\\$\\$/g
|
||||
# Or use online generator: https://hostingcanada.org/htpasswd-generator/
|
||||
# Format: username:$$2y$$10$$hashed_password
|
||||
BASIC_AUTH_USERS=admin:$$2y$$05$$HIIpotyLuLRMBX3sfUs6E.YM3lP9cpF7hK7sHaSXGs6mw/RS6MXCa
|
||||
|
||||
## Umami (PostgreSQL)
|
||||
# Database user for Umami analytics
|
||||
UMAMI_DB_USER=umami
|
||||
@@ -24,8 +17,6 @@ UMAMI_DB_PASS=your_strong_database_password
|
||||
UMAMI_DB_NAME=umami
|
||||
# Random 64-character secret for Umami app security
|
||||
UMAMI_APP_SECRET=your_64_character_random_secret_here
|
||||
# Website ID from Umami dashboard (use placeholder initially, update after setup)
|
||||
UMAMI_WEBSITE_ID=placeholder-website-id
|
||||
|
||||
## pgAdmin
|
||||
# Default email for pgAdmin login
|
||||
@@ -33,8 +24,36 @@ PGADMIN_DEFAULT_EMAIL=admin@your-domain.com
|
||||
# Strong password for pgAdmin login
|
||||
PGADMIN_DEFAULT_PASSWORD=your_strong_pgadmin_password
|
||||
|
||||
## Grafana
|
||||
# Admin username for Grafana dashboard
|
||||
GRAFANA_ADMIN_USER=admin
|
||||
# Strong password for Grafana admin user
|
||||
GRAFANA_ADMIN_PASS=your_strong_grafana_password
|
||||
## Authelia
|
||||
# Database name for Authelia
|
||||
AUTHELIA_DB_NAME=authelia
|
||||
# Database user for Authelia
|
||||
AUTHELIA_DB_USER=authelia
|
||||
# Database password for Authelia PostgreSQL
|
||||
AUTHELIA_DB_PASSWORD=your_authelia_db_password
|
||||
# JWT secret for Authelia (64+ character random string)
|
||||
AUTHELIA_JWT_SECRET=your_64_character_jwt_secret_here
|
||||
# Session secret for Authelia (64+ character random string)
|
||||
AUTHELIA_SESSION_SECRET=your_64_character_session_secret_here
|
||||
# Storage encryption key for Authelia (64+ character random string)
|
||||
AUTHELIA_STORAGE_ENCRYPTION_KEY=your_storage_encryption_key_here
|
||||
# SMTP password for Authelia notifier
|
||||
AUTHELIA_NOTIFIER_SMTP_PASSWORD=your_smtp_password_here
|
||||
|
||||
## Gitea
|
||||
# Database user for Gitea
|
||||
GITEA_DB_USER=gitea
|
||||
# Database password for Gitea PostgreSQL
|
||||
GITEA_DB_PASSWORD=your_gitea_db_password_here
|
||||
# Database name for Gitea
|
||||
GITEA_DB_NAME=gitea
|
||||
# Secret key for Gitea (64+ character random string - generate with: openssl rand -hex 32)
|
||||
GITEA_SECRET_KEY=your_64_character_gitea_secret_key_here
|
||||
# Internal token for Gitea (generate with: docker run -it --rm docker.gitea.com/gitea:1 gitea generate secret INTERNAL_TOKEN)
|
||||
GITEA_INTERNAL_TOKEN=your_gitea_internal_token_here
|
||||
|
||||
## Duplicati
|
||||
# Settings encryption key for Duplicati (random string for encrypting settings database)
|
||||
DUPLICATI_ENCRYPTION_KEY=your_duplicati_encryption_key_here
|
||||
# Web UI password for Duplicati
|
||||
DUPLICATI_PASSWORD=your_duplicati_password_here
|
||||
|
||||
60
.github/workflows/deploy.yml
vendored
60
.github/workflows/deploy.yml
vendored
@@ -1,60 +0,0 @@
|
||||
# .github/workflows/deploy.yml
|
||||
name: Deploy Gateway to VPS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- "docker-compose.yml"
|
||||
- ".github/workflows/deploy.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REMOTE_DIR: ${{ secrets.REMOTE_DIR }}
|
||||
|
||||
concurrency:
|
||||
group: deploy-prod
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Ship to VPS
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup SSH key
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan -p "${{ secrets.SSH_PORT }}" "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Login to GitHub Container Registry on VPS
|
||||
run: |
|
||||
ssh -p "${{ secrets.SSH_PORT }}" "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" \
|
||||
"echo '${{ secrets.GITHUB_TOKEN }}' | docker login ghcr.io -u '${{ github.actor }}' --password-stdin"
|
||||
|
||||
- name: Deploy (pull, up, prune)
|
||||
run: |
|
||||
ssh -p "${{ secrets.SSH_PORT }}" "${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}" \
|
||||
"export REMOTE_DIR='${{ secrets.REMOTE_DIR }}'; bash -se" <<'EOF'
|
||||
set -euo pipefail
|
||||
cd "$REMOTE_DIR"
|
||||
|
||||
git pull origin main
|
||||
docker compose pull
|
||||
docker compose up -d --remove-orphans
|
||||
docker image prune -af || true
|
||||
EOF
|
||||
|
||||
- name: Post-deploy smoke checks
|
||||
run: |
|
||||
echo "Deployed to ${{ secrets.SSH_HOST }}:${{ secrets.SSH_PORT }} → ${REMOTE_DIR}"
|
||||
echo "Traefik: https://traefik.gate.${{ secrets.DOMAIN }}"
|
||||
echo "Portainer: https://portainer.gate.${{ secrets.DOMAIN }}"
|
||||
echo "Uptime Kuma: https://uptime.gate.${{ secrets.DOMAIN }}"
|
||||
echo "Umami: https://umami.gate.${{ secrets.DOMAIN }}"
|
||||
echo "pgAdmin: https://pgadmin.gate.${{ secrets.DOMAIN }}"
|
||||
70
README.md
70
README.md
@@ -1,28 +1,37 @@
|
||||
# Launchpad Gateway
|
||||
|
||||
A production-ready Traefik-based reverse proxy gateway with automatic SSL/TLS, analytics, monitoring, and container management.
|
||||
A production-ready Traefik-based reverse proxy gateway with automatic SSL/TLS, analytics, monitoring, authentication, and container management.
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- **Automatic SSL/TLS** certificates via Let's Encrypt
|
||||
- **Reverse Proxy** with Traefik v3.1
|
||||
- **Authentication** with Authelia (2FA, password reset)
|
||||
- **Web Analytics** with Umami and PostgreSQL
|
||||
- **Container Management** via Portainer
|
||||
- **Uptime Monitoring** with Uptime Kuma
|
||||
- **Security Headers** and Basic Authentication
|
||||
- **Server Monitoring** with Beszel
|
||||
- **Git Repository Hosting** with Gitea
|
||||
- **Encrypted Backups** with Duplicati
|
||||
- **Security Headers** and Flexible Routing
|
||||
- **Flexible Domain Routing** (subdomains, paths, custom rules)
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
```
|
||||
Internet → Traefik (Port 80/443) → Internal Services (traefik_proxy network)
|
||||
Internet → Traefik (Port 80/443) → Authelia (Auth) → Internal Services (traefik_proxy network)
|
||||
```
|
||||
|
||||
### Current Services
|
||||
- **Traefik Dashboard**: `traefik.gate.${DOMAIN}` - Reverse proxy management
|
||||
- **Portainer**: `portainer.gate.${DOMAIN}` - Docker container management
|
||||
- **Uptime Kuma**: `uptime.gate.${DOMAIN}` - Service monitoring
|
||||
- **Umami Analytics**: `umami.gate.${DOMAIN}` - Web analytics dashboard
|
||||
- **Traefik Dashboard**: `traefik.${DOMAIN_PREFIX}.${DOMAIN}` - Reverse proxy management
|
||||
- **Authelia**: `auth.${DOMAIN_PREFIX}.${DOMAIN}` - Authentication portal
|
||||
- **Portainer**: `portainer.${DOMAIN_PREFIX}.${DOMAIN}` - Docker container management
|
||||
- **Uptime Kuma**: `uptime.${DOMAIN_PREFIX}.${DOMAIN}` - Service monitoring
|
||||
- **Umami Analytics**: `umami.${DOMAIN_PREFIX}.${DOMAIN}` - Web analytics dashboard
|
||||
- **pgAdmin**: `pgadmin.${DOMAIN_PREFIX}.${DOMAIN}` - PostgreSQL administration
|
||||
- **Beszel**: `beszel.${DOMAIN_PREFIX}.${DOMAIN}` - Server monitoring
|
||||
- **Gitea**: `git.${DOMAIN_PREFIX}.${DOMAIN}` - Self-hosted Git service (SSH on port 222)
|
||||
- **Duplicati**: `backup.${DOMAIN_PREFIX}.${DOMAIN}` - Encrypted cloud backup
|
||||
|
||||
## 🛠️ Quick Start
|
||||
|
||||
@@ -49,19 +58,37 @@ Update `.env` with your settings:
|
||||
```bash
|
||||
# Domain and timezone
|
||||
DOMAIN=your-domain.com
|
||||
TZ=Your/Timezone
|
||||
DOMAIN_PREFIX=test
|
||||
TZ=Africa/Cairo
|
||||
|
||||
# Let's Encrypt email
|
||||
ACME_EMAIL=admin@your-domain.com
|
||||
|
||||
# Basic auth (generate with: htpasswd -nB admin)
|
||||
BASIC_AUTH_USERS=admin:$$2y$$05$$your_hashed_password
|
||||
|
||||
# Database credentials
|
||||
UMAMI_DB_USER=umami
|
||||
UMAMI_DB_PASS=your_secure_password
|
||||
UMAMI_DB_NAME=umami
|
||||
UMAMI_APP_SECRET=your_64_character_secret
|
||||
|
||||
# Authelia secrets (generate with openssl rand -hex 32)
|
||||
AUTHELIA_DB_NAME=authelia
|
||||
AUTHELIA_DB_USER=authelia
|
||||
AUTHELIA_DB_PASSWORD=your_secure_password
|
||||
AUTHELIA_JWT_SECRET=your_64_char_secret
|
||||
AUTHELIA_SESSION_SECRET=your_64_char_secret
|
||||
AUTHELIA_STORAGE_ENCRYPTION_KEY=your_64_char_secret
|
||||
AUTHELIA_NOTIFIER_SMTP_PASSWORD=your_smtp_password
|
||||
|
||||
# Gitea secrets
|
||||
GITEA_DB_USER=gitea
|
||||
GITEA_DB_PASSWORD=your_secure_password
|
||||
GITEA_DB_NAME=gitea
|
||||
GITEA_SECRET_KEY=your_64_char_secret
|
||||
GITEA_INTERNAL_TOKEN=your_internal_token
|
||||
|
||||
# Duplicati secrets
|
||||
DUPLICATI_ENCRYPTION_KEY=your_encryption_key
|
||||
DUPLICATI_PASSWORD=your_backup_password
|
||||
```
|
||||
|
||||
|
||||
@@ -71,10 +98,15 @@ docker compose up -d
|
||||
```
|
||||
|
||||
### 4. Access Services
|
||||
- **Traefik Dashboard**: `https://traefik.gate.your-domain.com`
|
||||
- **Portainer**: `https://portainer.gate.your-domain.com`
|
||||
- **Uptime Kuma**: `https://uptime.gate.your-domain.com`
|
||||
- **Umami Analytics**: `https://umami.gate.your-domain.com`
|
||||
- **Traefik Dashboard**: `https://traefik.${DOMAIN_PREFIX}.${DOMAIN}`
|
||||
- **Authelia**: `https://auth.${DOMAIN_PREFIX}.${DOMAIN}`
|
||||
- **Portainer**: `https://portainer.${DOMAIN_PREFIX}.${DOMAIN}`
|
||||
- **Uptime Kuma**: `https://uptime.${DOMAIN_PREFIX}.${DOMAIN}`
|
||||
- **Umami Analytics**: `https://umami.${DOMAIN_PREFIX}.${DOMAIN}`
|
||||
- **pgAdmin**: `https://pgadmin.${DOMAIN_PREFIX}.${DOMAIN}`
|
||||
- **Beszel**: `https://beszel.${DOMAIN_PREFIX}.${DOMAIN}`
|
||||
- **Gitea**: `https://git.${DOMAIN_PREFIX}.${DOMAIN}` (SSH: port 222)
|
||||
- **Duplicati**: `https://backup.${DOMAIN_PREFIX}.${DOMAIN}`
|
||||
|
||||
## 📋 Adding New Services
|
||||
|
||||
@@ -231,16 +263,15 @@ services:
|
||||
|
||||
### Available Middlewares
|
||||
- **`security-headers`**: HSTS, XSS protection, content type sniffing prevention
|
||||
- **`basic-auth`**: HTTP Basic Authentication for admin interfaces
|
||||
- **`umami-analytics`**: Automatic web analytics tracking
|
||||
- **`authelia`**: Authentication and authorization with 2FA support
|
||||
|
||||
### Usage Examples
|
||||
```yaml
|
||||
# Public application with analytics
|
||||
- traefik.http.routers.app.middlewares=umami-analytics,security-headers
|
||||
- traefik.http.routers.app.middlewares=security-headers
|
||||
|
||||
# Admin interface with authentication
|
||||
- traefik.http.routers.admin.middlewares=basic-auth,security-headers
|
||||
- traefik.http.routers.admin.middlewares=authelia@docker,security-headers
|
||||
|
||||
# API endpoint (security headers only)
|
||||
- traefik.http.routers.api.middlewares=security-headers
|
||||
@@ -274,6 +305,7 @@ services:
|
||||
|
||||
### Built-in Monitoring
|
||||
- **Uptime Kuma**: Service availability monitoring
|
||||
- **Beszel**: Server resource monitoring (CPU, RAM, disk, network)
|
||||
- **Traefik Dashboard**: Traffic and routing metrics
|
||||
- **Umami Analytics**: Web traffic analytics (privacy-focused)
|
||||
|
||||
|
||||
61
authelia/configuration.template.yml
Normal file
61
authelia/configuration.template.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
# Authelia configuration
|
||||
# This is a minimal configuration for getting started with Authelia
|
||||
|
||||
server:
|
||||
address: 'tcp://:9091'
|
||||
endpoints:
|
||||
authz:
|
||||
forward-auth:
|
||||
implementation: 'ForwardAuth'
|
||||
|
||||
authentication_backend:
|
||||
file:
|
||||
path: '/config/users_database.yml'
|
||||
|
||||
access_control:
|
||||
default_policy: 'one_factor'
|
||||
rules:
|
||||
- domain: 'pgadmin.${DOMAIN_PREFIX}.${DOMAIN}'
|
||||
policy: 'two_factor'
|
||||
- domain: 'beszel.${DOMAIN_PREFIX}.${DOMAIN}'
|
||||
policy: 'two_factor'
|
||||
- domain: 'traefik.${DOMAIN_PREFIX}.${DOMAIN}'
|
||||
policy: 'two_factor'
|
||||
- domain: '*.${DOMAIN_PREFIX}.${DOMAIN}'
|
||||
policy: 'one_factor'
|
||||
|
||||
session:
|
||||
name: 'authelia_session'
|
||||
cookies:
|
||||
- domain: '${DOMAIN}'
|
||||
authelia_url: 'https://auth.${DOMAIN_PREFIX}.${DOMAIN}'
|
||||
default_redirection_url: 'https://portainer.${DOMAIN_PREFIX}.${DOMAIN}'
|
||||
|
||||
storage:
|
||||
postgres:
|
||||
address: 'tcp://authelia-db:5432'
|
||||
database: '${AUTHELIA_DB_NAME}'
|
||||
username: '${AUTHELIA_DB_USER}'
|
||||
password: '${AUTHELIA_DB_PASSWORD}'
|
||||
|
||||
notifier:
|
||||
disable_startup_check: true
|
||||
# Configure SMTP for production email notifications
|
||||
# For testing, you can use filesystem notifier instead:
|
||||
# filesystem:
|
||||
# filename: /config/notification.txt
|
||||
smtp:
|
||||
address: 'submissions://smtp.gmail.com:465'
|
||||
username: 'your-email@gmail.com'
|
||||
sender: 'Authelia <noreply@${DOMAIN}>'
|
||||
# For Gmail, use an App Password (not your regular password)
|
||||
# Generate at: https://myaccount.google.com/apppasswords
|
||||
|
||||
identity_validation:
|
||||
reset_password: {}
|
||||
|
||||
regulation:
|
||||
max_retries: 3
|
||||
find_time: 120
|
||||
ban_time: 300
|
||||
61
authelia/configuration.yml
Normal file
61
authelia/configuration.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
# Authelia configuration
|
||||
# This is a minimal configuration for getting started with Authelia
|
||||
|
||||
server:
|
||||
address: 'tcp://:9091'
|
||||
endpoints:
|
||||
authz:
|
||||
forward-auth:
|
||||
implementation: 'ForwardAuth'
|
||||
|
||||
authentication_backend:
|
||||
file:
|
||||
path: '/config/users_database.yml'
|
||||
|
||||
access_control:
|
||||
default_policy: 'one_factor'
|
||||
rules:
|
||||
- domain: 'pgadmin.test.3launchpad.com'
|
||||
policy: 'two_factor'
|
||||
- domain: 'beszel.test.3launchpad.com'
|
||||
policy: 'two_factor'
|
||||
- domain: 'traefik.test.3launchpad.com'
|
||||
policy: 'two_factor'
|
||||
- domain: '*.test.3launchpad.com'
|
||||
policy: 'one_factor'
|
||||
|
||||
session:
|
||||
name: 'authelia_session'
|
||||
cookies:
|
||||
- domain: '3launchpad.com'
|
||||
authelia_url: 'https://auth.test.3launchpad.com'
|
||||
default_redirection_url: 'https://portainer.test.3launchpad.com'
|
||||
|
||||
storage:
|
||||
postgres:
|
||||
address: 'tcp://authelia-db:5432'
|
||||
database: 'authelia'
|
||||
username: 'authelia'
|
||||
password: 'p/6EGIgTxPndniwUCY54G7q5jOPqXofF'
|
||||
|
||||
notifier:
|
||||
disable_startup_check: true
|
||||
# Configure SMTP for production email notifications
|
||||
# For testing, you can use filesystem notifier instead:
|
||||
# filesystem:
|
||||
# filename: /config/notification.txt
|
||||
smtp:
|
||||
address: 'submissions://smtp.gmail.com:465'
|
||||
username: 'your-email@gmail.com'
|
||||
sender: 'Authelia <noreply@3launchpad.com>'
|
||||
# For Gmail, use an App Password (not your regular password)
|
||||
# Generate at: https://myaccount.google.com/apppasswords
|
||||
|
||||
identity_validation:
|
||||
reset_password: {}
|
||||
|
||||
regulation:
|
||||
max_retries: 3
|
||||
find_time: 120
|
||||
ban_time: 300
|
||||
17
authelia/users_database.yml
Normal file
17
authelia/users_database.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
# Authelia users database
|
||||
# This is a simple file-based user database
|
||||
#
|
||||
# To generate a new password hash, run:
|
||||
# docker run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password 'YOUR_PASSWORD'
|
||||
#
|
||||
# IMPORTANT: Change the default password before production use!
|
||||
|
||||
users:
|
||||
admin:
|
||||
displayname: "Administrator"
|
||||
# Default password: "Admin@123456" - CHANGE THIS!
|
||||
password: "$argon2id$v=19$m=65536,t=3,p=4$O1Qjq7AB4/xJ7Qk1dUqp/g$PhVqFWEqyQTJeSnCeiCC3lrcWcpw37kYttw4Xh/qUsk"
|
||||
email: admin@3launchpad.com
|
||||
groups:
|
||||
- admins
|
||||
1
beszel_agent_data/fingerprint
Normal file
1
beszel_agent_data/fingerprint
Normal file
@@ -0,0 +1 @@
|
||||
c17e1b1086d55cc7d4bfacbfa6b9472ca975863413fb629f
|
||||
@@ -12,6 +12,11 @@ volumes:
|
||||
uptime_kuma_data:
|
||||
umami_data:
|
||||
pgadmin_data:
|
||||
authelia_db_data:
|
||||
beszel_data:
|
||||
gitea_data:
|
||||
gitea_db_data:
|
||||
duplicati_config:
|
||||
|
||||
########################
|
||||
# Services
|
||||
@@ -22,7 +27,7 @@ services:
|
||||
## Traefik — edge router + ACME (HTTP-01)
|
||||
## ─────────────────────────────────────────────
|
||||
traefik:
|
||||
image: traefik:v3.1
|
||||
image: traefik:latest
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -41,6 +46,9 @@ services:
|
||||
environment:
|
||||
TZ: "${TZ}"
|
||||
command:
|
||||
# Enable ping endpoint for health checks
|
||||
- --ping=true
|
||||
|
||||
# Experimental plugins
|
||||
- --experimental.plugins.traefik-umami-plugin.modulename=github.com/1cedsoda/traefik-umami-plugin
|
||||
- --experimental.plugins.traefik-umami-plugin.version=v1.0.3
|
||||
@@ -88,42 +96,98 @@ services:
|
||||
- --accesslog.filepath=/var/log/traefik/access.log
|
||||
- --accesslog.bufferingsize=100
|
||||
- --log.level=INFO
|
||||
- --metrics.prometheus=true
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- /var/run/docker.sock:/var/rundocker.sock:ro
|
||||
- traefik_letsencrypt:/letsencrypt
|
||||
- traefik_logs:/var/log/traefik
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- "traefik.enable=true"
|
||||
|
||||
# Reusable security headers
|
||||
- traefik.http.middlewares.security-headers.headers.stsSeconds=31536000
|
||||
- traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true
|
||||
- traefik.http.middlewares.security-headers.headers.stsPreload=true
|
||||
- traefik.http.middlewares.security-headers.headers.browserXssFilter=true
|
||||
- traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true
|
||||
- traefik.http.middlewares.security-headers.headers.referrerPolicy=no-referrer-when-downgrade
|
||||
- traefik.http.middlewares.security-headers.headers.frameDeny=true
|
||||
# Reusable security headers middleware
|
||||
- "traefik.http.middlewares.security-headers.headers.stsSeconds=31536000"
|
||||
- "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true"
|
||||
- "traefik.http.middlewares.security-headers.headers.stsPreload=true"
|
||||
- "traefik.http.middlewares.security-headers.headers.browserXssFilter=true"
|
||||
- "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true"
|
||||
- "traefik.http.middlewares.security-headers.headers.referrerPolicy=no-referrer-when-downgrade"
|
||||
|
||||
# Basic Auth middleware
|
||||
- traefik.http.middlewares.basic-auth.basicauth.users=${BASIC_AUTH_USERS}
|
||||
|
||||
# Umami Analytics middleware (commented out until real website ID is available)
|
||||
# - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.umamiHost=umami:3000
|
||||
# - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.websiteId=${UMAMI_WEBSITE_ID}
|
||||
# - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.forwardPath=umami
|
||||
# - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.scriptInjection=true
|
||||
# - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.scriptInjectionMode=tag
|
||||
# - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.autoTrack=true
|
||||
# - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.doNotTrack=false
|
||||
# - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.cache=false
|
||||
# - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.serverSideTracking=false
|
||||
# Authelia middleware
|
||||
- "traefik.http.middlewares.authelia.forwardAuth.address=http://authelia:9091/api/authz/forward-auth"
|
||||
- "traefik.http.middlewares.authelia.forwardAuth.trustForwardHeader=true"
|
||||
- "traefik.http.middlewares.authelia.forwardAuth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Email,Remote-Name"
|
||||
|
||||
# Traefik dashboard (protected)
|
||||
- traefik.http.routers.traefik.rule=Host(`traefik.gate.${DOMAIN}`)
|
||||
- traefik.http.routers.traefik.entrypoints=websecure
|
||||
- traefik.http.routers.traefik.tls.certresolver=le
|
||||
- traefik.http.routers.traefik.service=api@internal
|
||||
- traefik.http.routers.traefik.middlewares=basic-auth,security-headers
|
||||
- "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN_PREFIX}.${DOMAIN}`)"
|
||||
- "traefik.http.routers.traefik.entrypoints=websecure"
|
||||
- "traefik.http.routers.traefik.tls.certresolver=le"
|
||||
- "traefik.http.routers.traefik.service=api@internal"
|
||||
- "traefik.http.routers.traefik.middlewares=authelia@docker,security-headers@docker"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
## ─────────────────────────────────────────────
|
||||
## Authelia — authentication and authorization
|
||||
## ─────────────────────────────────────────────
|
||||
authelia:
|
||||
image: authelia/authelia:latest
|
||||
container_name: authelia
|
||||
restart: unless-stopped
|
||||
networks: [traefik_proxy]
|
||||
volumes:
|
||||
- ./authelia:/config
|
||||
entrypoint: /bin/sh
|
||||
command:
|
||||
- -c
|
||||
- |
|
||||
eval "echo \"$(cat /config/configuration.template.yml)\"" > /config/configuration.yml
|
||||
exec /app/entrypoint.sh
|
||||
environment:
|
||||
TZ: "${TZ}"
|
||||
DOMAIN: "${DOMAIN}"
|
||||
DOMAIN_PREFIX: "${DOMAIN_PREFIX}"
|
||||
AUTHELIA_DB_NAME: "${AUTHELIA_DB_NAME}"
|
||||
AUTHELIA_DB_USER: "${AUTHELIA_DB_USER}"
|
||||
AUTHELIA_DB_PASSWORD: "${AUTHELIA_DB_PASSWORD}"
|
||||
AUTHELIA_SESSION_SECRET: '${AUTHELIA_SESSION_SECRET}'
|
||||
AUTHELIA_STORAGE_ENCRYPTION_KEY: '${AUTHELIA_STORAGE_ENCRYPTION_KEY}'
|
||||
AUTHELIA_STORAGE_POSTGRES_PASSWORD: '${AUTHELIA_DB_PASSWORD}'
|
||||
AUTHELIA_NOTIFIER_SMTP_PASSWORD: '${AUTHELIA_NOTIFIER_SMTP_PASSWORD}'
|
||||
AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET: '${AUTHELIA_JWT_SECRET}'
|
||||
depends_on:
|
||||
- authelia-db
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.authelia.rule=Host(`auth.${DOMAIN_PREFIX}.${DOMAIN}`)"
|
||||
- "traefik.http.routers.authelia.entrypoints=websecure"
|
||||
- "traefik.http.routers.authelia.tls.certresolver=le"
|
||||
- "traefik.http.routers.authelia.middlewares=security-headers@docker"
|
||||
- "traefik.http.services.authelia.loadbalancer.server.port=9091"
|
||||
|
||||
## ─────────────────────────────────────────────
|
||||
## Authelia Database — PostgreSQL
|
||||
## ─────────────────────────────────────────────
|
||||
authelia-db:
|
||||
image: postgres:15-alpine
|
||||
container_name: authelia-db
|
||||
restart: unless-stopped
|
||||
networks: [traefik_proxy]
|
||||
environment:
|
||||
POSTGRES_DB: ${AUTHELIA_DB_NAME}
|
||||
POSTGRES_USER: ${AUTHELIA_DB_USER}
|
||||
POSTGRES_PASSWORD: ${AUTHELIA_DB_PASSWORD}
|
||||
volumes:
|
||||
- authelia_db_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U authelia -d authelia"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
## ─────────────────────────────────────────────
|
||||
## Portainer — Docker control plane
|
||||
@@ -133,34 +197,35 @@ services:
|
||||
container_name: portainer
|
||||
restart: unless-stopped
|
||||
networks: [traefik_proxy]
|
||||
command: --host unix:///var/run/docker.sock
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- portainer_data:/data
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.portainer.rule=Host(`portainer.gate.${DOMAIN}`)
|
||||
- traefik.http.routers.portainer.entrypoints=websecure
|
||||
- traefik.http.routers.portainer.tls.certresolver=le
|
||||
- traefik.http.routers.portainer.middlewares=security-headers
|
||||
- traefik.http.services.portainer.loadbalancer.server.port=9000
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.portainer.rule=Host(`portainer.${DOMAIN_PREFIX}.${DOMAIN}`)"
|
||||
- "traefik.http.routers.portainer.entrypoints=websecure"
|
||||
- "traefik.http.routers.portainer.tls.certresolver=le"
|
||||
- "traefik.http.routers.portainer.middlewares=security-headers@docker"
|
||||
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
|
||||
|
||||
## ─────────────────────────────────────────────
|
||||
## Uptime Kuma — status page / checks
|
||||
## ─────────────────────────────────────────────
|
||||
uptime-kuma:
|
||||
image: louislam/uptime-kuma:Latest
|
||||
image: louislam/uptime-kuma:latest
|
||||
container_name: uptime-kuma
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- uptime_kuma_data:/app/data
|
||||
networks: [traefik_proxy]
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.kuma.rule=Host(`uptime.gate.${DOMAIN}`)
|
||||
- traefik.http.routers.kuma.entrypoints=websecure
|
||||
- traefik.http.routers.kuma.tls.certresolver=le
|
||||
- traefik.http.routers.kuma.middlewares=basic-auth,security-headers
|
||||
- traefik.http.services.kuma.loadbalancer.server.port=3001
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.kuma.rule=Host(`uptime.${DOMAIN_PREFIX}.${DOMAIN}`)"
|
||||
- "traefik.http.routers.kuma.entrypoints=websecure"
|
||||
- "traefik.http.routers.kuma.tls.certresolver=le"
|
||||
- "traefik.http.routers.kuma.middlewares=security-headers@docker"
|
||||
- "traefik.http.services.kuma.loadbalancer.server.port=3001"
|
||||
|
||||
## ─────────────────────────────────────────────
|
||||
## Umami — web analytics
|
||||
@@ -177,12 +242,18 @@ services:
|
||||
depends_on:
|
||||
- umami-db
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.umami.rule=Host(`umami.gate.${DOMAIN}`)
|
||||
- traefik.http.routers.umami.entrypoints=websecure
|
||||
- traefik.http.routers.umami.tls.certresolver=le
|
||||
- traefik.http.routers.umami.middlewares=security-headers
|
||||
- traefik.http.services.umami.loadbalancer.server.port=3000
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.umami.rule=Host(`umami.${DOMAIN_PREFIX}.${DOMAIN}`)"
|
||||
- "traefik.http.routers.umami.entrypoints=websecure"
|
||||
- "traefik.http.routers.umami.tls.certresolver=le"
|
||||
- "traefik.http.routers.umami.middlewares=security-headers@docker"
|
||||
- "traefik.http.services.umami.loadbalancer.server.port=3000"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
## ─────────────────────────────────────────────
|
||||
## Umami Database — PostgreSQL
|
||||
@@ -198,6 +269,12 @@ services:
|
||||
POSTGRES_PASSWORD: ${UMAMI_DB_PASS}
|
||||
volumes:
|
||||
- umami_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${UMAMI_DB_USER} -d ${UMAMI_DB_NAME}"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# pgAdmin — PostgreSQL administration
|
||||
@@ -212,18 +289,141 @@ services:
|
||||
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
|
||||
PGADMIN_CONFIG_SERVER_MODE: 'True'
|
||||
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
|
||||
# Fix CSRF issues behind reverse proxy
|
||||
PGADMIN_CONFIG_WTF_CSRF_CHECK_DEFAULT: 'False'
|
||||
PGADMIN_CONFIG_WTF_CSRF_TIME_LIMIT: 'None'
|
||||
PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION: 'False'
|
||||
# Trust proxy headers
|
||||
PGADMIN_CONFIG_PROXY_X_HOST_COUNT: '1'
|
||||
PGADMIN_CONFIG_PROXY_X_PREFIX_COUNT: '1'
|
||||
volumes:
|
||||
- pgadmin_data:/var/lib/pgadmin
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.pgadmin.rule=Host(`pgadmin.gate.${DOMAIN}`)
|
||||
- traefik.http.routers.pgadmin.entrypoints=websecure
|
||||
- traefik.http.routers.pgadmin.tls.certresolver=le
|
||||
- traefik.http.services.pgadmin.loadbalancer.server.port=80
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.pgadmin.rule=Host(`pgadmin.${DOMAIN_PREFIX}.${DOMAIN}`)"
|
||||
- "traefik.http.routers.pgadmin.entrypoints=websecure"
|
||||
- "traefik.http.routers.pgadmin.tls.certresolver=le"
|
||||
- "traefik.http.routers.pgadmin.middlewares=security-headers@docker"
|
||||
- "traefik.http.services.pgadmin.loadbalancer.server.port=80"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
## ─────────────────────────────────────────────
|
||||
## Beszel Hub — lightweight server monitoring
|
||||
## ─────────────────────────────────────────────
|
||||
beszel:
|
||||
image: henrygd/beszel:latest
|
||||
container_name: beszel
|
||||
restart: unless-stopped
|
||||
networks: [traefik_proxy]
|
||||
volumes:
|
||||
- beszel_data:/beszel_data
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.beszel.rule=Host(`beszel.${DOMAIN_PREFIX}.${DOMAIN}`)"
|
||||
- "traefik.http.routers.beszel.entrypoints=websecure"
|
||||
- "traefik.http.routers.beszel.tls.certresolver=le"
|
||||
- "traefik.http.routers.beszel.middlewares=security-headers@docker"
|
||||
- "traefik.http.services.beszel.loadbalancer.server.port=8090"
|
||||
|
||||
## ─────────────────────────────────────────────
|
||||
## Gitea — self-hosted Git service
|
||||
## ─────────────────────────────────────────────
|
||||
gitea:
|
||||
image: docker.gitea.com/gitea:latest
|
||||
container_name: gitea
|
||||
restart: unless-stopped
|
||||
networks: [traefik_proxy]
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
- GITEA__database__DB_TYPE=postgres
|
||||
- GITEA__database__HOST=gitea-db:5432
|
||||
- GITEA__database__NAME=${GITEA_DB_NAME}
|
||||
- GITEA__database__USER=${GITEA_DB_USER}
|
||||
- GITEA__database__PASSWD=${GITEA_DB_PASSWORD}
|
||||
- GITEA__database__SSL_MODE=disable
|
||||
- GITEA__server__DOMAIN=git.${DOMAIN_PREFIX}.${DOMAIN}
|
||||
- GITEA__server__SSH_DOMAIN=git.${DOMAIN_PREFIX}.${DOMAIN}
|
||||
- GITEA__server__ROOT_URL=https://git.${DOMAIN_PREFIX}.${DOMAIN}/
|
||||
- GITEA__server__SSH_PORT=222
|
||||
- GITEA__server__SSH_LISTEN_PORT=22
|
||||
- GITEA__security__SECRET_KEY=${GITEA_SECRET_KEY}
|
||||
- GITEA__security__INTERNAL_TOKEN=${GITEA_INTERNAL_TOKEN}
|
||||
- GITEA__i18n__LANGS=en-US
|
||||
- GITEA__i18n__NAMES=English
|
||||
volumes:
|
||||
- gitea_data:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
- "222:22"
|
||||
depends_on:
|
||||
- gitea-db
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.gitea.rule=Host(`git.${DOMAIN_PREFIX}.${DOMAIN}`)"
|
||||
- "traefik.http.routers.gitea.entrypoints=websecure"
|
||||
- "traefik.http.routers.gitea.tls.certresolver=le"
|
||||
- "traefik.http.routers.gitea.middlewares=security-headers@docker,authelia@docker"
|
||||
- "traefik.http.services.gitea.loadbalancer.server.port=3000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/healthz"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
## ─────────────────────────────────────────────
|
||||
## Gitea Database — PostgreSQL
|
||||
## ─────────────────────────────────────────────
|
||||
gitea-db:
|
||||
image: postgres:15-alpine
|
||||
container_name: gitea-db
|
||||
restart: unless-stopped
|
||||
networks: [traefik_proxy]
|
||||
environment:
|
||||
POSTGRES_DB: ${GITEA_DB_NAME}
|
||||
POSTGRES_USER: ${GITEA_DB_USER}
|
||||
POSTGRES_PASSWORD: ${GITEA_DB_PASSWORD}
|
||||
volumes:
|
||||
- gitea_db_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${GITEA_DB_USER} -d ${GITEA_DB_NAME}"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
## ─────────────────────────────────────────────
|
||||
## Duplicati — encrypted cloud backup
|
||||
## ─────────────────────────────────────────────
|
||||
duplicati:
|
||||
image: lscr.io/linuxserver/duplicati:latest
|
||||
container_name: duplicati
|
||||
restart: unless-stopped
|
||||
networks: [traefik_proxy]
|
||||
environment:
|
||||
- PUID=0
|
||||
- PGID=0
|
||||
- TZ=${TZ}
|
||||
- SETTINGS_ENCRYPTION_KEY=${DUPLICATI_ENCRYPTION_KEY}
|
||||
- CLI_ARGS=--webservice-allowed-hostnames=* --webservice-password=${DUPLICATI_PASSWORD}
|
||||
volumes:
|
||||
- duplicati_config:/config
|
||||
- /:/source:ro
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.duplicati.rule=Host(`backup.${DOMAIN_PREFIX}.${DOMAIN}`)"
|
||||
- "traefik.http.routers.duplicati.entrypoints=websecure"
|
||||
- "traefik.http.routers.duplicati.tls.certresolver=le"
|
||||
- "traefik.http.routers.duplicati.middlewares=security-headers@docker"
|
||||
- "traefik.http.services.duplicati.loadbalancer.server.port=8200"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8200"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
Reference in New Issue
Block a user