Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ PORTAINER_OAUTH_CLIENT_SECRET=
# DATABASES (shared stack)
# -----------------------------------------------------------------------------
POSTGRES_PASSWORD= # REQUIRED: master postgres password
POSTGRES_ROOT_PASSWORD=${POSTGRES_PASSWORD}
REDIS_PASSWORD= # REQUIRED
MARIADB_ROOT_PASSWORD= # REQUIRED

Expand Down Expand Up @@ -94,6 +95,8 @@ NEXTCLOUD_ADMIN_PASSWORD= # REQUIRED
# -----------------------------------------------------------------------------
MEDIA_ROOT=/opt/homelab/media # Host path for media files
DOWNLOADS_ROOT=/opt/homelab/downloads
MEDIA_PATH=${MEDIA_ROOT}
DOWNLOAD_PATH=${DOWNLOADS_ROOT}

# -----------------------------------------------------------------------------
# OLLAMA / AI
Expand Down
125 changes: 125 additions & 0 deletions .env.test.tmp
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# =============================================================================
# HomeLab Stack — Environment Configuration
# Copy this file to .env and fill in your values
# Run: cp .env.example .env && ./scripts/setup-env.sh
# =============================================================================

# -----------------------------------------------------------------------------
# GENERAL
# -----------------------------------------------------------------------------
TZ=Asia/Shanghai
PUID=1000
PGID=1000
DOMAIN=yourdomain.com # Your base domain (e.g. home.example.com)
ACME_EMAIL=you@example.com # Let's Encrypt notification email

# -----------------------------------------------------------------------------
# TRAEFIK
# -----------------------------------------------------------------------------
TRAEFIK_DASHBOARD_USER=admin
# Generate password hash: echo $(htpasswd -nb admin yourpassword) | sed -e s/\$/\$\$/g
TRAEFIK_DASHBOARD_PASSWORD_HASH=

# -----------------------------------------------------------------------------
# PORTAINER
# -----------------------------------------------------------------------------
# No config needed — admin password set on first login

# -----------------------------------------------------------------------------
# AUTHENTIK (SSO)
# -----------------------------------------------------------------------------
AUTHENTIK_SECRET_KEY= # REQUIRED: openssl rand -base64 32
AUTHENTIK_POSTGRES_PASSWORD= # REQUIRED: strong random password
AUTHENTIK_REDIS_PASSWORD= # REQUIRED: strong random password
AUTHENTIK_ADMIN_EMAIL=
AUTHENTIK_ADMIN_PASSWORD=
AUTHENTIK_DOMAIN=auth.${DOMAIN}

# OAuth2 clients — auto-filled by scripts/setup-authentik.sh
GRAFANA_OAUTH_CLIENT_ID=
GRAFANA_OAUTH_CLIENT_SECRET=
GITEA_OAUTH_CLIENT_ID=
GITEA_OAUTH_CLIENT_SECRET=
OUTLINE_OAUTH_CLIENT_ID=
OUTLINE_OAUTH_CLIENT_SECRET=
PORTAINER_OAUTH_CLIENT_ID=
PORTAINER_OAUTH_CLIENT_SECRET=

# -----------------------------------------------------------------------------
# DATABASES (shared stack)
# -----------------------------------------------------------------------------
POSTGRES_PASSWORD= # REQUIRED: master postgres password
POSTGRES_ROOT_PASSWORD=${POSTGRES_PASSWORD}
REDIS_PASSWORD= # REQUIRED
MARIADB_ROOT_PASSWORD= # REQUIRED

# Per-service database credentials
GITEA_DB_PASSWORD=
NEXTCLOUD_DB_PASSWORD=
OUTLINE_DB_PASSWORD=
AUTHENTIK_DB_PASSWORD=

# -----------------------------------------------------------------------------
# GRAFANA
# -----------------------------------------------------------------------------
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD= # REQUIRED

# -----------------------------------------------------------------------------
# VAULTWARDEN
# -----------------------------------------------------------------------------
VAULTWARDEN_ADMIN_TOKEN= # REQUIRED: openssl rand -base64 48

# -----------------------------------------------------------------------------
# WIREGUARD
# -----------------------------------------------------------------------------
WG_HOST= # Your public IP or domain
WG_PASSWORD= # WireGuard Easy web UI password
WG_PORT=51820

# -----------------------------------------------------------------------------
# CLOUDFLARE DDNS
# -----------------------------------------------------------------------------
CF_API_TOKEN=
CF_ZONE_ID=
CF_RECORD_NAME=

# -----------------------------------------------------------------------------
# NEXTCLOUD
# -----------------------------------------------------------------------------
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD= # REQUIRED

