This commit is contained in:
kelinfoxy
2026-01-26 23:45:56 -05:00
17 changed files with 1137 additions and 278 deletions

View File

@@ -47,6 +47,12 @@ docker-compose/
- **Gluetun network mode**: Download clients use `network_mode: "service:gluetun"` for VPN routing
- **Port mapping**: Only core services expose ports (80/443 for Traefik); others route via Traefik labels
### TLS and Multi-Server Architecture
- **Shared CA**: Core server generates CA for signing certificates across all servers
- **Docker TLS**: Remote servers use TCP 2376 with mutual TLS for Sablier access
- **Certificate Management**: Automated generation and distribution of client/server certificates
- **Sablier Integration**: Core Sablier connects to remote Docker daemons via TLS for lazy loading
## Critical Operational Principles
### 1. Security-First SSO Strategy
@@ -284,6 +290,15 @@ Secrets auto-generated by `ez-homelab.sh`:
- **Renewal**: Traefik handles automatically (90-day Let's Encrypt certs)
- **Usage**: Services use `tls.certresolver=letsencrypt` label (no per-service cert requests)
### Remote Server TLS Setup
For multi-server deployments with Sablier lazy loading:
- **Core server**: Generates shared CA and client certificates
- **Remote servers**: Use `ez-homelab.sh` option 3, specify core server IP for CA import
- **Certificate chain**: Core has CA + client certs; remotes have CA + server certs
- **Sablier connection**: Uses TCP 2376 with mutual TLS to remote Docker daemons
- **Security**: All Docker API access encrypted and authenticated
- **Failure handling**: Setup fails if CA cannot be copied (prevents inconsistent TLS state)
### Homepage Dashboard AI Configuration
Homepage (`/opt/stacks/dashboards/`) uses dynamic variable replacement:
- Services configured in `homepage/config/services.yaml`
@@ -357,6 +372,17 @@ docker logs gluetun | grep -i wireguard # Verify connection
```
Verify: `SURFSHARK_PRIVATE_KEY` set in `.env`, service using `network_mode: "service:gluetun"`, ports mapped in Gluetun
### Sablier TLS Connection Issues
```bash
# Test Docker TLS connection from core to remote
cd /opt/stacks/core/shared-ca
docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem --host=tcp://REMOTE_IP:2376 ps
# Check Sablier logs
docker compose -f /opt/stacks/core/docker-compose.yml logs sablier-service
```
Verify: Remote Docker daemon configured with shared CA, certificates properly signed, firewall allows 2376/tcp
### Wildcard Certificate Issues
```bash
docker logs traefik | grep -i certificate

3
.gitignore vendored
View File

@@ -71,6 +71,9 @@ yarn-error.log*
*.pfx
acme.json
# Docker TLS certificates directory
docker-tls/
# Nextcloud application files (should be mounted via volumes)
docker-compose/productivity/nextcloud/html/

View File

@@ -99,10 +99,10 @@ services:
# Sablier - Lazy loading service for Docker containers
# Controls startup/shutdown of lazy-loaded services, must always run
# REQUIREMENTS FOR DOCKER API ACCESS:
# 1. Docker daemon must be configured to listen on TCP port 2375
# 1. Docker daemon must be configured to listen on TCP port 2376 with TLS
# 2. DOCKER_HOST environment variable must point to accessible Docker API endpoint
# 3. Firewall must allow TCP connections to Docker API port (default 2375)
# 4. For production, consider using TLS for Docker API communication
# 3. Firewall must allow TCP connections to Docker API port (2376)
# 4. TLS certificates must be mounted and environment variables set
# 5. Ensure dockerproxy service is running and accessible
sablier-service:
image: sablierapp/sablier:latest
@@ -115,7 +115,11 @@ services:
- SABLIER_DOCKER_API_VERSION=1.51
- SABLIER_DOCKER_NETWORK=traefik-network
- SABLIER_LOG_LEVEL=debug
- DOCKER_HOST=tcp://192.168.4.11:2375
- DOCKER_HOST=tcp://${SERVER_IP}:2376
- DOCKER_TLS_VERIFY=1
- DOCKER_CERT_PATH=/certs
volumes:
- ./sablier-certs:/certs:ro
ports:
- 10000:10000
labels:
@@ -130,6 +134,6 @@ networks:
x-dockge:
urls:
- https://auth.${DOMAIN}
- https://{$SERVER_IP}:9091
- https://${SERVER_IP}:9091
- https://traefik.${DOMAIN}
- https://{$SERVER_IP}:8080
- https://${SERVER_IP}:8080

View File

@@ -114,9 +114,9 @@ x-dockge:
urls:
# Proxied URLs (through Traefik)
- https://homepage.${DOMAIN}
- https://{$SERVER_IP}:3003
- https://${SERVER_IP}:3003
- https://homarr.${DOMAIN}
- https://{$SERVER_IP}:7575
- https://${SERVER_IP}:7575
networks:
homelab-network:

View File

@@ -240,3 +240,19 @@ networks:
external: true
traefik-network:
external: true
x-dockge:
urls:
# Proxied URLs (through Traefik)
- https://ha.${DOMAIN}
- http://${SERVER_IP}:8123
- https://esphome.${DOMAIN}
- http://${SERVER_IP}:6052
- https://tasmoadmin.${DOMAIN}
- http://${SERVER_IP}:8084
- https://motioneye.${DOMAIN}
- http://${SERVER_IP}:8765
- https://nodered.${DOMAIN}
- http://${SERVER_IP}:1880
- mqtt://${SERVER_IP}:1883
- https://zigbee2mqtt.${DOMAIN}

View File

@@ -28,7 +28,7 @@ services:
- PGID=${PGID}
- TZ=${TZ}
healthcheck:
test: ["CMD", "curl", "-f", "http://${SERVER_IP}:8989/"]
test: ["CMD", "curl", "-f", "http://localhost:8989/"]
interval: 30s
timeout: 10s
retries: 3
@@ -73,7 +73,7 @@ services:
- PGID=${PGID}
- TZ=${TZ}
healthcheck:
test: ["CMD", "curl", "-f", "http://${SERVER_IP}:7878/"]
test: ["CMD", "curl", "-f", "http://localhost:7878/"]
interval: 30s
timeout: 10s
retries: 3
@@ -116,7 +116,7 @@ services:
- PGID=${PGID}
- TZ=${TZ}
healthcheck:
test: ["CMD", "curl", "-f", "http://${SERVER_IP}:9696/"]
test: ["CMD", "curl", "-f", "http://localhost:9696/"]
interval: 30s
timeout: 10s
retries: 3
@@ -315,7 +315,7 @@ services:
- LOG_LEVEL=info
- TZ=${TZ}
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://${SERVER_IP}:5055/"]
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:5055/"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -128,7 +128,9 @@ x-dockge:
urls:
# Proxied URLs (through Traefik)
- https://jellyfin.${DOMAIN}
- http://${SERVER_IP}:8096
- https://calibre.${DOMAIN}
- http://${SERVER_IP}:8083
networks:
homelab-network:

View File

@@ -8,18 +8,18 @@
# - See individual service comments for specific reasoning
# Service Access URLs:
# - Prometheus: http://server-ip:9090 (or configure Traefik)
# - Grafana: http://server-ip:3000 (or configure Traefik)
# - Prometheus: http://${SERVER_IP}:9090 (or configure Traefik)
# - Grafana: http://${SERVER_IP}:3000 (or configure Traefik)
# - Uptime Kuma: https://status.${DOMAIN}
# - Node Exporter: http://server-ip:9100/metrics
# - cAdvisor: http://server-ip:8082
# - Loki: http://server-ip:3100
# - Node Exporter: http://${SERVER_IP}:9100/metrics
# - cAdvisor: http://${SERVER_IP}:8082
# - Loki: http://${SERVER_IP}:3100
# NOTE: Prometheus, Grafana, Loki use ports because they need to be accessible to other services
# Add Traefik labels if you want https://prometheus.${DOMAIN} access
services:
# Prometheus - Metrics collection and storage
# Access at: http://server-ip:9090
# Access at: http://${SERVER_IP}:9090
prometheus:
image: prom/prometheus:v2.48.1
deploy:
@@ -66,7 +66,7 @@ services:
- "traefik.http.services.prometheus.loadbalancer.server.port=9090"
# Grafana - Metrics visualization
# Access at: http://server-ip:3000
# Access at: http://${SERVER_IP}:3000
# Default credentials: admin / admin (change on first login)
grafana:
image: grafana/grafana:10.2.3
@@ -115,7 +115,7 @@ services:
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
# Node Exporter - Host metrics exporter
# Metrics at: http://server-ip:9100/metrics
# Metrics at: http://${SERVER_IP}:9100/metrics
node-exporter:
image: prom/node-exporter:v1.7.0
container_name: node-exporter
@@ -138,7 +138,7 @@ services:
- "homelab.description=Hardware and OS metrics exporter"
# cAdvisor - Container metrics exporter
# Access at: http://server-ip:8082
# Access at: http://${SERVER_IP}:8082
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.47.2
container_name: cadvisor
@@ -215,7 +215,7 @@ services:
- "traefik.http.services.uptime-kuma.loadbalancer.server.port=3001"
# Loki - Log aggregation
# Access at: http://server-ip:3100
# Access at: http://${SERVER_IP}:3100
loki:
image: grafana/loki:2.9.3
deploy:
@@ -289,3 +289,13 @@ networks:
external: true
traefik-network:
external: true
x-dockge:
urls:
# Proxied URLs (through Traefik)
- http://${SERVER_IP}:9090
- http://${SERVER_IP}:3000
- https://uptime-kuma.${DOMAIN}
- http://${SERVER_IP}:9100/metrics
- http://${SERVER_IP}:8082
- http://${SERVER_IP}:3100

View File

@@ -327,12 +327,12 @@ x-dockge:
urls:
# Proxied URLs (through Traefik)
- https://nextcloud.${DOMAIN}
- https://{$SERVER_IP}:8089
- https://${SERVER_IP}:8089
- https://mealie.${DOMAIN}
- https://{$SERVER_IP}:9000
- https://${SERVER_IP}:9000
- https://wordpress.${DOMAIN}
- https://{$SERVER_IP}:8088
- https://${SERVER_IP}:8088
- https://gitea.${DOMAIN}
- https://{$SERVER_IP}:3010
- https://${SERVER_IP}:3010
- https://jupyter.${DOMAIN}
- https://{$SERVER_IP}:8890
- https://${SERVER_IP}:8890

View File

@@ -237,10 +237,10 @@ networks:
x-dockge:
urls:
- https://backrest.${DOMAIN}
- https://{$SERVER_IP}:9898
- https://${SERVER_IP}:9898
- https://duplicati.${DOMAIN}
- https://{$SERVER_IP}:8200
- https://${SERVER_IP}:8200
- https://forms.${DOMAIN}
- https://{$SERVER_IP}:3002
- https://${SERVER_IP}:3002
- https://vault.${DOMAIN}
- https://{$SERVER_IP}:8091
- https://${SERVER_IP}:8091

View File

@@ -54,7 +54,7 @@ services:
- "traefik.http.services.qbittorrent.loadbalancer.server.port=8081"
# Sablier configuration
- "sablier.enable=true"
- "sablier.group=qbittorrent"
- "sablier.group=${SERVER_HOSTNAME}-qbittorrent"
- "sablier.sessionDuration=1h"
# qBittorrent - Torrent client

View File

@@ -181,8 +181,8 @@ x-dockge:
urls:
# Proxied URLs (through Traefik)
- https://bookstack.${DOMAIN}
- https://{$SERVER_IP}:6875
- https://${SERVER_IP}:6875
- https://dokuwiki.${DOMAIN}
- https://{$SERVER_IP}:8087
- https://${SERVER_IP}:8087
- https://mediawiki.${DOMAIN}
- https://{$SERVER_IP}:8086
- https://${SERVER_IP}:8086

View File

@@ -1,6 +1,66 @@
# On Demand Remote Services with Authelia, Sablier & Traefik
## 4 Step Process
## Overview
This guide explains how to set up lazy-loading services on remote servers (like Raspberry Pi) that start automatically when accessed via Traefik. The core server runs Sablier, which connects to remote Docker daemons via TLS to manage container lifecycle.
## Prerequisites
- Core server with Traefik, Authelia, and Sablier deployed
- Remote server with Docker installed
- Shared TLS CA configured between core and remote servers
## Automated Setup
For new remote servers, use the automated script:
1. On the remote server, run `ez-homelab.sh` and select option 3 (Infrastructure Only)
2. When prompted, enter the core server IP for shared TLS CA
3. The script will automatically:
- Copy shared CA from core server via SSH
- Configure Docker TLS with shared certificates
- Generate server certificates signed by shared CA
- Set up Docker daemon for TLS on port 2376
**Important**: The script will fail if it cannot copy the shared CA from the core server. Ensure SSH access is configured between servers before running option 3.
## Manual Setup (if automated fails)
If the automated setup fails, manually configure TLS:
### On Core Server:
```bash
# Generate server certificates for remote server
cd /opt/stacks/core/shared-ca
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=<REMOTE_IP>" -new -key server-key.pem -out server.csr
echo "subjectAltName = DNS:<REMOTE_IP>,IP:<REMOTE_IP>,IP:127.0.0.1" > extfile.cnf
openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile extfile.cnf
```
### On Remote Server:
```bash
# Copy certificates
scp user@core-server:/opt/stacks/core/shared-ca/ca.pem /opt/stacks/core/shared-ca/
scp user@core-server:/opt/stacks/core/shared-ca/server-cert.pem /opt/stacks/core/shared-ca/
scp user@core-server:/opt/stacks/core/shared-ca/server-key.pem /opt/stacks/core/shared-ca/
# Update Docker daemon
sudo tee /etc/docker/daemon.json > /dev/null <<EOF
{
"tls": true,
"tlsverify": true,
"tlscacert": "/opt/stacks/core/shared-ca/ca.pem",
"tlscert": "/opt/stacks/core/shared-ca/server-cert.pem",
"tlskey": "/opt/stacks/core/shared-ca/server-key.pem"
}
EOF
sudo systemctl restart docker
```
## 4 Step Process for Adding Services
1. Add route & service in Traefik external hosts file
2. Add middleware in Sablier config file (sablier.yml)
3. Add labels to compose files on Remote Host
@@ -115,3 +175,302 @@ docker stop <service>
Access your service by the proxy url.
---
# Deployment Plan for Multi-Server Setup
This section provides a complete deployment plan for scenarios where the core infrastructure (Traefik, Authelia, Sablier) runs on one server, and application services run on remote servers. This setup enables centralized control and routing while maintaining service isolation.
## Architecture Overview
- **Core Server**: Hosts Traefik (reverse proxy), Authelia (SSO), Sablier (lazy loading controller)
- **Remote/Media Servers**: Host application containers controlled by Sablier
- **Communication**: TLS-secured Docker API calls between servers
## Prerequisites
- Both servers must be on the same network and able to communicate
- SSH access configured between servers (key-based or password authentication supported)
- Domain configured with DuckDNS or similar
- The EZ-Homelab script handles most Docker TLS and certificate setup automatically
- Basic understanding of Docker concepts (optional - script guides you through setup)
## Step 1: Configure Docker TLS on All Servers
### On Each Server (Core and Remote)
1. **Install Docker** (if not already installed):
```bash
curl -fsSL https://get.docker.com | sh
usermod -aG docker $USER
systemctl enable docker
systemctl start docker
# Log out and back in for group changes
```
2. **Generate TLS Certificates**:
```bash
mkdir -p ~/EZ-Homelab/docker-tls
cd ~/EZ-Homelab/docker-tls
# Generate CA
openssl genrsa -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem -subj "/C=US/ST=State/L=City/O=Organization/CN=Docker-CA"
# Generate server key and cert (replace SERVER_IP with actual IP)
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=<SERVER_IP>" -new -key server-key.pem -out server.csr
echo "subjectAltName = DNS:<SERVER_IP>,IP:<SERVER_IP>,IP:127.0.0.1" > extfile.cnf
openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile extfile.cnf
# Generate client key and cert
openssl genrsa -out client-key.pem 4096
openssl req -subj "/CN=client" -new -key client-key.pem -out client.csr
openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem
```
3. **Configure Docker Daemon**:
Create `/etc/docker/daemon.json`:
```json
{
"tls": true,
"tlsverify": true,
"tlscacert": "/home/$USER/EZ-Homelab/docker-tls/ca.pem",
"tlscert": "/home/$USER/EZ-Homelab/docker-tls/server-cert.pem",
"tlskey": "/home/$USER/EZ-Homelab/docker-tls/server-key.pem"
}
```
4. **Update Systemd Service**:
```bash
sudo sed -i 's|-H fd://|-H fd:// -H tcp://0.0.0.0:2376|' /lib/systemd/system/docker.service
sudo systemctl daemon-reload
sudo systemctl restart docker
```
5. **Configure Firewall**:
```bash
sudo ufw allow 2376/tcp
sudo ufw --force enable
```
## Certificate and Secret Sharing
The EZ-Homelab script automatically handles certificate and secret sharing for infrastructure-only deployments:
### Automatic Process (Recommended)
1. **On Remote Server**: Run `./scripts/ez-homelab.sh` and select option 3
2. **Script Actions**:
- Prompts for core server IP
- Tests SSH connectivity
- Copies Docker TLS certificates for remote control
- Sets up certificates in the correct location
### Manual Process (Fallback)
If automatic sharing fails, manually share certificates:
1. **On Core Server**:
```bash
# Copy client certificates to remote server
scp /opt/stacks/core/docker-tls/ca.pem /opt/stacks/core/docker-tls/client-cert.pem /opt/stacks/core/docker-tls/client-key.pem user@remote-server:/opt/stacks/infrastructure/docker-tls/
```
2. **On Remote Server**:
```bash
# Ensure certificates are in the correct location
ls -la /opt/stacks/infrastructure/docker-tls/
# Should contain: ca.pem, client-cert.pem, client-key.pem
```
## Step 3: Deploy Core Infrastructure
### On Core Server
1. **Run the EZ-Homelab script** with core deployment:
```bash
cd ~/EZ-Homelab
./scripts/ez-homelab.sh
# Select option 1 (Default Setup) or 2 (Core Only)
```
The script will:
- Generate Authelia secrets automatically
- Configure TLS for Docker API
- Deploy Traefik, Authelia, and Sablier
- Set up certificates for secure communication
2. **Verify core deployment**:
```bash
# Check services are running
docker ps --filter "label=com.docker.compose.project=core"
# Test Authelia access
curl -k https://auth.<your-domain>
```
## Step 4: Deploy Remote Infrastructure
### On Remote/Media Server
1. **Run the EZ-Homelab script** with infrastructure-only deployment:
```bash
cd ~/EZ-Homelab
./scripts/ez-homelab.sh
# Select option 3 (Infrastructure Only)
```
The script will automatically:
- Prompt for core server IP address
- Establish SSH connection to core server
- Copy Authelia secrets and TLS certificates
- Configure Docker TLS for remote control
- Set up required networks and directories
2. **Manual certificate sharing** (if automatic fails):
If SSH connection fails, manually copy certificates:
```bash
# On core server, copy certs to remote server
scp /opt/stacks/core/docker-tls/ca.pem /opt/stacks/core/docker-tls/client-cert.pem /opt/stacks/core/docker-tls/client-key.pem user@remote-server:/opt/stacks/infrastructure/docker-tls/
# On remote server, copy Authelia secrets
scp /home/kelin/EZ-Homelab/.env user@remote-server:/home/kelin/EZ-Homelab/.env.core
```
## Step 5: Configure Sablier for Remote Control
### On Core Server
Update Sablier configuration to control remote servers:
1. **Edit core docker-compose.yml**:
```yaml
sablier-service:
environment:
- DOCKER_HOST=tcp://<REMOTE_SERVER_IP>:2376
- DOCKER_TLS_VERIFY=1
- DOCKER_CERT_PATH=/certs
volumes:
- ./docker-tls/ca.pem:/certs/ca.pem:ro
- ./docker-tls/client-cert.pem:/certs/cert.pem:ro
- ./docker-tls/client-key.pem:/certs/key.pem:ro
```
2. **Restart core stack**:
```bash
cd /opt/stacks/core
docker compose down
docker compose up -d
```
## Step 6: Deploy Application Services
### On Remote Server
1. **Deploy application stacks** with Sablier labels:
```yaml
# Example: /opt/stacks/media-management/docker-compose.yml
services:
sonarr:
labels:
- sablier.enable=true
- sablier.group=<REMOTE_HOSTNAME>-media
- sablier.start-on-demand=true
```
2. **Deploy and stop services** for lazy loading:
```bash
cd /opt/stacks/media-management
docker compose up -d
docker compose stop
```
## Step 5: Configure Traefik Routing
### On Core Server
Since Traefik cannot auto-discover labels from remote Docker hosts, use the file provider method:
1. **Create external host configuration**:
`/opt/stacks/core/traefik/dynamic/external-host-<remote_server>.yml`
```yaml
http:
routers:
sonarr-remote:
rule: "Host(`sonarr.<DOMAIN>`)"
entrypoints:
- websecure
service: sonarr-remote
tls:
certResolver: letsencrypt
middlewares:
- sablier-<remote_hostname>-arr@file
- authelia@docker
services:
sonarr-remote:
loadBalancer:
servers:
- url: "http://<REMOTE_IP>:8989"
passHostHeader: true
```
2. **Create Sablier middleware configuration**:
`/opt/stacks/core/traefik/dynamic/sablier.yml`
```yaml
http:
middlewares:
sablier-<remote_hostname>-arr:
plugin:
sablier:
sablierUrl: http://sablier-service:10000
group: <remote_hostname>-arr
sessionDuration: 2m
ignoreUserAgent: curl
dynamic:
displayName: "Media Management Services"
theme: ghost
show-details-by-default: true
```
3. **Restart Traefik**:
```bash
docker restart traefik
```
## Step 6: Verification and Testing
1. **Check Sablier connection**:
```bash
# On core server
docker logs sablier-service
# Should show groups from remote server
```
2. **Test lazy loading**:
- Access `https://sonarr.<DOMAIN>`
- Should show Sablier loading page
- Container should start on remote server
3. **Verify Traefik routes**:
```bash
curl -k https://localhost:8080/api/http/routers | jq
```
## Troubleshooting
- **TLS Connection Issues**: Check certificate validity and paths
- **Sablier Not Detecting Groups**: Verify DOCKER_HOST and certificates
- **Traefik Routing Problems**: Check external host YAML syntax
- **Network Connectivity**: Ensure ports 2376, 80, 443 are open between servers
## Security Considerations
- TLS certificates expire after 365 days - monitor and renew
- Limit Docker API access to trusted networks
- Use strong firewall rules
- Regularly update all components
This setup provides centralized management with distributed execution, optimal for resource management and security.

View File

@@ -2,6 +2,42 @@
Welcome to the AI-Homelab documentation! This is your comprehensive guide to deploying and managing a production-ready homelab infrastructure with 50+ pre-configured services.
# Mission Statement
> ### Provide a fast, convenient, and free way to deploy homelab servers with popular features
> * DuckDNS subdomains with LetsEncrypt SSL wildcard certificates
> * Security First - Secure Single Sign On with Authelia
> * Traefik - Proxy Host for an entire homelab
> * Sablier - Lazy Loading of services
> * TLS Certificates for docker proxy
>&nbsp;
# Deployment Scenarios
1. **Single server**
**Select Option 1**
* Use Dockge to start the desired stacks
* Use Homepage to explore your services
2. **Core Server + Remote Server**
**Core Server**
* A small low power device, like a Raspberry Pi 4 4GB.
* Deploy core stack. (Authelia, DuckDNS, Traefik, Sablier, Homepage)
* Functions as a gateway between the internet and your servers.
* Configure a remote host file for each Remote Server.
* Configure sablier.yml for services to lazyload.
* Generate TLS certificate, all servers use the same TLS certificate for dockerproxy
**Remote Server(s)**
* Your old office or gaming PC, SBCs, second hand PCs, etc
* Select Option 3 to install Infrastructure only
* Ensure the TLS certificate was copied correctly and docker daemon configured
* Use Dockge to start the desired stacks
* Use Homepage to explore your services
## 📚 Documentation Structure
### 🚀 Getting Started

View File

@@ -150,6 +150,63 @@ cd /opt/stacks/dashboards
docker compose up -d
```
## Step 10.5: Multi-Server TLS Setup (Optional)
If you plan to deploy services on remote servers (like Raspberry Pi) that will be managed by Sablier for lazy loading, set up shared TLS certificates.
### On Core Server (where Traefik/Authelia run):
```bash
# Create shared CA directory
sudo mkdir -p /opt/stacks/core/shared-ca
sudo chown $USER:$USER /opt/stacks/core/shared-ca
# Generate shared CA certificate
cd /opt/stacks/core/shared-ca
openssl genrsa -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem -subj "/C=US/ST=State/L=City/O=Homelab/CN=Homelab-CA"
# Set proper permissions
chmod 600 ca-key.pem
chmod 644 ca.pem
```
### On Remote Servers:
```bash
# Create TLS directory
sudo mkdir -p /opt/stacks/core/shared-ca
sudo chown $USER:$USER /opt/stacks/core/shared-ca
# Copy shared CA from core server (replace CORE_IP with your core server IP)
scp user@CORE_IP:/opt/stacks/core/shared-ca/ca.pem /opt/stacks/core/shared-ca/
scp user@CORE_IP:/opt/stacks/core/shared-ca/ca-key.pem /opt/stacks/core/shared-ca/
# Generate client certificate for Docker client connections
openssl genrsa -out client-key.pem 4096
openssl req -subj "/CN=client" -new -key client-key.pem -out client.csr
openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem
# Configure Docker TLS
sudo tee /etc/docker/daemon.json > /dev/null <<EOF
{
"tls": true,
"tlsverify": true,
"tlscacert": "/opt/stacks/core/shared-ca/ca.pem",
"tlscert": "/opt/stacks/core/shared-ca/server-cert.pem",
"tlskey": "/opt/stacks/core/shared-ca/server-key.pem"
}
EOF
# Update Docker service to listen on TLS port
sudo sed -i 's|-H fd://|-H fd:// -H tcp://0.0.0.0:2376|' /lib/systemd/system/docker.service
sudo systemctl daemon-reload
sudo systemctl restart docker
# Test TLS connection from core server
# On core server, run: docker --tlsverify --tlscacert /opt/stacks/core/shared-ca/ca.pem --tlscert /opt/stacks/core/shared-ca/client-cert.pem --tlskey /opt/stacks/core/shared-ca/client-key.pem -H tcp://REMOTE_IP:2376 ps
```
## Step 11: Verify Deployment
```bash

View File

@@ -1,8 +1,9 @@
#!/bin/bash
# EZ-Homelab Unified Setup & Deployment Script
# This script provides a guided setup and deployment experience
# Run as: ./ez-homelab.sh (will use sudo when needed)
# Two step process required for first-time setup:
# Run 'sudo ./ez-homelab.sh' to install Docker and perform system setup
# Run './ez-homelab.sh' to deploy stacks after initial setup
set -e # Exit on error
# Colors for output
@@ -29,13 +30,190 @@ log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Reusable function to replace environment variable placeholders in files
replace_env_placeholders() {
local file_path="$1"
local missing_vars=""
if [ ! -f "$file_path" ]; then
log_warning "File $file_path does not exist, skipping placeholder replacement"
return
fi
# Find all ${VAR} patterns in the file
local vars=$(grep -o '\${[^}]*}' "$file_path" | sed 's/\${//' | sed 's/}//' | sort | uniq)
for var in $vars; do
if [ -z "${!var:-}" ]; then
log_warning "Environment variable $var not found in .env file"
missing_vars="$missing_vars $var"
else
# Replace ${VAR} with the value
sed -i "s|\${$var}|${!var}|g" "$file_path"
fi
done
# Store missing vars for end-of-script summary
if [ -n "$missing_vars" ]; then
MISSING_VARS_SUMMARY="${MISSING_VARS_SUMMARY}${missing_vars}"
fi
}
# Function to generate shared CA for multi-server TLS
generate_shared_ca() {
local ca_dir="/opt/stacks/core/shared-ca"
mkdir -p "$ca_dir"
openssl genrsa -out "$ca_dir/ca-key.pem" 4096
openssl req -new -x509 -days 365 -key "$ca_dir/ca-key.pem" -sha256 -out "$ca_dir/ca.pem" -subj "/C=US/ST=State/L=City/O=Homelab/CN=Homelab-CA"
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$ca_dir"
log_success "Shared CA generated"
}
# Function to setup multi-server TLS for remote servers
setup_multi_server_tls() {
local ca_dir="/opt/stacks/core/shared-ca"
sudo mkdir -p "$ca_dir"
sudo chown "$ACTUAL_USER:$ACTUAL_USER" "$ca_dir"
if [ -n "$CORE_SERVER_IP" ]; then
log_info "Setting up multi-server TLS using shared CA from core server $CORE_SERVER_IP..."
else
# Prompt for core server IP if not set
read -p "Enter the IP address of your core server: " CORE_SERVER_IP
while [ -z "$CORE_SERVER_IP" ]; do
log_warning "Core server IP is required for shared TLS"
read -p "Enter the IP address of your core server: " CORE_SERVER_IP
done
log_info "Setting up multi-server TLS using shared CA from core server $CORE_SERVER_IP..."
fi
# Prompt for SSH username if not set
if [ -z "$SSH_USER" ]; then
DEFAULT_SSH_USER="${DEFAULT_USER:-$USER}"
read -p "SSH username for core server [$DEFAULT_SSH_USER]: " SSH_USER
SSH_USER="${SSH_USER:-$DEFAULT_SSH_USER}"
fi
# Test SSH connection - try key authentication first
log_info "Testing SSH connection to core server ($SSH_USER@$CORE_SERVER_IP)..."
if ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o BatchMode=yes "$SSH_USER@$CORE_SERVER_IP" "echo 'SSH connection successful'" 2>/dev/null; then
log_success "SSH connection established using key authentication"
USE_SSHPASS=false
else
# Key authentication failed, try password authentication
log_info "Key authentication failed, trying password authentication..."
read -s -p "Enter SSH password for $SSH_USER@$CORE_SERVER_IP: " SSH_PASSWORD
echo ""
if sshpass -p "$SSH_PASSWORD" ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "echo 'SSH connection successful'" 2>/dev/null; then
log_success "SSH connection established using password authentication"
USE_SSHPASS=true
else
log_error "Cannot connect to core server via SSH. Please check:"
echo " 1. SSH is running on the core server"
echo " 2. SSH keys are properly configured, or username/password are correct"
echo " 3. The core server IP is correct"
echo ""
TLS_ISSUES_SUMMARY="⚠️ TLS Configuration Issue: Cannot connect to core server $CORE_SERVER_IP via SSH
This will prevent Sablier from connecting to remote Docker daemons.
To fix this:
1. Ensure SSH is running on the core server
2. Configure SSH keys or provide correct password
3. Verify the core server IP is correct
4. Test SSH connection: ssh $SSH_USER@$CORE_SERVER_IP
Without SSH access, shared CA cannot be fetched for secure multi-server TLS."
return
fi
fi
# Fetch shared CA certificates from core server
log_info "Fetching shared CA certificates from core server..."
SHARED_CA_EXISTS=false
# Check if shared CA exists on core server (check both old and new locations)
if [ "$USE_SSHPASS" = true ] && [ -n "$SSH_PASSWORD" ]; then
if sshpass -p "$SSH_PASSWORD" ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/shared-ca/ca.pem ] && [ -f /opt/stacks/core/shared-ca/ca-key.pem ] && [ -r /opt/stacks/core/shared-ca/ca.pem ] && [ -r /opt/stacks/core/shared-ca/ca-key.pem ]" 2>/dev/null; then
SHARED_CA_EXISTS=true
SHARED_CA_PATH="/opt/stacks/core/shared-ca"
log_info "Detected CA certificate and key in shared-ca location"
elif sshpass -p "$SSH_PASSWORD" ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/docker-tls/ca.pem ] && [ -f /opt/stacks/core/docker-tls/ca-key.pem ] && [ -r /opt/stacks/core/docker-tls/ca.pem ] && [ -r /opt/stacks/core/docker-tls/ca-key.pem ]" 2>/dev/null; then
SHARED_CA_EXISTS=true
SHARED_CA_PATH="/opt/stacks/core/docker-tls"
log_info "Detected CA certificate and key in docker-tls location"
fi
else
if ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/shared-ca/ca.pem ] && [ -f /opt/stacks/core/shared-ca/ca-key.pem ] && [ -r /opt/stacks/core/shared-ca/ca.pem ] && [ -r /opt/stacks/core/shared-ca/ca-key.pem ]" 2>/dev/null; then
SHARED_CA_EXISTS=true
SHARED_CA_PATH="/opt/stacks/core/shared-ca"
log_info "Detected CA certificate and key in shared-ca location"
elif ssh -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP" "[ -f /opt/stacks/core/docker-tls/ca.pem ] && [ -f /opt/stacks/core/docker-tls/ca-key.pem ] && [ -r /opt/stacks/core/docker-tls/ca.pem ] && [ -r /opt/stacks/core/docker-tls/ca-key.pem ]" 2>/dev/null; then
SHARED_CA_EXISTS=true
SHARED_CA_PATH="/opt/stacks/core/docker-tls"
log_info "Detected CA certificate and key in docker-tls location"
fi
fi
if [ "$SHARED_CA_EXISTS" = true ]; then
# Copy existing shared CA from core server
set +e
scp_output=$(scp -o StrictHostKeyChecking=no "$SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca.pem" "$SSH_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca-key.pem" "$ca_dir/" 2>&1)
scp_exit_code=$?
set -e
if [ $scp_exit_code -eq 0 ]; then
log_success "Shared CA certificate and key fetched from core server"
setup_docker_tls
else
log_error "Failed to fetch shared CA certificate and key from core server"
TLS_ISSUES_SUMMARY="⚠️ TLS Configuration Issue: Could not copy shared CA from core server $CORE_SERVER_IP
SCP Error: $scp_output
To fix this:
1. Ensure SSH key authentication works: ssh $ACTUAL_USER@$CORE_SERVER_IP
2. Verify core server has: $SHARED_CA_PATH/ca.pem and ca-key.pem
3. Check file permissions on core server: ls -la $SHARED_CA_PATH/
4. Manually copy CA: scp $ACTUAL_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca.pem $ca_dir/
scp $ACTUAL_USER@$CORE_SERVER_IP:$SHARED_CA_PATH/ca-key.pem $ca_dir/
5. Regenerate server certificates: run setup_docker_tls after copying
6. Restart Docker: sudo systemctl restart docker
Then restart Sablier on the core server to reconnect."
return
fi
else
log_warning "Shared CA certificates not found on core server."
log_info "Please ensure the core server has been set up first and has generated the shared CA certificates."
TLS_ISSUES_SUMMARY="⚠️ TLS Configuration Issue: Shared CA certificates not found on core server $CORE_SERVER_IP
This will prevent Sablier from connecting to remote Docker daemons.
To fix this:
1. Ensure the core server is set up and has generated shared CA certificates
2. Verify SSH access: ssh $ACTUAL_USER@$CORE_SERVER_IP
3. Check core server locations: /opt/stacks/core/shared-ca/ or /opt/stacks/core/docker-tls/
4. Manually copy CA certificates if needed
5. Re-run the infrastructure deployment
Without shared CA, remote Docker access will not work securely."
return
fi
}
# Get script directory and repo directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
REPO_DIR="$( cd "$SCRIPT_DIR/.." && pwd )"
# Get actual user
if [ "$EUID" -eq 0 ]; then
ACTUAL_USER=${SUDO_USER:-$USER}
else
ACTUAL_USER=$USER
fi
# Default values
DOMAIN=""
SERVER_IP=""
CORE_SERVER_IP=""
ADMIN_USER=""
ADMIN_EMAIL=""
ADMIN_PASSWORD=""
@@ -43,6 +221,7 @@ DEPLOY_CORE=false
DEPLOY_INFRASTRUCTURE=false
DEPLOY_DASHBOARDS=false
SETUP_STACKS=false
TLS_ISSUES_SUMMARY=""
# Load existing .env file if it exists
load_env_file() {
@@ -56,8 +235,12 @@ load_env_file() {
echo " Domain: ${DOMAIN:-Not set}"
echo " Server IP: ${SERVER_IP:-Not set}"
echo " Server Hostname: ${SERVER_HOSTNAME:-Not set}"
echo " Admin User: ${AUTHELIA_ADMIN_USER:-Not set}"
echo " Admin Email: ${AUTHELIA_ADMIN_EMAIL:-Not set}"
echo " Default User: ${DEFAULT_USER:-Not set}"
if [ -n "${DEFAULT_PASSWORD:-}" ]; then
echo " Default Password: [HIDDEN]"
else
echo " Default Password: Not set"
fi
echo " Timezone: ${TZ:-Not set}"
echo ""
@@ -74,27 +257,29 @@ save_env_file() {
# Create .env file if it doesn't exist
if [ ! -f "$REPO_DIR/.env" ]; then
cp "$REPO_DIR/.env.example" "$REPO_DIR/.env"
sudo -u "$ACTUAL_USER" cp "$REPO_DIR/.env.example" "$REPO_DIR/.env"
fi
# Update values
sed -i "s%DOMAIN=.*%DOMAIN=$DOMAIN%" "$REPO_DIR/.env"
sed -i "s%SERVER_IP=.*%SERVER_IP=$SERVER_IP%" "$REPO_DIR/.env"
sed -i "s%SERVER_HOSTNAME=.*%SERVER_HOSTNAME=$SERVER_HOSTNAME%" "$REPO_DIR/.env"
sed -i "s%TZ=.*%TZ=$TZ%" "$REPO_DIR/.env"
# Update values as the actual user
sudo -u "$ACTUAL_USER" sed -i "s%DOMAIN=.*%DOMAIN=$DOMAIN%" "$REPO_DIR/.env"
sudo -u "$ACTUAL_USER" sed -i "s%SERVER_IP=.*%SERVER_IP=$SERVER_IP%" "$REPO_DIR/.env"
sudo -u "$ACTUAL_USER" sed -i "s%SERVER_HOSTNAME=.*%SERVER_HOSTNAME=$SERVER_HOSTNAME%" "$REPO_DIR/.env"
sudo -u "$ACTUAL_USER" sed -i "s%TZ=.*%TZ=$TZ%" "$REPO_DIR/.env"
# Authelia settings (only if deploying core)
# Authelia settings (only generate secrets if deploying core)
if [ "$DEPLOY_CORE" = true ]; then
# Ensure we have admin credentials
if [ -z "$ADMIN_USER" ]; then
ADMIN_USER="admin"
ADMIN_USER="${DEFAULT_USER:-admin}"
fi
if [ -z "$ADMIN_EMAIL" ]; then
ADMIN_EMAIL="${ADMIN_USER}@${DOMAIN}"
ADMIN_EMAIL="${DEFAULT_EMAIL:-${ADMIN_USER}@${DOMAIN}}"
fi
if [ -z "$ADMIN_PASSWORD" ]; then
ADMIN_PASSWORD="${DEFAULT_PASSWORD:-changeme123}"
if [ "$ADMIN_PASSWORD" = "changeme123" ]; then
log_info "Using default admin password (changeme123) - please change this after setup!"
ADMIN_PASSWORD="changeme123"
fi
fi
if [ -z "$AUTHELIA_JWT_SECRET" ]; then
@@ -108,11 +293,11 @@ save_env_file() {
fi
# Save Authelia settings to .env
sed -i "s%AUTHELIA_JWT_SECRET=.*%AUTHELIA_JWT_SECRET=$AUTHELIA_JWT_SECRET%" "$REPO_DIR/.env"
sed -i "s%AUTHELIA_SESSION_SECRET=.*%AUTHELIA_SESSION_SECRET=$AUTHELIA_SESSION_SECRET%" "$REPO_DIR/.env"
sed -i "s%AUTHELIA_STORAGE_ENCRYPTION_KEY=.*%AUTHELIA_STORAGE_ENCRYPTION_KEY=$AUTHELIA_STORAGE_ENCRYPTION_KEY%" "$REPO_DIR/.env"
sed -i "s%# AUTHELIA_ADMIN_USER=.*%AUTHELIA_ADMIN_USER=$ADMIN_USER%" "$REPO_DIR/.env"
sed -i "s%# AUTHELIA_ADMIN_EMAIL=.*%AUTHELIA_ADMIN_EMAIL=$ADMIN_EMAIL%" "$REPO_DIR/.env"
sudo -u "$ACTUAL_USER" sed -i "s%AUTHELIA_JWT_SECRET=.*%AUTHELIA_JWT_SECRET=$AUTHELIA_JWT_SECRET%" "$REPO_DIR/.env"
sudo -u "$ACTUAL_USER" sed -i "s%AUTHELIA_SESSION_SECRET=.*%AUTHELIA_SESSION_SECRET=$AUTHELIA_SESSION_SECRET%" "$REPO_DIR/.env"
sudo -u "$ACTUAL_USER" sed -i "s%AUTHELIA_STORAGE_ENCRYPTION_KEY=.*%AUTHELIA_STORAGE_ENCRYPTION_KEY=$AUTHELIA_STORAGE_ENCRYPTION_KEY%" "$REPO_DIR/.env"
sudo -u "$ACTUAL_USER" sed -i "s%# AUTHELIA_ADMIN_USER=.*%AUTHELIA_ADMIN_USER=$ADMIN_USER%" "$REPO_DIR/.env"
sudo -u "$ACTUAL_USER" sed -i "s%# AUTHELIA_ADMIN_EMAIL=.*%AUTHELIA_ADMIN_EMAIL=$ADMIN_EMAIL%" "$REPO_DIR/.env"
# Generate password hash if needed
if [ -z "$AUTHELIA_ADMIN_PASSWORD" ]; then
@@ -129,8 +314,8 @@ save_env_file() {
fi
# Save password hash
sed -i "s%# AUTHELIA_ADMIN_PASSWORD=.*%AUTHELIA_ADMIN_PASSWORD=$AUTHELIA_ADMIN_PASSWORD%" "$REPO_DIR/.env"
sed -i "s%AUTHELIA_ADMIN_PASSWORD=.*%AUTHELIA_ADMIN_PASSWORD=$AUTHELIA_ADMIN_PASSWORD%" "$REPO_DIR/.env"
sudo -u "$ACTUAL_USER" sed -i "s%# AUTHELIA_ADMIN_PASSWORD=.*%AUTHELIA_ADMIN_PASSWORD=$AUTHELIA_ADMIN_PASSWORD%" "$REPO_DIR/.env"
sudo -u "$ACTUAL_USER" sed -i "s%AUTHELIA_ADMIN_PASSWORD=.*%AUTHELIA_ADMIN_PASSWORD=$AUTHELIA_ADMIN_PASSWORD%" "$REPO_DIR/.env"
fi
log_success "Configuration saved to .env file"
@@ -139,64 +324,76 @@ save_env_file() {
# Prompt for required values
prompt_for_values() {
echo ""
log_info "Please provide the following information:"
echo " (Press Enter without typing to keep the current/default value shown in brackets)"
log_info "Configuration Setup:"
echo ""
# Set defaults from env file or hardcoded fallbacks
DEFAULT_DOMAIN="${DOMAIN:-example.duckdns.org}"
DEFAULT_SERVER_IP="${SERVER_IP:-$(hostname -I | awk '{print $1}')}"
DEFAULT_CORE_SERVER_IP="${CORE_SERVER_IP:-}"
DEFAULT_SERVER_HOSTNAME="${SERVER_HOSTNAME:-$(hostname)}"
DEFAULT_TZ="${TZ:-America/New_York}"
# Display current/default configuration
echo "Please review the following configuration:"
echo " Domain: $DEFAULT_DOMAIN"
echo " Server IP: $DEFAULT_SERVER_IP"
echo " Server Hostname: $DEFAULT_SERVER_HOSTNAME"
echo " Timezone: $DEFAULT_TZ"
if [ "$DEPLOY_CORE" = false ] && [ -z "$DEFAULT_CORE_SERVER_IP" ]; then
echo " Core Server IP: [Will be prompted for multi-server TLS]"
elif [ -n "$DEFAULT_CORE_SERVER_IP" ]; then
echo " Core Server IP: $DEFAULT_CORE_SERVER_IP"
fi
if [ "$DEPLOY_CORE" = true ]; then
DEFAULT_ADMIN_USER="${DEFAULT_USER:-admin}"
DEFAULT_ADMIN_EMAIL="${DEFAULT_EMAIL:-${DEFAULT_ADMIN_USER}@${DEFAULT_DOMAIN}}"
echo " Admin User: $DEFAULT_ADMIN_USER"
echo " Admin Email: $DEFAULT_ADMIN_EMAIL"
echo " Admin Password: [Will be prompted if needed]"
fi
echo ""
read -p "Use these default values? (Y/n): " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Nn]$ ]]; then
echo "Please enter custom values:"
echo ""
# Domain
if [ -z "$DOMAIN" ]; then
read -p "Enter your domain (e.g., example.duckdns.org): " DOMAIN
while [ -z "$DOMAIN" ]; do
log_warning "Domain is required"
read -p "Enter your domain (e.g., example.duckdns.org): " DOMAIN
done
else
read -p "Domain [$DOMAIN] (press Enter to keep current): " input
[ -n "$input" ] && DOMAIN="$input"
fi
read -p "Domain [$DEFAULT_DOMAIN]: " DOMAIN
DOMAIN="${DOMAIN:-$DEFAULT_DOMAIN}"
# Server IP
if [ -z "$SERVER_IP" ]; then
read -p "Enter your server IP address: " SERVER_IP
while [ -z "$SERVER_IP" ]; do
log_warning "Server IP is required"
read -p "Enter your server IP address: " SERVER_IP
done
else
read -p "Server IP [$SERVER_IP] (press Enter to keep current): " input
[ -n "$input" ] && SERVER_IP="$input"
fi
read -p "Server IP [$DEFAULT_SERVER_IP]: " SERVER_IP
SERVER_IP="${SERVER_IP:-$DEFAULT_SERVER_IP}"
# Server Hostname
if [ -z "$SERVER_HOSTNAME" ]; then
SERVER_HOSTNAME="debian"
fi
read -p "Server hostname [$SERVER_HOSTNAME] (press Enter to keep current): " input
[ -n "$input" ] && SERVER_HOSTNAME="$input"
read -p "Server Hostname [$DEFAULT_SERVER_HOSTNAME]: " SERVER_HOSTNAME
SERVER_HOSTNAME="${SERVER_HOSTNAME:-$DEFAULT_SERVER_HOSTNAME}"
# Timezone
if [ -z "$TZ" ]; then
TZ="America/New_York"
read -p "Timezone [$DEFAULT_TZ]: " TZ
TZ="${TZ:-$DEFAULT_TZ}"
# Core server IP (for multi-server setup)
if [ "$DEPLOY_CORE" = false ]; then
echo ""
read -p "Core server IP (for shared TLS CA): " CORE_SERVER_IP
fi
read -p "Timezone [$TZ] (press Enter to keep current): " input
[ -n "$input" ] && TZ="$input"
# Admin credentials (only if deploying core)
if [ "$DEPLOY_CORE" = true ]; then
echo ""
log_info "Authelia Admin Credentials:"
if [ -z "$ADMIN_USER" ]; then
ADMIN_USER="admin"
fi
read -p "Admin username [$ADMIN_USER] (press Enter to keep current): " input
[ -n "$input" ] && ADMIN_USER="$input"
read -p "Admin username [$DEFAULT_ADMIN_USER]: " ADMIN_USER
ADMIN_USER="${ADMIN_USER:-$DEFAULT_ADMIN_USER}"
if [ -z "$ADMIN_EMAIL" ]; then
ADMIN_EMAIL="${ADMIN_USER}@${DOMAIN}"
fi
read -p "Admin email [$ADMIN_EMAIL] (press Enter to keep current): " input
[ -n "$input" ] && ADMIN_EMAIL="$input"
read -p "Admin email [$DEFAULT_ADMIN_EMAIL]: " ADMIN_EMAIL
ADMIN_EMAIL="${ADMIN_EMAIL:-$DEFAULT_ADMIN_EMAIL}"
if [ -z "$ADMIN_PASSWORD" ]; then
while [ -z "$ADMIN_PASSWORD" ]; do
@@ -211,6 +408,19 @@ prompt_for_values() {
log_info "Admin password already configured"
fi
fi
else
# Use defaults
DOMAIN="$DEFAULT_DOMAIN"
SERVER_IP="$DEFAULT_SERVER_IP"
SERVER_HOSTNAME="$DEFAULT_SERVER_HOSTNAME"
TZ="$DEFAULT_TZ"
CORE_SERVER_IP="$DEFAULT_CORE_SERVER_IP"
if [ "$DEPLOY_CORE" = true ]; then
ADMIN_USER="$DEFAULT_ADMIN_USER"
ADMIN_EMAIL="$DEFAULT_ADMIN_EMAIL"
fi
fi
echo ""
}
@@ -235,12 +445,18 @@ system_setup() {
# Step 2: Install required packages
log_info "Step 2/10: Installing required packages..."
apt-get install -y curl wget git htop nano vim ufw fail2ban unattended-upgrades apt-listchanges
apt-get install -y curl wget git htop nano vim ufw fail2ban unattended-upgrades apt-listchanges sshpass
# Step 3: Install Docker
log_info "Step 3/10: Installing Docker..."
if command -v docker &> /dev/null && docker --version &> /dev/null; then
log_success "Docker is already installed ($(docker --version))"
# Check if user is in docker group
if ! groups "$ACTUAL_USER" | grep -q docker; then
log_info "Adding $ACTUAL_USER to docker group..."
usermod -aG docker "$ACTUAL_USER"
NEEDS_LOGOUT=true
fi
# Check if Docker service is running
if ! systemctl is-active --quiet docker; then
log_warning "Docker service is not running, starting it..."
@@ -253,6 +469,7 @@ system_setup() {
else
curl -fsSL https://get.docker.com | sh
usermod -aG docker "$ACTUAL_USER"
NEEDS_LOGOUT=true
fi
# Step 4: Install Docker Compose
@@ -265,107 +482,79 @@ system_setup() {
log_success "Docker Compose installed ($(docker-compose --version))"
fi
# Step 5: Configure UFW firewall
log_info "Step 5/10: Configuring firewall..."
# Step 5: Generate shared CA for multi-server TLS
log_info "Step 5/10: Generating shared CA certificate for multi-server TLS..."
generate_shared_ca
# Step 6: Configure Docker TLS
log_info "Step 6/10: Configuring Docker TLS..."
setup_docker_tls
# Step 7: Configure UFW firewall
log_info "Step 7/10: Configuring firewall..."
ufw --force enable
ufw allow ssh
ufw allow 80
ufw allow 443
ufw allow 2376/tcp # Docker TLS port
log_success "Firewall configured"
# Step 6: Configure automatic updates
log_info "Step 6/10: Configuring automatic updates..."
# Step 8: Configure automatic updates
log_info "Step 8/10: Configuring automatic updates..."
dpkg-reconfigure -f noninteractive unattended-upgrades
# Step 7: Create required directories
log_info "Step 7/10: Creating required directories..."
mkdir -p /opt/stacks/core
mkdir -p /opt/stacks/infrastructure
mkdir -p /opt/stacks/dashboards
mkdir -p /opt/dockge
# Step 8: Set proper ownership
log_info "Step 8/10: Setting directory ownership..."
chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/stacks
chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/dockge
# Step 9: Create Docker networks
log_info "Step 9/10: Creating Docker networks..."
# Step 10: Create Docker networks
log_info "Step 10/10: Creating Docker networks..."
docker network create homelab-network 2>/dev/null && log_success "Created homelab-network" || log_info "homelab-network already exists"
docker network create traefik-network 2>/dev/null && log_success "Created traefik-network" || log_info "traefik-network already exists"
docker network create media-network 2>/dev/null && log_success "Created media-network" || log_info "media-network already exists"
# Step 10: Generate SSH keys for Git (optional)
log_info "Step 10/10: SSH key setup (optional)..."
if [ ! -f "/home/$ACTUAL_USER/.ssh/id_rsa" ]; then
log_info "Generating SSH key for $ACTUAL_USER..."
sudo -u "$ACTUAL_USER" ssh-keygen -t rsa -b 4096 -f "/home/$ACTUAL_USER/.ssh/id_rsa" -N ""
log_info "SSH public key:"
cat "/home/$ACTUAL_USER/.ssh/id_rsa.pub"
echo ""
log_info "Add this key to your Git provider (GitHub, GitLab, etc.)"
fi
# Step 9: Set proper ownership
log_info "Step 9/10: Setting directory ownership..."
chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt
log_success "System setup completed!"
echo ""
if [ "$NEEDS_LOGOUT" = true ]; then
log_info "Please log out and back in for Docker group changes to take effect."
echo ""
fi
}
# Deployment function
perform_deployment() {
log_info "Starting deployment..."
# Switch back to regular user if we were running as root
if [ "$EUID" -eq 0 ]; then
ACTUAL_USER=${SUDO_USER:-$USER}
log_info "Switching to user $ACTUAL_USER for deployment..."
exec sudo -u "$ACTUAL_USER" "$0" "$@"
fi
# Source the .env file
source "$REPO_DIR/.env"
# Step 1: Create required directories
log_info "Step 1: Creating required directories..."
mkdir -p /opt/stacks/core
mkdir -p /opt/stacks/infrastructure
mkdir -p /opt/stacks/dashboards
mkdir -p /opt/dockge
log_success "Directories created"
# Step 2: Create Docker networks (if they don't exist)
log_info "Step 2: Creating Docker networks..."
docker network create homelab-network 2>/dev/null && log_success "Created homelab-network" || log_info "homelab-network already exists"
docker network create traefik-network 2>/dev/null && log_success "Created traefik-network" || log_info "traefik-network already exists"
docker network create media-network 2>/dev/null && log_success "Created media-network" || log_info "media-network already exists"
echo ""
# Step 3: Deploy Dockge (always deployed)
log_info "Step 3: Deploying Dockge stack manager..."
# Deploy Dockge function
deploy_dockge() {
log_info "Deploying Dockge stack manager..."
log_info " - Dockge (Docker Compose Manager)"
echo ""
# Copy Dockge stack files
cp "$REPO_DIR/docker-compose/dockge/docker-compose.yml" /opt/dockge/docker-compose.yml
cp "$REPO_DIR/.env" /opt/dockge/.env
sudo cp "$REPO_DIR/docker-compose/dockge/docker-compose.yml" /opt/dockge/docker-compose.yml
sudo cp "$REPO_DIR/.env" /opt/dockge/.env
# Replace placeholders in Dockge compose file
replace_env_placeholders "/opt/dockge/docker-compose.yml"
# Deploy Dockge stack
cd /opt/dockge
docker compose up -d
log_success "Dockge deployed"
echo ""
}
# Deploy core infrastructure
if [ "$DEPLOY_CORE" = true ]; then
log_info "Step 4: Deploying core infrastructure stack..."
# Deploy core stack function
deploy_core() {
log_info "Deploying core stack..."
log_info " - DuckDNS (Dynamic DNS)"
log_info " - Traefik (Reverse Proxy with SSL)"
log_info " - Authelia (Single Sign-On)"
echo ""
# Copy core stack files
cp "$REPO_DIR/docker-compose/core/docker-compose.yml" /opt/stacks/core/docker-compose.yml
cp "$REPO_DIR/.env" /opt/stacks/core/.env
sudo cp "$REPO_DIR/docker-compose/core/docker-compose.yml" /opt/stacks/core/docker-compose.yml
sudo cp "$REPO_DIR/.env" /opt/stacks/core/.env
# Replace placeholders in core compose file
replace_env_placeholders "/opt/stacks/core/docker-compose.yml"
# Copy configs
if [ -d "/opt/stacks/core/traefik" ]; then
@@ -398,17 +587,23 @@ perform_deployment() {
sed -i "s/\${DEFAULT_EMAIL}/${AUTHELIA_ADMIN_EMAIL}/g" /opt/stacks/core/authelia/users_database.yml
sed -i "s|\$argon2id\$v=19\$m=65536,t=3,p=4\$CHANGEME|${AUTHELIA_ADMIN_PASSWORD}|g" /opt/stacks/core/authelia/users_database.yml
# Generate shared CA for multi-server TLS
log_info "Generating shared CA certificate for multi-server TLS..."
generate_shared_ca
# Replace placeholders in core compose file
replace_env_placeholders "/opt/stacks/core/docker-compose.yml"
# Deploy core stack
cd /opt/stacks/core
docker compose up -d
log_success "Core infrastructure deployed"
echo ""
fi
}
# Deploy infrastructure stack
if [ "$DEPLOY_INFRASTRUCTURE" = true ]; then
step_num=$([ "$DEPLOY_CORE" = true ] && echo "5" || echo "4")
log_info "Step $step_num: Deploying infrastructure stack..."
# Deploy infrastructure stack function
deploy_infrastructure() {
log_info "Deploying infrastructure stack..."
log_info " - Pi-hole (DNS Ad Blocker)"
log_info " - Watchtower (Container Updates)"
log_info " - Dozzle (Log Viewer)"
@@ -420,6 +615,9 @@ perform_deployment() {
cp "$REPO_DIR/docker-compose/infrastructure/docker-compose.yml" /opt/stacks/infrastructure/docker-compose.yml
cp "$REPO_DIR/.env" /opt/stacks/infrastructure/.env
# Replace placeholders in infrastructure compose file
replace_env_placeholders "/opt/stacks/infrastructure/docker-compose.yml"
# Copy any additional config directories
for config_dir in "$REPO_DIR/docker-compose/infrastructure"/*/; do
if [ -d "$config_dir" ] && [ "$(basename "$config_dir")" != "." ]; then
@@ -433,53 +631,186 @@ perform_deployment() {
sed -i '/middlewares=authelia@docker/d' /opt/stacks/infrastructure/docker-compose.yml
fi
# Replace placeholders in infrastructure compose file
replace_env_placeholders "/opt/stacks/infrastructure/docker-compose.yml"
# Deploy infrastructure stack
cd /opt/stacks/infrastructure
docker compose up -d
log_success "Infrastructure stack deployed"
echo ""
fi
}
# Deploy dashboard stack
if [ "$DEPLOY_DASHBOARDS" = true ]; then
if [ "$DEPLOY_CORE" = true ] && [ "$DEPLOY_INFRASTRUCTURE" = true ]; then
step_num=6
elif [ "$DEPLOY_CORE" = true ] || [ "$DEPLOY_INFRASTRUCTURE" = true ]; then
step_num=5
else
step_num=4
fi
log_info "Step $step_num: Deploying dashboard stack..."
# Deploy dashboards stack function
deploy_dashboards() {
log_info "Deploying dashboard stack..."
log_info " - Homepage (Application Dashboard)"
log_info " - Homarr (Modern Dashboard)"
echo ""
# Create dashboards directory
mkdir -p /opt/stacks/dashboards
sudo mkdir -p /opt/stacks/dashboards
# Copy dashboards compose file
cp "$REPO_DIR/docker-compose/dashboards/docker-compose.yml" /opt/stacks/dashboards/docker-compose.yml
cp "$REPO_DIR/.env" /opt/stacks/dashboards/.env
# Replace placeholders in dashboards compose file
replace_env_placeholders "/opt/stacks/dashboards/docker-compose.yml"
# Copy homepage config
if [ -d "$REPO_DIR/docker-compose/dashboards/homepage" ]; then
cp -r "$REPO_DIR/docker-compose/dashboards/homepage" /opt/stacks/dashboards/
fi
# Replace placeholders in dashboards compose file
replace_env_placeholders "/opt/stacks/dashboards/docker-compose.yml"
# Deploy dashboards stack
cd /opt/stacks/dashboards
docker compose up -d
log_success "Dashboard stack deployed"
echo ""
}
# Deployment function
perform_deployment() {
log_info "Starting deployment..."
# Initialize missing vars summary
MISSING_VARS_SUMMARY=""
TLS_ISSUES_SUMMARY=""
# Switch back to regular user if we were running as root
if [ "$EUID" -eq 0 ]; then
ACTUAL_USER=${SUDO_USER:-$USER}
log_info "Switching to user $ACTUAL_USER for deployment..."
exec sudo -u "$ACTUAL_USER" "$0" "$@"
fi
# Source the .env file
source "$REPO_DIR/.env"
# Step 1: Create required directories
log_info "Step 1: Creating required directories..."
sudo mkdir -p /opt/stacks/core || { log_error "Failed to create /opt/stacks/core"; exit 1; }
sudo mkdir -p /opt/stacks/infrastructure || { log_error "Failed to create /opt/stacks/infrastructure"; exit 1; }
sudo mkdir -p /opt/stacks/dashboards || { log_error "Failed to create /opt/stacks/dashboards"; exit 1; }
sudo mkdir -p /opt/dockge || { log_error "Failed to create /opt/dockge"; exit 1; }
sudo chown -R "$USER:$USER" /opt/stacks
sudo chown -R "$USER:$USER" /opt/dockge
log_success "Directories created"
# Step 2: Setup multi-server TLS if needed
if [ "$DEPLOY_CORE" = false ]; then
setup_multi_server_tls
fi
# Step 3: Create Docker networks (if they don't exist)
log_info "Step $([ "$DEPLOY_CORE" = false ] && echo "3" || echo "2"): Creating Docker networks..."
docker network create homelab-network 2>/dev/null && log_success "Created homelab-network" || log_info "homelab-network already exists"
docker network create traefik-network 2>/dev/null && log_success "Created traefik-network" || log_info "traefik-network already exists"
docker network create media-network 2>/dev/null && log_success "Created media-network" || log_info "media-network already exists"
echo ""
# Step 4: Deploy Dockge (always deployed)
deploy_dockge
# Deploy core stack
if [ "$DEPLOY_CORE" = true ]; then
deploy_core
fi
# Deploy infrastructure stack
if [ "$DEPLOY_INFRASTRUCTURE" = true ]; then
step_num=$([ "$DEPLOY_CORE" = true ] && echo "6" || echo "5")
deploy_infrastructure
fi
# Deploy dashboard stack
if [ "$DEPLOY_DASHBOARDS" = true ]; then
if [ "$DEPLOY_CORE" = true ] && [ "$DEPLOY_INFRASTRUCTURE" = true ]; then
step_num=7
elif [ "$DEPLOY_CORE" = true ] || [ "$DEPLOY_INFRASTRUCTURE" = true ]; then
step_num=6
else
step_num=5
fi
deploy_dashboards
fi
# Setup stacks for Dockge
if [ "$SETUP_STACKS" = true ]; then
setup_stacks_for_dockge
fi
# Report any missing variables
if [ -n "$MISSING_VARS_SUMMARY" ]; then
log_warning "The following environment variables were missing and may cause issues:"
echo "$MISSING_VARS_SUMMARY"
log_info "Please update your .env file and redeploy affected stacks."
fi
# Report any TLS issues
if [ -n "$TLS_ISSUES_SUMMARY" ]; then
echo ""
log_warning "TLS Configuration Issues Detected:"
echo "$TLS_ISSUES_SUMMARY"
echo ""
fi
}
# Setup stacks for Dockge function
# Setup Docker TLS function
setup_docker_tls() {
local TLS_DIR="/home/$ACTUAL_USER/EZ-Homelab/docker-tls"
# Create TLS directory
sudo mkdir -p "$TLS_DIR"
sudo chown "$ACTUAL_USER:$ACTUAL_USER" "$TLS_DIR"
# Use shared CA if available, otherwise generate local CA
if [ -f "/opt/stacks/core/shared-ca/ca.pem" ] && [ -f "/opt/stacks/core/shared-ca/ca-key.pem" ]; then
log_info "Using shared CA certificate for Docker TLS..."
cp "/opt/stacks/core/shared-ca/ca.pem" "$TLS_DIR/ca.pem"
cp "/opt/stacks/core/shared-ca/ca-key.pem" "$TLS_DIR/ca-key.pem"
else
log_info "Generating local CA certificate for Docker TLS..."
# Generate CA
openssl genrsa -out "$TLS_DIR/ca-key.pem" 4096
openssl req -new -x509 -days 365 -key "$TLS_DIR/ca-key.pem" -sha256 -out "$TLS_DIR/ca.pem" -subj "/C=US/ST=State/L=City/O=Organization/CN=Docker-CA"
fi
# Generate server key and cert
openssl genrsa -out "$TLS_DIR/server-key.pem" 4096
openssl req -subj "/CN=$SERVER_IP" -new -key "$TLS_DIR/server-key.pem" -out "$TLS_DIR/server.csr"
echo "subjectAltName = DNS:$SERVER_IP,IP:$SERVER_IP,IP:127.0.0.1" > "$TLS_DIR/extfile.cnf"
openssl x509 -req -days 365 -in "$TLS_DIR/server.csr" -CA "$TLS_DIR/ca.pem" -CAkey "$TLS_DIR/ca-key.pem" -CAcreateserial -out "$TLS_DIR/server-cert.pem" -extfile "$TLS_DIR/extfile.cnf"
# Generate client key and cert
openssl genrsa -out "$TLS_DIR/client-key.pem" 4096
openssl req -subj "/CN=client" -new -key "$TLS_DIR/client-key.pem" -out "$TLS_DIR/client.csr"
openssl x509 -req -days 365 -in "$TLS_DIR/client.csr" -CA "$TLS_DIR/ca.pem" -CAkey "$TLS_DIR/ca-key.pem" -CAcreateserial -out "$TLS_DIR/client-cert.pem"
# Configure Docker daemon
sudo tee /etc/docker/daemon.json > /dev/null <<EOF
{
"tls": true,
"tlsverify": true,
"tlscacert": "$TLS_DIR/ca.pem",
"tlscert": "$TLS_DIR/server-cert.pem",
"tlskey": "$TLS_DIR/server-key.pem"
}
EOF
# Update systemd service
sudo sed -i 's|-H fd://|-H fd:// -H tcp://0.0.0.0:2376|' /lib/systemd/system/docker.service
# Reload and restart Docker
sudo systemctl daemon-reload
sudo systemctl restart docker
log_success "Docker TLS configured on port 2376"
}
setup_stacks_for_dockge() {
log_info "Setting up all stacks for Dockge..."
@@ -496,6 +827,9 @@ setup_stacks_for_dockge() {
cp "$REPO_STACK_DIR/docker-compose.yml" "$STACK_DIR/docker-compose.yml"
cp "$REPO_DIR/.env" "$STACK_DIR/.env"
# Replace placeholders in the compose file
replace_env_placeholders "$STACK_DIR/docker-compose.yml"
# Copy any additional config directories
for config_dir in "$REPO_STACK_DIR"/*/; do
if [ -d "$config_dir" ] && [ "$(basename "$config_dir")" != "." ]; then
@@ -535,6 +869,7 @@ show_main_menu() {
echo "3) 🔧 Infrastructure Only"
echo " - Deploy Dockge and monitoring tools"
echo " - Requires existing Traefik (from previous setup)"
echo " - Configures TLS for remote Docker access (Sablier)"
echo " - Services accessible without authentication"
echo " - All stacks prepared for Dockge"
echo ""
@@ -569,7 +904,7 @@ main() {
log_info "Selected: Core Only"
DEPLOY_CORE=true
DEPLOY_INFRASTRUCTURE=false
DEPLOY_DASHBOARDS=false
DEPLOY_DASHBOARDS=true
SETUP_STACKS=true
;;
3)
@@ -627,6 +962,21 @@ main() {
fi
fi
# Ensure required directories exist
log_info "Ensuring required directories exist..."
if [ "$EUID" -eq 0 ]; then
mkdir -p /opt/stacks/core
mkdir -p /opt/stacks/infrastructure
mkdir -p /opt/stacks/dashboards
mkdir -p /opt/dockge
else
sudo mkdir -p /opt/stacks/core
sudo mkdir -p /opt/stacks/infrastructure
sudo mkdir -p /opt/stacks/dashboards
sudo mkdir -p /opt/dockge
fi
log_success "Directories ready"
# Prompt for configuration values
prompt_for_values

View File

@@ -1,10 +1,6 @@
Info
I am developing the EZ-Homelab repository. The focus is on the repo not the test system.
I'm troubleshooting the ez-homelab.sh setup process. The script works pretty well. However I noticed some of the compose files
are missing a ports section. In order for a remote traefik & sablier install to work, the sablier.yml and traefik external host yml
files are configured on the remote server using the ip:port of the service. Therefore all services in all compose files must include
port mapping if it has a webui.
Make the changes in the repo folder, then I will run the ez-homelab.sh script and check the results.
I'm troubleshooting the ez-homelab.sh setup process.
Install notes