# -----------------------------------------------------------------------------
# MEDIA STACK
# -----------------------------------------------------------------------------
MEDIA_ROOT=/opt/homelab/media # Host path for media files
DOWNLOADS_ROOT=/opt/homelab/downloads
MEDIA_PATH=${MEDIA_ROOT}
DOWNLOAD_PATH=${DOWNLOADS_ROOT}

# -----------------------------------------------------------------------------
# OLLAMA / AI
# -----------------------------------------------------------------------------
OLLAMA_GPU_ENABLED=false # Set to true if you have NVIDIA GPU

# -----------------------------------------------------------------------------
# NOTIFICATIONS
# -----------------------------------------------------------------------------
GOTIFY_PASSWORD= # REQUIRED
NTFY_AUTH_ENABLED=true

# -----------------------------------------------------------------------------
# NETWORK PROXY (optional — for CN users with local proxy)
# -----------------------------------------------------------------------------
HTTP_PROXY=
HTTPS_PROXY=
NO_PROXY=localhost,127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
DOCKER_PROXY_ENABLED=false

# -----------------------------------------------------------------------------
# CN MIRROR CONFIG (auto-set by setup-cn-mirrors.sh)
# -----------------------------------------------------------------------------
CN_MODE=false
CN_APT_MIRROR=https://mirrors.aliyun.com/ubuntu
CN_DOCKER_MIRROR=https://docker.m.daocloud.io
36 changes: 36 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Integration Tests

on:
push:
paths:
- 'stacks/**'
- 'scripts/**'
- 'tests/**'
- '.github/workflows/test.yml'
pull_request:

jobs:
test:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Prepare env
run: |
cp .env.example .env
docker network create proxy
touch config/traefik/acme.json
chmod 600 config/traefik/acme.json
- name: Validate compose syntax
run: |
for f in stacks/*/docker-compose.yml; do
docker compose -f "$f" config --quiet
done
- name: Start base stack
run: docker compose -f stacks/base/docker-compose.yml -f stacks/base/docker-compose.local.yml up -d
- name: Run base tests
run: tests/run-tests.sh --stack base
- name: Upload report
uses: actions/upload-artifact@v4
with:
name: test-report
path: tests/results/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ volumes/
logs/
*.log
~/.homelab/
tests/results/

# OS
.DS_Store
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ cd homelab-stack
./install.sh

# 3. Launch base infrastructure
docker compose -f docker-compose.base.yml up -d
docker compose -f stacks/base/docker-compose.yml up -d

# 4. Launch any stack
./scripts/stack-manager.sh start media
Expand Down Expand Up @@ -86,7 +86,7 @@ All stacks share:
```
homelab-stack/
├── install.sh # Entry point — env check + guided setup
├── docker-compose.base.yml # Core infrastructure
├── stacks/base/docker-compose.yml # Core infrastructure
├── .env.example # All configurable variables
├── BOUNTY.md # Bounty task overview
Expand Down
2 changes: 1 addition & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ chmod 600 config/traefik/acme.json 2>/dev/null || touch config/traefik/acme.json
# Step 5: Launch base infrastructure
# ---------------------------------------------------------------------------
log_step "Launching base infrastructure"
docker compose -f docker-compose.base.yml up -d
docker compose -f stacks/base/docker-compose.yml up -d

log_info ""
log_info "${GREEN}${BOLD}✓ Base infrastructure is up!${NC}"
Expand Down
2 changes: 1 addition & 1 deletion scripts/backup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)"
BASE_DIR="$SCRIPT_DIR/.."
ENV_FILE="$BASE_DIR/config/.env"
ENV_FILE="$BASE_DIR/.env"

[[ -f "$ENV_FILE" ]] && source "$ENV_FILE"

Expand Down
2 changes: 1 addition & 1 deletion scripts/stack-manager.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ stack_start() {
compose_file=$(get_compose_file "$stack")
log_info "Starting stack: $stack"
# Load .env if exists
local env_file="$BASE_DIR/config/.env"
local env_file="$BASE_DIR/.env"
[[ -f "$env_file" ]] && set -a && source "$env_file" && set +a
docker compose -f "$compose_file" up -d --remove-orphans
log_info "Stack $stack started"
Expand Down
141 changes: 3 additions & 138 deletions scripts/test-stacks.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,140 +1,5 @@
#!/usr/bin/env bash
# =============================================================================
# HomeLab Stack Integration Tests
# =============================================================================
set -uo pipefail
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)"
BASE_DIR="$SCRIPT_DIR/.."
ENV_FILE="$BASE_DIR/config/.env"
[[ -f "$ENV_FILE" ]] && source "$ENV_FILE"

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m'

PASSED=0; FAILED=0; SKIPPED=0

log_pass() { echo -e " ${GREEN}✓${NC} $*"; ((PASSED++)); }
log_fail() { echo -e " ${RED}✗${NC} $*"; ((FAILED++)); }
log_skip() { echo -e " ${YELLOW}~${NC} $* (skipped)"; ((SKIPPED++)); }
log_group() { echo -e "\n${BLUE}${BOLD}[$*]${NC}"; }

http_check() {
local name=$1 url=$2 expected=${3:-200}
local code
code=$(curl -sf -o /dev/null -w '%{http_code}' --connect-timeout 5 --max-time 10 "$url" 2>/dev/null || echo 000)
if [[ "$code" == "$expected" ]] || [[ "$code" =~ ^[23] ]]; then
log_pass "$name ($url) → HTTP $code"
else
log_fail "$name ($url) → HTTP $code (expected ~2xx/3xx)"
fi
}

container_check() {
local name=$1
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${name}$"; then
local health
health=$(docker inspect --format '{{.State.Health.Status}}' "$name" 2>/dev/null || echo 'no-healthcheck')
if [[ "$health" == 'healthy' ]] || [[ "$health" == 'no-healthcheck' ]]; then
log_pass "Container $name is running ($health)"
else
log_fail "Container $name unhealthy: $health"
fi
else
log_skip "Container $name not running"
fi
}

port_check() {
local name=$1 host=${2:-localhost} port=$3
if nc -z -w3 "$host" "$port" 2>/dev/null; then
log_pass "$name port $port is open"
else
log_skip "$name port $port not reachable"
fi
}

# ---- Tests ----

log_group "Base Infrastructure"
container_check traefik
container_check portainer
container_check watchtower
port_check Traefik localhost 80
port_check Traefik-HTTPS localhost 443

log_group "SSO (Authentik)"
container_check authentik-server
container_check authentik-worker
container_check authentik-postgresql
container_check authentik-redis
http_check Authentik "http://localhost:9000/if/flow/default-authentication-flow/"

log_group "Monitoring"
container_check prometheus
container_check grafana
container_check loki
container_check alertmanager
http_check Prometheus "http://localhost:9090/-/healthy"
http_check Grafana "http://localhost:3000/api/health"
http_check Alertmanager "http://localhost:9093/-/healthy"

log_group "Databases"
container_check homelab-postgres
container_check homelab-redis
container_check homelab-mariadb
port_check PostgreSQL localhost 5432
port_check Redis localhost 6379
port_check MariaDB localhost 3306

log_group "Media Stack"
container_check jellyfin
container_check sonarr
container_check radarr
container_check qbittorrent
http_check Jellyfin "http://localhost:8096/health"

log_group "Productivity Stack"
container_check gitea
container_check vaultwarden
http_check Gitea "http://localhost:3001"
http_check Vaultwarden "http://localhost:8080"

log_group "Network Stack"
container_check adguardhome
container_check nginx-proxy-manager
container_check wg-easy
port_check WireGuard localhost 51820

log_group "Storage Stack"
container_check nextcloud
container_check minio
container_check filebrowser
http_check MinIO "http://localhost:9001"

log_group "AI Stack"
container_check ollama
container_check open-webui
http_check Ollama "http://localhost:11434"

log_group "Home Automation"
container_check homeassistant
container_check node-red
http_check HomeAssistant "http://localhost:8123"
http_check NodeRED "http://localhost:1880"

log_group "Notifications"
container_check ntfy
http_check ntfy "http://localhost:2586"

log_group "Dashboard"
container_check homepage
http_check Homepage "http://localhost:3010"

# ---- Summary ----
echo ""
echo -e "${BOLD}========================================${NC}"
echo -e " Results: ${GREEN}$PASSED passed${NC} | ${RED}$FAILED failed${NC} | ${YELLOW}$SKIPPED skipped${NC}"
echo -e "${BOLD}========================================${NC}"

[[ $FAILED -eq 0 ]] && exit 0 || exit 1
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.."; pwd)"
exec "$BASE_DIR/tests/run-tests.sh" --all "$@"
3 changes: 3 additions & 0 deletions stacks/base/docker-compose.local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ services:
ports:
- "80:80"
- "8080:8080"
portainer:
ports:
- "9000:9000"
10 changes: 10 additions & 0 deletions tests/ci/docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
services:
traefik:
image: traefik:v3.1.6
command:
- --entrypoints.web.address=:80
- --api.insecure=true
ports:
- "80:80"
whoami:
image: traefik/whoami:v1.10.4
Loading