ok... ok... I'll add it to git...
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
# Docker Compose Stacks
|
||||
|
||||
This directory contains version-controlled Docker Compose files for all services in the tower-of-joy infrastructure.
|
||||
|
||||
## Deployment
|
||||
|
||||
### Via Portainer API (Recommended for Automation)
|
||||
|
||||
Use the `update-stack.sh` script to programmatically update stacks via Portainer's REST API:
|
||||
|
||||
```bash
|
||||
# Update a stack from YAML file
|
||||
cd stacks/
|
||||
./update-stack.sh open-webui.yml
|
||||
```
|
||||
|
||||
**First-time setup:**
|
||||
- Script will prompt for Portainer admin credentials
|
||||
- Generates and stores API token in `.portainer-token` (gitignored)
|
||||
- Subsequent runs use stored token automatically
|
||||
|
||||
**Non-interactive mode (for scripts/automation):**
|
||||
```bash
|
||||
export PORTAINER_USERNAME="admin"
|
||||
export PORTAINER_PASSWORD="your-password"
|
||||
./update-stack.sh open-webui.yml
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Safe for LLM agents and automation
|
||||
- No need for remote Portainer UI access
|
||||
- Version-controlled YAML files as source of truth
|
||||
- Automatic authentication and token management
|
||||
|
||||
### Via Portainer UI
|
||||
1. Navigate to **Stacks** → **Add Stack**
|
||||
2. Choose **Upload** and select the `.yml` file
|
||||
3. Review configuration and adjust environment variables
|
||||
4. Click **Deploy the stack**
|
||||
|
||||
### Via Docker CLI
|
||||
```bash
|
||||
# Deploy a stack
|
||||
docker compose -f stacks/<stack-name>.yml up -d
|
||||
|
||||
# Stop a stack
|
||||
docker compose -f stacks/<stack-name>.yml down
|
||||
|
||||
# Update a stack
|
||||
docker compose -f stacks/<stack-name>.yml pull
|
||||
docker compose -f stacks/<stack-name>.yml up -d
|
||||
```
|
||||
|
||||
## Stack Inventory
|
||||
|
||||
### Phase 1: Foundation
|
||||
|
||||
| Stack | File | Ports | GPU | Description |
|
||||
|-------|------|-------|-----|-------------|
|
||||
| **Portainer** | `portainer.yml` | 8080, 8443 | No | Container management UI |
|
||||
| **Nginx Proxy Manager** | `nginx-proxy-manager.yml` | 8000, 80, 443 | No | Reverse proxy and unified web interface |
|
||||
| **Ollama** | `ollama.yml` | 11434 | **Yes** | ML model serving with GPU acceleration |
|
||||
|
||||
### Phase 2: Networking
|
||||
|
||||
| Stack | File | Ports | GPU | Description |
|
||||
|-------|------|-------|-----|-------------|
|
||||
| **Headscale** | `headscale.yml` | 8085, 9090 | No | Self-hosted Tailscale control server |
|
||||
|
||||
### Phase 3: Monitoring
|
||||
|
||||
| Stack | File | Ports | GPU | Description |
|
||||
|-------|------|-------|-----|-------------|
|
||||
| **Uptime Kuma** | `uptime-kuma.yml` | 3001 | No | Service availability monitoring |
|
||||
| **Netdata** | `netdata.yml` | 19999 | No | Real-time system performance monitoring |
|
||||
| **Heimdall** | `heimdall.yml` | 8888, 8889 | No | Application dashboard |
|
||||
|
||||
### Phase 4: Optimization
|
||||
|
||||
| Stack | File | Ports | GPU | Description |
|
||||
|-------|------|-------|-----|-------------|
|
||||
| **Watchtower** | `watchtower.yml` | - | No | Automatic container updates |
|
||||
| **Duplicati** | `duplicati.yml` | 8200 | No | Backup solution |
|
||||
|
||||
### Backlog: Applications
|
||||
|
||||
| Stack | File | Ports | GPU | Description |
|
||||
|-------|------|-------|-----|-------------|
|
||||
| **Jellyfin** | `jellyfin.yml` | 8096, 8920, 7359, 1900 | **Yes** | Media server with GPU transcoding |
|
||||
| **Nextcloud** | `nextcloud.yml` | 8082 | No | Cloud storage (includes DB and Redis) |
|
||||
| **Gitea** | `gitea.yml` | 3002, 2222 | No | Git repository hosting (includes PostgreSQL) |
|
||||
| **Samba** | `samba.yml` | 139, 445 | No | Network file sharing |
|
||||
|
||||
## Port Allocation
|
||||
|
||||
### Infrastructure Services (8000-8099)
|
||||
- 8000: Nginx Proxy Manager (unified web interface)
|
||||
- 8080: Portainer
|
||||
- 8081: AMP (game servers - existing)
|
||||
- 8082: Nextcloud
|
||||
- 8085: Headscale
|
||||
- 8096: Jellyfin
|
||||
|
||||
### Git & Development Services
|
||||
- 2222: Gitea SSH
|
||||
- 3002: Gitea HTTP
|
||||
|
||||
### Monitoring Services (3000-3999, 19000-19999)
|
||||
- 3001: Uptime Kuma
|
||||
- 8200: Duplicati
|
||||
- 8888: Heimdall
|
||||
- 19999: Netdata
|
||||
|
||||
### ML/API Services (11000+)
|
||||
- 11434: Ollama
|
||||
|
||||
### Network Services
|
||||
- 80: HTTP (NPM reverse proxy)
|
||||
- 443: HTTPS (NPM reverse proxy)
|
||||
- 139, 445: Samba/SMB
|
||||
- 9090: Headscale metrics
|
||||
|
||||
## Storage Convention
|
||||
|
||||
All stacks follow the dual-disk strategy:
|
||||
|
||||
**SSD (Performance):**
|
||||
- Configs: `/home/jpmschweitzer/docker-data/<service>/config`
|
||||
- Cache: `/home/jpmschweitzer/docker-data/<service>/cache`
|
||||
- Databases: `/home/jpmschweitzer/docker-data/<service>/db`
|
||||
|
||||
**HDD (Capacity):**
|
||||
- User content: `/mnt/media/<service>/data`
|
||||
- Media files: `/mnt/media/<service>/media`
|
||||
- Backups: `/mnt/media/backups/<service>`
|
||||
|
||||
## GPU Services
|
||||
|
||||
Stacks requiring GPU access (marked with **Yes** above):
|
||||
- `ollama.yml` - ML model inference
|
||||
- `jellyfin.yml` - Hardware transcoding
|
||||
|
||||
**Prerequisites:**
|
||||
- NVIDIA Container Toolkit installed
|
||||
- GPU verified: `docker run --rm --gpus all nvidia/cuda:11.4.0-base-ubuntu20.04 nvidia-smi`
|
||||
|
||||
## Before Deploying
|
||||
|
||||
1. **Review environment variables** - Change default passwords!
|
||||
2. **Create directories** - Ensure volume paths exist
|
||||
3. **Check ports** - Verify no conflicts with existing services
|
||||
4. **GPU services** - Confirm NVIDIA toolkit installed
|
||||
5. **Update STATUS.md** - Mark stack as deployed when complete
|
||||
|
||||
## After Deploying
|
||||
|
||||
1. **Test service** - Access web UI or API endpoint
|
||||
2. **Check logs** - `docker logs <container-name>`
|
||||
3. **Verify GPU** - `docker exec <container> nvidia-smi` (if applicable)
|
||||
4. **Update documentation** - Add to STATUS.md and CHANGELOG.md
|
||||
5. **Configure backup** - Add to Duplicati backup job
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Update a Stack
|
||||
```bash
|
||||
# Pull latest images
|
||||
docker compose -f stacks/<stack-name>.yml pull
|
||||
|
||||
# Recreate containers with new images
|
||||
docker compose -f stacks/<stack-name>.yml up -d
|
||||
|
||||
# Or let Watchtower handle it automatically
|
||||
```
|
||||
|
||||
### Backup Stack Configuration
|
||||
```bash
|
||||
# Stacks are version-controlled in this directory
|
||||
# Backup container data separately (see scripts/backup.sh)
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
- Container won't start: `docker logs <container-name>`
|
||||
- Port conflicts: `sudo netstat -tulpn | grep <port>`
|
||||
- Permission issues: Check volume path ownership
|
||||
- GPU not detected: Verify NVIDIA toolkit and restart Docker
|
||||
|
||||
---
|
||||
|
||||
*For detailed implementation instructions, see containers/implementation-plan.md*
|
||||
@@ -0,0 +1,103 @@
|
||||
version: '3.8'
|
||||
|
||||
# Core API - OpenAPI-compatible functions and AI orchestration for Open WebUI
|
||||
# Purpose: Provides OpenAI-compatible API (/v1/chat/completions) and tool functions (web scraping)
|
||||
# Port: 8083 (HTTP API)
|
||||
# Network: ai-dataplane (shared with Open WebUI, Ollama, Qdrant)
|
||||
#
|
||||
# Setup: Create venv before first deployment:
|
||||
# cd /home/jpmschweitzer/Projects/portainer-core/services/core-api
|
||||
# python3 -m venv .venv
|
||||
# source .venv/bin/activate
|
||||
# pip install -r requirements.txt
|
||||
|
||||
services:
|
||||
core-api:
|
||||
image: python:3.12
|
||||
container_name: core-api
|
||||
restart: unless-stopped
|
||||
|
||||
# Hot-reload development mode
|
||||
command: >
|
||||
sh -c "
|
||||
if [ ! -f /venv/bin/activate ]; then
|
||||
echo 'Creating venv and installing dependencies...' &&
|
||||
python3 -m venv /venv &&
|
||||
/venv/bin/pip install --upgrade pip &&
|
||||
/venv/bin/pip install -r /app/requirements.txt;
|
||||
fi &&
|
||||
/venv/bin/uvicorn src.main:app
|
||||
--host 0.0.0.0
|
||||
--port 8083
|
||||
--reload
|
||||
--reload-dir /app/src
|
||||
"
|
||||
|
||||
ports:
|
||||
- "8083:8083"
|
||||
|
||||
environment:
|
||||
# Application
|
||||
- APP_NAME=Core API
|
||||
- APP_VERSION=1.0.0-phase1
|
||||
- DEBUG=true
|
||||
|
||||
# Server
|
||||
- HOST=0.0.0.0
|
||||
- PORT=8083
|
||||
|
||||
# Logging
|
||||
- LOG_LEVEL=INFO
|
||||
|
||||
# Ollama Configuration (AI Orchestration)
|
||||
- OLLAMA_BASE_URL=http://ollama:11434
|
||||
- OLLAMA_TIMEOUT=300
|
||||
|
||||
# Model Configuration
|
||||
- DEFAULT_MODEL=gemma:7b
|
||||
- LIGHTWEIGHT_MODELS=gemma:2b,gemma:7b
|
||||
- HEAVY_MODELS=mistral:7b,gemma2:9b,mixtral:8x7b
|
||||
- CODE_MODELS=codestral:latest,codegemma:latest
|
||||
|
||||
# Model Aliases (OpenAI → Local)
|
||||
- ALIAS_GPT35=gemma:7b
|
||||
- ALIAS_GPT4=mistral:7b
|
||||
- ALIAS_GPT4_TURBO=mixtral:8x7b
|
||||
- ALIAS_GPT4_CODE=codestral:latest
|
||||
|
||||
# Web Scraper settings
|
||||
- WEB_SCRAPER_REQUEST_TIMEOUT=30
|
||||
- WEB_SCRAPER_MAX_REDIRECTS=5
|
||||
- WEB_SCRAPER_USER_AGENT=Mozilla/5.0 (compatible; CoreAPI/1.0)
|
||||
- WEB_SCRAPER_DEFAULT_MAX_LENGTH=10000
|
||||
- WEB_SCRAPER_MAX_LINKS_TO_EXTRACT=50
|
||||
|
||||
# Python path
|
||||
- PYTHONPATH=/app
|
||||
|
||||
volumes:
|
||||
# Mount source code for live editing (not .venv - that's container-specific)
|
||||
- /home/jpmschweitzer/Projects/portainer-core/services/core-api:/app
|
||||
|
||||
# Persist container's venv for fast restarts
|
||||
- /home/jpmschweitzer/docker-data/core-api/venv:/venv
|
||||
|
||||
# Persist logs
|
||||
- /home/jpmschweitzer/docker-data/core-api/logs:/app/logs
|
||||
|
||||
networks:
|
||||
- ai-dataplane
|
||||
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8083/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
networks:
|
||||
ai-dataplane:
|
||||
external: true
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
#############################################################################
|
||||
# Portainer Stack Creator
|
||||
#
|
||||
# Creates new Portainer stacks via REST API using local YAML files.
|
||||
#
|
||||
# Usage:
|
||||
# ./create-stack.sh <stack-name.yml>
|
||||
#
|
||||
# Example:
|
||||
# ./create-stack.sh gitea.yml
|
||||
#############################################################################
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="$SCRIPT_DIR/.env"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Load credentials
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
log_error ".env file not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source "$ENV_FILE"
|
||||
|
||||
PORTAINER_URL="${PORTAINER_URL:-http://localhost:8001}"
|
||||
|
||||
# Validate arguments
|
||||
if [ $# -ne 1 ]; then
|
||||
log_error "Usage: $0 <stack-file.yml>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
YAML_FILE="$1"
|
||||
STACK_NAME=$(basename "$YAML_FILE" .yml)
|
||||
|
||||
if [ ! -f "$YAML_FILE" ]; then
|
||||
log_error "File not found: $YAML_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Creating stack: $STACK_NAME"
|
||||
|
||||
# Authenticate
|
||||
log_info "Authenticating with Portainer..."
|
||||
TOKEN=$(curl -s -X POST "$PORTAINER_URL/api/auth" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"username\":\"$PORTAINER_USERNAME\",\"password\":\"$PORTAINER_PASSWORD\"}" \
|
||||
| python3 -c "import sys, json; print(json.load(sys.stdin)['jwt'])" 2>/dev/null)
|
||||
|
||||
if [ -z "$TOKEN" ]; then
|
||||
log_error "Authentication failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "Authenticated"
|
||||
|
||||
# Get endpoint ID
|
||||
ENDPOINT_ID=$(curl -s -X GET "$PORTAINER_URL/api/endpoints" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
| python3 -c "import sys, json; print(json.load(sys.stdin)[0]['Id'])" 2>/dev/null)
|
||||
|
||||
if [ -z "$ENDPOINT_ID" ]; then
|
||||
log_error "Failed to get endpoint ID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Endpoint ID: $ENDPOINT_ID"
|
||||
|
||||
# Read YAML content
|
||||
YAML_CONTENT=$(cat "$YAML_FILE")
|
||||
|
||||
# Create stack
|
||||
log_info "Creating stack in Portainer..."
|
||||
|
||||
RESPONSE=$(curl -s -X POST "$PORTAINER_URL/api/stacks/create/standalone/string?endpointId=$ENDPOINT_ID" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- <<EOF
|
||||
{
|
||||
"name": "$STACK_NAME",
|
||||
"stackFileContent": $(echo "$YAML_CONTENT" | python3 -c "import sys, json; print(json.dumps(sys.stdin.read()))")
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# Check for errors
|
||||
if echo "$RESPONSE" | grep -q '"message"' && echo "$RESPONSE" | grep -q '"err"'; then
|
||||
log_error "Failed to create stack: $RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "✓ Stack '$STACK_NAME' created successfully!"
|
||||
echo "$RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(f\"Stack ID: {data.get('Id', 'N/A')}\")" 2>/dev/null || true
|
||||
@@ -0,0 +1,91 @@
|
||||
version: '3.8'
|
||||
|
||||
# Gitea - Self-Hosted Git Service with Database
|
||||
# Application Layer
|
||||
# Ports: 3002 (HTTP), 2222 (SSH)
|
||||
# GPU: No
|
||||
# Storage: SSD (repositories, database)
|
||||
|
||||
services:
|
||||
gitea-db:
|
||||
image: postgres:14-alpine
|
||||
container_name: gitea-db
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# Database on SSD for performance
|
||||
- /home/jpmschweitzer/docker-data/gitea/db:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_USER=gitea
|
||||
- POSTGRES_PASSWORD=gPdM7QV4gvotE9f9lGS4yj
|
||||
- POSTGRES_DB=gitea
|
||||
- TZ=Europe/Amsterdam
|
||||
networks:
|
||||
- gitea-network
|
||||
|
||||
gitea:
|
||||
image: gitea/gitea:latest
|
||||
container_name: gitea
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3002:3000" # HTTP web interface
|
||||
- "2222:22" # SSH git access (mapped to avoid host SSH conflict)
|
||||
volumes:
|
||||
# Git repositories and app config on SSD
|
||||
- /home/jpmschweitzer/docker-data/gitea/data:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
- GITEA__database__DB_TYPE=postgres
|
||||
- GITEA__database__HOST=gitea-db:5432
|
||||
- GITEA__database__NAME=gitea
|
||||
- GITEA__database__USER=gitea
|
||||
- GITEA__database__PASSWD=gPdM7QV4gvotE9f9lGS4yj
|
||||
- TZ=Europe/Amsterdam
|
||||
depends_on:
|
||||
- gitea-db
|
||||
networks:
|
||||
- gitea-network
|
||||
|
||||
networks:
|
||||
gitea-network:
|
||||
driver: bridge
|
||||
|
||||
# ⚠️ SECURITY WARNING:
|
||||
# Change POSTGRES_PASSWORD and GITEA__database__PASSWD before deploying!
|
||||
# Use a strong, unique password.
|
||||
#
|
||||
# After Deployment:
|
||||
# 1. Access http://localhost:3002
|
||||
# 2. First-time setup:
|
||||
# - Server domain: localhost:3002 or your domain
|
||||
# - SSH server domain: localhost
|
||||
# - SSH server port: 2222 (external)
|
||||
# - HTTP listen port: 3000 (internal)
|
||||
# - Application URL: http://localhost:3002 or https://git.schweitz.net (if using NPM)
|
||||
# - Database: PostgreSQL (pre-configured via environment variables)
|
||||
# - Create admin account (strong password!)
|
||||
# 3. Configure additional settings:
|
||||
# - Email settings (optional)
|
||||
# - Enable/disable user registration
|
||||
# - Configure webhooks and integrations
|
||||
#
|
||||
# SSH Git Clone Usage:
|
||||
# git clone ssh://git@localhost:2222/username/repo.git
|
||||
#
|
||||
# Nginx Proxy Manager Setup (for external access):
|
||||
# 1. Add proxy host: git.schweitz.net → http://gitea:3000
|
||||
# 2. Enable SSL with Let's Encrypt
|
||||
# 3. Update GITEA__server__ROOT_URL in environment to https://git.schweitz.net
|
||||
#
|
||||
# Features:
|
||||
# - Git repository hosting
|
||||
# - Organizations and teams
|
||||
# - Issue tracking
|
||||
# - Pull requests and code review
|
||||
# - Wiki and project documentation
|
||||
# - CI/CD integration (Gitea Actions)
|
||||
# - Webhooks for automation
|
||||
# - Migration from GitHub/GitLab
|
||||
# - Lightweight and fast
|
||||
@@ -0,0 +1,56 @@
|
||||
version: '3.8'
|
||||
|
||||
# Headscale - Self-Hosted Tailscale Control Server
|
||||
# Phase 2: Networking & External Access
|
||||
# Ports: 8085 (Web/API), 9090 (Metrics)
|
||||
# GPU: No
|
||||
# Storage: SSD (config and database)
|
||||
|
||||
services:
|
||||
headscale:
|
||||
image: headscale/headscale:latest
|
||||
container_name: headscale
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8085:8080" # Web/API port
|
||||
- "9090:9090" # Metrics port (optional)
|
||||
volumes:
|
||||
- /home/jpmschweitzer/docker-data/headscale/config:/etc/headscale
|
||||
- /home/jpmschweitzer/docker-data/headscale/data:/var/lib/headscale
|
||||
command: serve
|
||||
environment:
|
||||
- TZ=Europe/Amsterdam
|
||||
networks:
|
||||
- headscale-network
|
||||
|
||||
networks:
|
||||
headscale-network:
|
||||
driver: bridge
|
||||
|
||||
# Setup Instructions:
|
||||
# 1. Create directories:
|
||||
# mkdir -p ~/docker-data/headscale/{config,data}
|
||||
#
|
||||
# 2. Generate config:
|
||||
# docker exec headscale headscale config generate > ~/docker-data/headscale/config/config.yaml
|
||||
#
|
||||
# 3. Edit config (important settings):
|
||||
# - server_url: http://tower-of-joy:8085 (or your IP)
|
||||
# - db_type: sqlite3
|
||||
# - db_path: /var/lib/headscale/db.sqlite
|
||||
#
|
||||
# 4. Restart container: docker restart headscale
|
||||
#
|
||||
# 5. Create user: docker exec headscale headscale users create homelab
|
||||
#
|
||||
# 6. Generate pre-auth key:
|
||||
# docker exec headscale headscale preauthkeys create --user homelab --expiration 24h
|
||||
#
|
||||
# 7. Connect devices:
|
||||
# - Install Tailscale client on devices
|
||||
# - Run: tailscale up --login-server=http://tower-of-joy:8085 --authkey=<key>
|
||||
#
|
||||
# Verify:
|
||||
# - Health check: curl http://localhost:8085/health
|
||||
# - List users: docker exec headscale headscale users list
|
||||
# - List nodes: docker exec headscale headscale nodes list
|
||||
@@ -0,0 +1,43 @@
|
||||
version: '3.8'
|
||||
|
||||
# Heimdall - Application Dashboard
|
||||
# Phase 3: Monitoring & Management
|
||||
# Ports: 8888 (HTTP), 8889 (HTTPS)
|
||||
# GPU: No
|
||||
# Storage: SSD (configuration)
|
||||
|
||||
services:
|
||||
heimdall:
|
||||
image: linuxserver/heimdall:latest
|
||||
container_name: heimdall
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8888:80"
|
||||
- "8889:443"
|
||||
volumes:
|
||||
- /home/jpmschweitzer/docker-data/heimdall:/config
|
||||
environment:
|
||||
- PUID=1000 # Your user ID (run: id -u)
|
||||
- PGID=1000 # Your group ID (run: id -g)
|
||||
- TZ=Europe/Amsterdam
|
||||
|
||||
# After Deployment:
|
||||
# 1. Access http://localhost:8888
|
||||
# 2. Add application tiles for quick access:
|
||||
# - Portainer: http://tower-of-joy:8080
|
||||
# - NPM: http://tower-of-joy:8000
|
||||
# - Jellyfin: http://tower-of-joy:8096
|
||||
# - Nextcloud: http://tower-of-joy:8082
|
||||
# - AMP: http://tower-of-joy:8081
|
||||
# - Uptime Kuma: http://tower-of-joy:3001
|
||||
# - Netdata: http://tower-of-joy:19999
|
||||
# - Ollama: http://tower-of-joy:11434
|
||||
# 3. Customize colors and icons for each service
|
||||
# 4. Set as browser homepage for easy access
|
||||
#
|
||||
# Features:
|
||||
# - Unified dashboard for all services
|
||||
# - One-click access to any service
|
||||
# - Custom backgrounds and themes
|
||||
# - Search functionality
|
||||
# - Mobile-friendly
|
||||
@@ -0,0 +1,69 @@
|
||||
version: '3.8'
|
||||
|
||||
# Jellyfin - Media Server with GPU Transcoding
|
||||
# Backlog: Application Deployment
|
||||
# Ports: 8096 (HTTP), 8920 (HTTPS), 7359 (Auto-discovery), 1900 (DLNA)
|
||||
# GPU: YES - Requires NVIDIA Container Toolkit
|
||||
# Storage: SSD (config/cache), HDD (media files)
|
||||
|
||||
services:
|
||||
jellyfin:
|
||||
image: jellyfin/jellyfin:latest
|
||||
container_name: jellyfin
|
||||
user: 1000:1000 # Replace with your UID:GID (run: id)
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# Config and cache on SSD (performance-critical)
|
||||
- /home/jpmschweitzer/docker-data/jellyfin/config:/config
|
||||
- /home/jpmschweitzer/docker-data/jellyfin/cache:/cache
|
||||
|
||||
# Media files on HDD (read-only for safety)
|
||||
- /mnt/media/jellyfin/movies:/media/movies:ro
|
||||
- /mnt/media/jellyfin/series:/media/series:ro
|
||||
environment:
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=all
|
||||
- TZ=Europe/Amsterdam
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu, video, compute, utility]
|
||||
|
||||
# GPU Transcoding Setup:
|
||||
# 1. Deploy stack
|
||||
# 2. Verify GPU access: docker exec jellyfin nvidia-smi
|
||||
# 3. Access http://localhost:8096
|
||||
# 4. Complete initial setup wizard
|
||||
# 5. Navigate to: Dashboard → Playback → Transcoding
|
||||
# 6. Configure hardware acceleration:
|
||||
# - Hardware acceleration: NVIDIA NVENC
|
||||
# - Enable hardware decoding: Check all applicable formats
|
||||
# - Enable hardware encoding: Enabled
|
||||
# - Encoding preset: Auto or High Quality
|
||||
# 7. Test with video playback
|
||||
# 8. Monitor GPU: watch -n 1 nvidia-smi
|
||||
#
|
||||
# Expected Results:
|
||||
# - Dashboard shows "(hw)" during transcoding
|
||||
# - nvidia-smi shows Video Engine usage
|
||||
# - CPU usage remains low during transcoding
|
||||
# - RTX 2080 Ti can handle multiple 4K transcodes simultaneously
|
||||
#
|
||||
# Media Organization:
|
||||
# /mnt/media/jellyfin/
|
||||
# ├── movies/
|
||||
# │ ├── Movie Title (Year)/
|
||||
# │ │ └── Movie Title (Year).mkv
|
||||
# ├── tv/
|
||||
# │ ├── TV Show Name/
|
||||
# │ │ ├── Season 01/
|
||||
# │ │ │ ├── S01E01.mkv
|
||||
# │ │ │ └── S01E02.mkv
|
||||
# └── music/
|
||||
# ├── Artist/
|
||||
# │ ├── Album/
|
||||
# │ │ └── Track.mp3
|
||||
@@ -0,0 +1,62 @@
|
||||
version: '3.8'
|
||||
|
||||
# Maintenance Runner - Scheduled Tasks & Scripts
|
||||
# Purpose: Centralized container for running maintenance tasks (backups, cleanup, health checks, etc.)
|
||||
# Ports: None (background service)
|
||||
# GPU: No
|
||||
# Storage: Read access to Docker data, write to backup location
|
||||
|
||||
services:
|
||||
maintenance:
|
||||
image: alpine:latest
|
||||
container_name: maintenance
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# Docker configs to backup (read-only)
|
||||
- /home/jpmschweitzer/docker-data:/data/docker-data:ro
|
||||
|
||||
# Host-based service configs to backup (read-only)
|
||||
- /home/jpmschweitzer/.config/code-server:/data/code-server-config:ro
|
||||
|
||||
# Backup destination (read-write)
|
||||
- /mnt/media/backups:/backups
|
||||
|
||||
# Maintenance scripts
|
||||
- /home/jpmschweitzer/docker-data/maintenance/scripts:/scripts:ro
|
||||
|
||||
# Crontab configuration
|
||||
- /home/jpmschweitzer/docker-data/maintenance/crontab:/etc/crontabs/root:ro
|
||||
|
||||
# Logs
|
||||
- /home/jpmschweitzer/docker-data/maintenance/logs:/var/log/maintenance
|
||||
|
||||
# Docker socket for container management (read-only)
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- TZ=Europe/Amsterdam
|
||||
command: >
|
||||
sh -c "
|
||||
apk add --no-cache docker-cli &&
|
||||
crond -f -l 2
|
||||
"
|
||||
# crond flags:
|
||||
# -f: foreground (don't daemonize)
|
||||
# -l 2: log level 2 (errors and info)
|
||||
|
||||
# Usage:
|
||||
# 1. Create maintenance scripts in ~/docker-data/maintenance/scripts/
|
||||
# 2. Define schedule in ~/docker-data/maintenance/crontab
|
||||
# 3. Container will execute scripts per schedule
|
||||
#
|
||||
# Adding new maintenance tasks:
|
||||
# 1. Create new script in scripts/ directory
|
||||
# 2. Make it executable: chmod +x script-name.sh
|
||||
# 3. Add to crontab with desired schedule
|
||||
# 4. Restart container: docker restart maintenance
|
||||
#
|
||||
# View logs:
|
||||
# - Cron logs: docker logs maintenance
|
||||
# - Task logs: ~/docker-data/maintenance/logs/
|
||||
#
|
||||
# Manual task execution:
|
||||
# docker exec maintenance /scripts/backup-configs.sh
|
||||
@@ -0,0 +1,48 @@
|
||||
version: '3.8'
|
||||
|
||||
# Netdata - Real-Time System Performance Monitoring
|
||||
# Phase 3: Monitoring & Management
|
||||
# Ports: 19999
|
||||
# GPU: No
|
||||
# Storage: Minimal (reads from host /proc and /sys)
|
||||
|
||||
services:
|
||||
netdata:
|
||||
image: netdata/netdata:latest
|
||||
container_name: netdata
|
||||
restart: unless-stopped
|
||||
hostname: tower-of-joy
|
||||
ports:
|
||||
- "19999:19999"
|
||||
cap_add:
|
||||
- SYS_PTRACE # Required for process monitoring
|
||||
security_opt:
|
||||
- apparmor:unconfined
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- TZ=Europe/Amsterdam
|
||||
# Optional: Claim to Netdata Cloud for remote access
|
||||
# - NETDATA_CLAIM_TOKEN=your-claim-token
|
||||
# - NETDATA_CLAIM_URL=https://app.netdata.cloud
|
||||
|
||||
# After Deployment:
|
||||
# 1. Access http://localhost:19999
|
||||
# 2. Explore dashboard sections:
|
||||
# - CPU usage
|
||||
# - RAM usage
|
||||
# - Disk I/O
|
||||
# - Network traffic
|
||||
# - Docker containers
|
||||
# - GPU monitoring (if configured)
|
||||
# 3. Set up alerts (optional)
|
||||
# 4. Configure alarm notifications
|
||||
#
|
||||
# Features:
|
||||
# - Real-time graphs (1s granularity)
|
||||
# - Per-container metrics
|
||||
# - System health monitoring
|
||||
# - Historical data
|
||||
# - Low resource overhead
|
||||
@@ -0,0 +1,99 @@
|
||||
version: '3.8'
|
||||
|
||||
# Nextcloud - Cloud Storage with Database and Redis
|
||||
# Backlog: Application Deployment
|
||||
# Ports: 8082
|
||||
# GPU: No
|
||||
# Storage: SSD (config/database), HDD (user data)
|
||||
|
||||
services:
|
||||
nextcloud-db:
|
||||
image: mariadb:10.11
|
||||
container_name: nextcloud-db
|
||||
command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# Database on SSD for performance
|
||||
- /home/jpmschweitzer/docker-data/nextcloud/db:/var/lib/mysql
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=xDgobrmzXOl+GgvBdXC9+z5v0OrWb29t
|
||||
- MYSQL_PASSWORD=maF91Sw9is6Zb57JVxU/gPGP8O/DsxFq
|
||||
- MYSQL_DATABASE=nextcloud
|
||||
- MYSQL_USER=nextcloud
|
||||
- TZ=Europe/Amsterdam
|
||||
networks:
|
||||
- nextcloud-network
|
||||
|
||||
nextcloud-redis:
|
||||
image: redis:alpine
|
||||
container_name: nextcloud-redis
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Europe/Amsterdam
|
||||
networks:
|
||||
- nextcloud-network
|
||||
|
||||
nextcloud:
|
||||
image: nextcloud:stable
|
||||
container_name: nextcloud
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8082:80"
|
||||
volumes:
|
||||
# App config on SSD
|
||||
- /home/jpmschweitzer/docker-data/nextcloud/config:/var/www/html
|
||||
|
||||
# User data on HDD (large files)
|
||||
- /mnt/media/nextcloud/data:/var/www/html/data
|
||||
environment:
|
||||
- MYSQL_HOST=nextcloud-db
|
||||
- MYSQL_PASSWORD=maF91Sw9is6Zb57JVxU/gPGP8O/DsxFq
|
||||
- MYSQL_DATABASE=nextcloud
|
||||
- MYSQL_USER=nextcloud
|
||||
- REDIS_HOST=nextcloud-redis
|
||||
- TZ=Europe/Amsterdam
|
||||
depends_on:
|
||||
- nextcloud-db
|
||||
- nextcloud-redis
|
||||
networks:
|
||||
- nextcloud-network
|
||||
|
||||
networks:
|
||||
nextcloud-network:
|
||||
driver: bridge
|
||||
|
||||
# ⚠️ SECURITY WARNING:
|
||||
# Change MYSQL_ROOT_PASSWORD and MYSQL_PASSWORD before deploying!
|
||||
# Use strong, unique passwords.
|
||||
#
|
||||
# After Deployment:
|
||||
# 1. Access http://localhost:8082
|
||||
# 2. First-time setup:
|
||||
# - Create admin account (strong password!)
|
||||
# - Data folder: /var/www/html/data (default)
|
||||
# - Database: MySQL/MariaDB
|
||||
# - Database user: nextcloud
|
||||
# - Database password: (the one you set above)
|
||||
# - Database name: nextcloud
|
||||
# - Database host: nextcloud-db
|
||||
# 3. Wait for installation (may take a few minutes)
|
||||
# 4. Configure trusted domains:
|
||||
# docker exec -u www-data nextcloud php occ config:system:set trusted_domains 1 --value=tower-of-joy
|
||||
# docker exec -u www-data nextcloud php occ config:system:set trusted_domains 2 --value=192.168.x.x
|
||||
#
|
||||
# Optimization (recommended):
|
||||
# docker exec -u www-data nextcloud php occ db:add-missing-indices
|
||||
# docker exec -u www-data nextcloud php occ db:convert-filecache-bigint
|
||||
# docker exec -u www-data nextcloud php occ background:cron
|
||||
#
|
||||
# Add cron job for background tasks:
|
||||
# echo "*/5 * * * * docker exec -u www-data nextcloud php cron.php" | sudo tee -a /etc/crontab
|
||||
#
|
||||
# Features:
|
||||
# - File sync and share
|
||||
# - Calendar and contacts
|
||||
# - Collaborative editing
|
||||
# - Photo gallery
|
||||
# - Mobile apps (iOS/Android)
|
||||
# - Desktop sync client
|
||||
# - External storage support
|
||||
@@ -0,0 +1,36 @@
|
||||
version: '3.8'
|
||||
|
||||
# Nginx Proxy Manager - Reverse Proxy & Unified Web Interface
|
||||
# Phase 1: Foundation Setup
|
||||
# Ports: 8000 (Admin UI), 80 (HTTP), 443 (HTTPS)
|
||||
# GPU: No
|
||||
# Storage: SSD (configs and SSL certificates)
|
||||
|
||||
services:
|
||||
nginx-proxy-manager:
|
||||
image: jc21/nginx-proxy-manager:latest
|
||||
container_name: nginx-proxy-manager
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8000:81" # Admin web interface (unified entry point)
|
||||
- "80:80" # HTTP reverse proxy traffic
|
||||
- "443:443" # HTTPS reverse proxy traffic
|
||||
volumes:
|
||||
# SSD storage for configs and certificates (performance-critical)
|
||||
- /home/jpmschweitzer/docker-data/nginx-proxy-manager/data:/data
|
||||
- /home/jpmschweitzer/docker-data/nginx-proxy-manager/letsencrypt:/etc/letsencrypt
|
||||
environment:
|
||||
- DB_SQLITE_FILE=/data/database.sqlite
|
||||
- TZ=Europe/Amsterdam
|
||||
|
||||
# Setup Instructions:
|
||||
# 1. Deploy this stack
|
||||
# 2. Access http://localhost:8000
|
||||
# 3. Default login: admin@example.com / changeme
|
||||
# 4. IMPORTANT: Change admin credentials immediately!
|
||||
# 5. Add proxy hosts for your services (Portainer, Jellyfin, etc.)
|
||||
#
|
||||
# Example Proxy Host Configuration:
|
||||
# - Domain: portainer.tower-of-joy.local
|
||||
# - Forward to: portainer:9000
|
||||
# - Enable SSL with Let's Encrypt (optional)
|
||||
@@ -0,0 +1,56 @@
|
||||
version: '3.8'
|
||||
|
||||
# Ollama - GPU-Accelerated ML Model Serving
|
||||
# Phase 1: Foundation Setup
|
||||
# Ports: 11434 (API)
|
||||
# GPU: YES - Requires NVIDIA Container Toolkit
|
||||
# Storage: SSD or HDD for models (models are 2-15GB each)
|
||||
|
||||
services:
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
container_name: ollama
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "11434:11434" # Ollama API endpoint
|
||||
volumes:
|
||||
# Model storage - choose based on available space:
|
||||
# SSD (faster load times): /home/jpmschweitzer/docker-data/ollama/models
|
||||
# HDD (more space): /mnt/media/ollama/models
|
||||
- /home/jpmschweitzer/docker-data/ollama/models:/root/.ollama
|
||||
environment:
|
||||
- TZ=Europe/Amsterdam
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=all
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
|
||||
# GPU Requirements:
|
||||
# - RTX 2080 Ti (11GB VRAM)
|
||||
# - Suitable for 3B-13B parameter models
|
||||
# - NVIDIA Container Toolkit must be installed
|
||||
#
|
||||
# After Deployment:
|
||||
# 1. Verify GPU access: docker exec ollama nvidia-smi
|
||||
# 2. Pull a model: docker exec ollama ollama pull llama3.2:3b
|
||||
# 3. List models: docker exec ollama ollama list
|
||||
# 4. Test inference: docker exec ollama ollama run llama3.2:3b "Hello"
|
||||
# 5. Monitor GPU during inference: watch -n 1 nvidia-smi
|
||||
#
|
||||
# Recommended Models for RTX 2080 Ti (11GB VRAM):
|
||||
# - llama3.2:3b (2GB) - Fast, general purpose
|
||||
# - mistral:7b (4GB) - High quality, coding
|
||||
# - codellama:7b (4GB) - Code-specialized
|
||||
# - phi3:mini (2GB) - Fast reasoning
|
||||
#
|
||||
# API Usage:
|
||||
# curl http://localhost:11434/api/generate -d '{
|
||||
# "model": "llama3.2:3b",
|
||||
# "prompt": "Why is the sky blue?",
|
||||
# "stream": false
|
||||
# }'
|
||||
@@ -0,0 +1,58 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
open-webui:
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
container_name: open-webui
|
||||
restart: unless-stopped
|
||||
|
||||
ports:
|
||||
- "82:8080"
|
||||
|
||||
environment:
|
||||
# Ollama connection (direct - fallback)
|
||||
- OLLAMA_BASE_URL=http://192.168.86.149:11434
|
||||
|
||||
# Tatlock AI Orchestrator (OpenAI-compatible endpoint)
|
||||
- OPENAI_API_BASE_URLS=http://core-api:8083/v1
|
||||
- OPENAI_API_KEYS=dummy
|
||||
|
||||
# Default model
|
||||
- DEFAULT_MODELS=gemma3:12b
|
||||
|
||||
# Enable features
|
||||
- ENABLE_RAG_WEB_SEARCH=true
|
||||
- ENABLE_OLLAMA_API=true
|
||||
- WEBUI_AUTH=true
|
||||
|
||||
# Web search configuration
|
||||
- RAG_WEB_SEARCH_ENGINE=duckduckgo
|
||||
|
||||
# RAG & Vector Database - Qdrant for conversation memory
|
||||
- VECTOR_DB=qdrant
|
||||
- QDRANT_URI=http://qdrant:6333
|
||||
- RAG_EMBEDDING_ENGINE=ollama
|
||||
- RAG_EMBEDDING_MODEL=nomic-embed-text
|
||||
- RAG_EMBEDDING_MODEL_AUTO_UPDATE=true
|
||||
|
||||
# Enable native memory feature
|
||||
- ENABLE_MEMORY=true
|
||||
- MEMORY_COLLECTION_NAME=open-webui_memories
|
||||
|
||||
# Session settings
|
||||
- WEBUI_SESSION_COOKIE_SAME_SITE=lax
|
||||
- WEBUI_SESSION_COOKIE_SECURE=false
|
||||
|
||||
volumes:
|
||||
- /home/jpmschweitzer/docker-data/open-webui:/app/backend/data
|
||||
|
||||
networks:
|
||||
- ai-dataplane
|
||||
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
|
||||
networks:
|
||||
ai-dataplane:
|
||||
driver: bridge
|
||||
name: ai-dataplane
|
||||
@@ -0,0 +1,62 @@
|
||||
version: '3.8'
|
||||
|
||||
# Organizr - Unified Dashboard with Tabbed Interface
|
||||
# "One page to rule them all" - Tabs for all services in single interface
|
||||
# Phase 3.5: Unified Access Layer
|
||||
# Ports: 9999 (Web UI)
|
||||
# GPU: No
|
||||
# Storage: SSD (configs and user data)
|
||||
|
||||
services:
|
||||
organizr:
|
||||
image: organizr/organizr:latest
|
||||
container_name: organizr
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9999:80"
|
||||
volumes:
|
||||
# SSD storage for configs and user data
|
||||
- /home/jpmschweitzer/docker-data/organizr:/config
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Amsterdam
|
||||
- fpm=true # Enable PHP-FPM for better performance
|
||||
|
||||
# Setup Instructions:
|
||||
# 1. Ensure tower-of-joy is connected to Headscale mesh (get mesh IP)
|
||||
# 2. Deploy this stack: make deploy-organizr
|
||||
# 3. Access via mesh IP: http://10.99.0.1:9999 (or local: http://192.168.86.149:9999)
|
||||
# 4. Complete setup wizard
|
||||
# 5. Add tabs using MESH IPs for VPN access:
|
||||
# - Portainer: http://10.99.0.1:8001
|
||||
# - Uptime Kuma: http://10.99.0.1:3001
|
||||
# - Netdata: http://10.99.0.1:19999
|
||||
# - Heimdall: http://10.99.0.1:8888
|
||||
# - Jellyfin: http://10.99.0.1:8096 (when deployed)
|
||||
# - Nextcloud: http://10.99.0.1:8082 (when deployed)
|
||||
#
|
||||
# Access Pattern (Option B - Hybrid):
|
||||
# VPN Access (Primary - Admin Tools):
|
||||
# - Connect to Headscale VPN
|
||||
# - Access Organizr: http://10.99.0.1:9999
|
||||
# - All tabs use mesh IPs (10.99.0.x)
|
||||
# - Secure, no public exposure
|
||||
#
|
||||
# Public Access (Media/Files Only):
|
||||
# - https://home.schweitz.net → Organizr (optional public)
|
||||
# - https://media.schweitz.net → Jellyfin
|
||||
# - https://cloud.schweitz.net → Nextcloud
|
||||
# - Admin tools NOT accessible without VPN
|
||||
#
|
||||
# Mesh IP Benefits:
|
||||
# - Works from anywhere (VPN connected)
|
||||
# - No complex proxy rewrites needed
|
||||
# - Secure by default
|
||||
# - Easy device addition
|
||||
#
|
||||
# Important Notes:
|
||||
# - Use mesh IPs (10.99.0.x) instead of local IPs (192.168.86.x)
|
||||
# - Some services may block iframe embedding (X-Frame-Options header)
|
||||
# - Organizr authentication secures public access
|
||||
# - See docs/mesh-access-strategy.md for complete guide
|
||||
@@ -0,0 +1,25 @@
|
||||
version: '3.8'
|
||||
|
||||
# Portainer - Container Management UI
|
||||
# Phase 1: Foundation Setup
|
||||
# Ports: 8080 (HTTP), 8443 (HTTPS)
|
||||
# GPU: No
|
||||
# Storage: Docker volume (portainer_data)
|
||||
|
||||
services:
|
||||
portainer:
|
||||
image: portainer/portainer-ce:latest
|
||||
container_name: portainer
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:9000" # Main Portainer web UI
|
||||
- "8443:9443" # Portainer HTTPS access
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock # Docker socket for container management
|
||||
- portainer_data:/data # Persistent data storage
|
||||
environment:
|
||||
- TZ=Europe/Amsterdam
|
||||
|
||||
volumes:
|
||||
portainer_data:
|
||||
name: portainer_data
|
||||
@@ -0,0 +1,55 @@
|
||||
version: '3.8'
|
||||
|
||||
# Qdrant Vector Database
|
||||
# Purpose: Efficient vector storage for Open WebUI RAG (conversation memory & documents)
|
||||
# Ports: 6333 (HTTP API), 6334 (gRPC)
|
||||
# GPU: NO - CPU-based vector operations are efficient
|
||||
# Storage: SSD for vector data (performance-critical)
|
||||
|
||||
services:
|
||||
qdrant:
|
||||
image: qdrant/qdrant:latest
|
||||
container_name: qdrant
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6333:6333" # HTTP API
|
||||
- "6334:6334" # gRPC API
|
||||
volumes:
|
||||
# Vector storage on SSD for performance
|
||||
- /home/jpmschweitzer/docker-data/qdrant/storage:/qdrant/storage
|
||||
# Snapshots for backups
|
||||
- /home/jpmschweitzer/docker-data/qdrant/snapshots:/qdrant/snapshots
|
||||
environment:
|
||||
- TZ=Europe/Amsterdam
|
||||
networks:
|
||||
- ai-dataplane
|
||||
|
||||
networks:
|
||||
ai-dataplane:
|
||||
external: true
|
||||
|
||||
# Qdrant Performance Notes:
|
||||
# - Optimized for high-dimensional vectors (embeddings)
|
||||
# - Supports HNSW indexing for fast similarity search
|
||||
# - Efficient memory usage (~1-2GB for thousands of documents)
|
||||
# - No GPU required (CPU operations are fast enough)
|
||||
#
|
||||
# Storage Estimates:
|
||||
# - ~1KB per conversation turn (with embedding)
|
||||
# - 10,000 turns = ~10MB
|
||||
# - Very efficient for conversation memory
|
||||
#
|
||||
# After Deployment:
|
||||
# 1. Check logs: docker logs qdrant
|
||||
# 2. Access UI: http://localhost:6333/dashboard
|
||||
# 3. Verify API: curl http://localhost:6333/collections
|
||||
#
|
||||
# Integration with Open WebUI:
|
||||
# - Set VECTOR_DB=qdrant in Open WebUI
|
||||
# - Set QDRANT_URL=http://qdrant:6333
|
||||
# - Open WebUI will automatically create collections
|
||||
#
|
||||
# Collections Created:
|
||||
# - Documents: User-uploaded files for RAG
|
||||
# - Conversations: Chat history for memory
|
||||
# - Web search results: Cached search results
|
||||
@@ -0,0 +1,73 @@
|
||||
version: '3.8'
|
||||
|
||||
# Samba - Network File Sharing (SMB/CIFS)
|
||||
# Backlog: Application Deployment
|
||||
# Ports: 139, 445
|
||||
# GPU: No
|
||||
# Storage: HDD (shares from media drive)
|
||||
|
||||
services:
|
||||
samba:
|
||||
image: dperson/samba
|
||||
container_name: samba
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "139:139"
|
||||
- "445:445"
|
||||
environment:
|
||||
- TZ=Europe/Amsterdam
|
||||
- USERID=1000 # Your user ID (run: id -u)
|
||||
- GROUPID=1000 # Your group ID (run: id -g)
|
||||
volumes:
|
||||
# Config on SSD
|
||||
- /home/jpmschweitzer/docker-data/samba:/share/config
|
||||
|
||||
# Shares from HDD
|
||||
- /mnt/media/jellyfin:/share/media # Media files (read/write)
|
||||
- /mnt/media/downloads:/share/downloads # Downloads folder
|
||||
- /mnt/media/backups:/share/backups:ro # Backups (read-only)
|
||||
command: >
|
||||
-s "Media;/share/media;yes;no;yes;all"
|
||||
-s "Downloads;/share/downloads;yes;no;no;all"
|
||||
-s "Backups;/share/backups;yes;no;yes;all"
|
||||
-u "jpmschweitzer;IG3omTybtVW3pVmmBi1D5FjnQ0MnZLUG"
|
||||
-p
|
||||
|
||||
# ⚠️ SECURITY WARNING:
|
||||
# Change CHANGEME_SAMBA_PASSWORD before deploying!
|
||||
#
|
||||
# Share Configuration Format:
|
||||
# -s "ShareName;/path;browseable;readonly;guest;users"
|
||||
#
|
||||
# Current Shares:
|
||||
# 1. Media - Read/write access to Jellyfin media
|
||||
# 2. Downloads - Read/write downloads folder (guest: no)
|
||||
# 3. Backups - Read-only access to backups
|
||||
#
|
||||
# Note: Nextcloud files accessible via web interface at https://cloud.schweitz.net
|
||||
#
|
||||
# Access from Clients:
|
||||
#
|
||||
# Windows:
|
||||
# 1. Open File Explorer
|
||||
# 2. Address bar: \\tower-of-joy\Media
|
||||
# 3. Enter credentials: jpmschweitzer / (your password)
|
||||
#
|
||||
# Mac:
|
||||
# 1. Finder → Go → Connect to Server
|
||||
# 2. Enter: smb://tower-of-joy/Media
|
||||
# 3. Enter credentials
|
||||
#
|
||||
# Linux:
|
||||
# 1. Install smbclient: sudo apt install smbclient
|
||||
# 2. List shares: smbclient -L tower-of-joy -U jpmschweitzer
|
||||
# 3. Connect: smbclient //tower-of-joy/Media -U jpmschweitzer
|
||||
# Or mount: sudo mount -t cifs //tower-of-joy/Media /mnt/media -o username=jpmschweitzer
|
||||
#
|
||||
# Mobile (iOS/Android):
|
||||
# Use file manager apps that support SMB (e.g., FE File Explorer, Solid Explorer)
|
||||
#
|
||||
# Firewall Configuration:
|
||||
# If using UFW, allow Samba:
|
||||
# sudo ufw allow 139/tcp
|
||||
# sudo ufw allow 445/tcp
|
||||
Executable
+252
@@ -0,0 +1,252 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
#############################################################################
|
||||
# Portainer Stack Updater (Automation-Only)
|
||||
#
|
||||
# Updates Portainer stacks via REST API using local YAML files.
|
||||
# Credentials stored in .env file (gitignored).
|
||||
#
|
||||
# Usage:
|
||||
# ./update-stack.sh <stack-name.yml>
|
||||
#
|
||||
# Example:
|
||||
# ./update-stack.sh open-webui.yml
|
||||
#############################################################################
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TOKEN_FILE="$SCRIPT_DIR/.portainer-token"
|
||||
ENV_FILE="$SCRIPT_DIR/.env"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Load credentials from .env
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
log_error ".env file not found at $ENV_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source "$ENV_FILE"
|
||||
|
||||
if [ -z "${PORTAINER_USERNAME:-}" ] || [ -z "${PORTAINER_PASSWORD:-}" ]; then
|
||||
log_error "PORTAINER_USERNAME and PORTAINER_PASSWORD must be set in .env file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PORTAINER_URL="${PORTAINER_URL:-http://localhost:8080}"
|
||||
|
||||
#############################################################################
|
||||
# Authentication
|
||||
#############################################################################
|
||||
|
||||
authenticate() {
|
||||
log_info "Authenticating with Portainer..."
|
||||
|
||||
local response
|
||||
response=$(curl -s -X POST "$PORTAINER_URL/api/auth" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"username\":\"$PORTAINER_USERNAME\",\"password\":\"$PORTAINER_PASSWORD\"}" \
|
||||
2>&1)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "Failed to connect to Portainer at $PORTAINER_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract token
|
||||
local token
|
||||
token=$(echo "$response" | python3 -c "import sys, json; print(json.load(sys.stdin)['jwt'])" 2>/dev/null)
|
||||
|
||||
if [ -z "$token" ]; then
|
||||
log_error "Failed to authenticate. Check credentials in .env file."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Save token
|
||||
echo "$token" > "$TOKEN_FILE"
|
||||
chmod 600 "$TOKEN_FILE"
|
||||
|
||||
log_success "Authentication successful"
|
||||
echo "$token"
|
||||
}
|
||||
|
||||
get_token() {
|
||||
if [ -f "$TOKEN_FILE" ]; then
|
||||
cat "$TOKEN_FILE"
|
||||
else
|
||||
authenticate
|
||||
fi
|
||||
}
|
||||
|
||||
#############################################################################
|
||||
# Stack Operations
|
||||
#############################################################################
|
||||
|
||||
get_endpoint_id() {
|
||||
local token="$1"
|
||||
|
||||
local response
|
||||
response=$(curl -s -X GET "$PORTAINER_URL/api/endpoints" \
|
||||
-H "Authorization: Bearer $token" 2>&1)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "Failed to get endpoints"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get first endpoint (local docker)
|
||||
local endpoint_id
|
||||
endpoint_id=$(echo "$response" | python3 -c "import sys, json; print(json.load(sys.stdin)[0]['Id'])" 2>/dev/null)
|
||||
|
||||
if [ -z "$endpoint_id" ]; then
|
||||
log_error "No endpoints found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$endpoint_id"
|
||||
}
|
||||
|
||||
get_stack_id() {
|
||||
local token="$1"
|
||||
local stack_name="$2"
|
||||
|
||||
local response
|
||||
response=$(curl -s -X GET "$PORTAINER_URL/api/stacks" \
|
||||
-H "Authorization: Bearer $token" 2>&1)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "Failed to get stacks"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Find stack by name
|
||||
local stack_id
|
||||
stack_id=$(echo "$response" | python3 -c "
|
||||
import sys, json
|
||||
stacks = json.load(sys.stdin)
|
||||
for stack in stacks:
|
||||
if stack['Name'] == '$stack_name':
|
||||
print(stack['Id'])
|
||||
break
|
||||
" 2>/dev/null)
|
||||
|
||||
echo "$stack_id"
|
||||
}
|
||||
|
||||
update_stack() {
|
||||
local token="$1"
|
||||
local stack_id="$2"
|
||||
local endpoint_id="$3"
|
||||
local yaml_content="$4"
|
||||
|
||||
log_info "Updating stack ID $stack_id..."
|
||||
|
||||
# Create JSON payload
|
||||
local payload
|
||||
payload=$(python3 -c "
|
||||
import json, sys
|
||||
data = {
|
||||
'stackFileContent': '''$yaml_content''',
|
||||
'prune': False,
|
||||
'pullImage': False
|
||||
}
|
||||
print(json.dumps(data))
|
||||
")
|
||||
|
||||
local response
|
||||
response=$(curl -s -X PUT "$PORTAINER_URL/api/stacks/$stack_id?endpointId=$endpoint_id" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" 2>&1)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "Failed to update stack"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if response contains error
|
||||
if echo "$response" | grep -q '"message"'; then
|
||||
log_error "Stack update failed: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_success "Stack updated successfully"
|
||||
return 0
|
||||
}
|
||||
|
||||
#############################################################################
|
||||
# Main
|
||||
#############################################################################
|
||||
|
||||
main() {
|
||||
# Validate arguments
|
||||
if [ $# -ne 1 ]; then
|
||||
log_error "Usage: $0 <stack-file.yml>"
|
||||
log_error "Example: $0 open-webui.yml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local yaml_file="$1"
|
||||
local yaml_path
|
||||
|
||||
# Support both relative and absolute paths
|
||||
if [ -f "$yaml_file" ]; then
|
||||
yaml_path="$yaml_file"
|
||||
elif [ -f "$SCRIPT_DIR/$yaml_file" ]; then
|
||||
yaml_path="$SCRIPT_DIR/$yaml_file"
|
||||
else
|
||||
log_error "Stack file not found: $yaml_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract stack name from filename (remove .yml extension)
|
||||
local stack_name
|
||||
stack_name=$(basename "$yaml_file" .yml)
|
||||
|
||||
log_info "Updating stack: $stack_name"
|
||||
|
||||
# Read YAML content
|
||||
local yaml_content
|
||||
yaml_content=$(cat "$yaml_path")
|
||||
|
||||
# Get or create API token
|
||||
local token
|
||||
token=$(get_token)
|
||||
|
||||
# Get endpoint ID (local Docker)
|
||||
local endpoint_id
|
||||
endpoint_id=$(get_endpoint_id "$token")
|
||||
|
||||
if [ -z "$endpoint_id" ]; then
|
||||
log_error "Failed to get endpoint ID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get stack ID
|
||||
local stack_id
|
||||
stack_id=$(get_stack_id "$token" "$stack_name")
|
||||
|
||||
if [ -z "$stack_id" ]; then
|
||||
log_error "Stack '$stack_name' not found in Portainer"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Update stack
|
||||
if update_stack "$token" "$stack_id" "$endpoint_id" "$yaml_content"; then
|
||||
log_success "✓ Stack '$stack_name' updated successfully"
|
||||
exit 0
|
||||
else
|
||||
log_error "Failed to update stack"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,52 @@
|
||||
version: '3.8'
|
||||
|
||||
# Uptime Kuma - Service Availability Monitoring
|
||||
# Phase 3: Monitoring & Management
|
||||
# Ports: 3001
|
||||
# GPU: No
|
||||
# Storage: SSD (monitoring data)
|
||||
|
||||
services:
|
||||
uptime-kuma:
|
||||
image: louislam/uptime-kuma:latest
|
||||
container_name: uptime-kuma
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3001:3001"
|
||||
volumes:
|
||||
- /home/jpmschweitzer/docker-data/uptime-kuma:/app/data
|
||||
environment:
|
||||
- TZ=Europe/Amsterdam
|
||||
networks:
|
||||
- default
|
||||
- ai-dataplane
|
||||
- nextcloud-network
|
||||
- headscale-network
|
||||
- samba-network
|
||||
|
||||
networks:
|
||||
ai-dataplane:
|
||||
external: true
|
||||
name: ai-dataplane
|
||||
nextcloud-network:
|
||||
external: true
|
||||
name: nextcloud_nextcloud-network
|
||||
headscale-network:
|
||||
external: true
|
||||
name: stacks_headscale-network
|
||||
samba-network:
|
||||
external: true
|
||||
name: samba_default
|
||||
|
||||
# After Deployment:
|
||||
# 1. Access http://localhost:3001
|
||||
# 2. Create admin account on first visit
|
||||
# 3. Add monitors for services:
|
||||
# - Jellyfin: http://localhost:8096
|
||||
# - Nextcloud: http://localhost:8082
|
||||
# - Portainer: http://localhost:8080
|
||||
# - NPM: http://localhost:8000
|
||||
# - Headscale: http://localhost:8085/health
|
||||
# - Ollama: http://localhost:11434/api/tags
|
||||
# 4. Set check intervals (60 seconds recommended)
|
||||
# 5. Configure notifications (optional: email, Discord, Slack)
|
||||
@@ -0,0 +1,43 @@
|
||||
version: '3.8'
|
||||
|
||||
# Watchtower - Automatic Container Updates
|
||||
# Phase 4: Optimization & Security
|
||||
# Ports: None (runs as background service)
|
||||
# GPU: No
|
||||
# Storage: None (reads Docker socket)
|
||||
|
||||
services:
|
||||
watchtower:
|
||||
image: containrrr/watchtower:latest
|
||||
container_name: watchtower
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- WATCHTOWER_CLEANUP=true # Remove old images after update
|
||||
- WATCHTOWER_SCHEDULE=0 0 4 * * * # Run at 4 AM daily (cron format)
|
||||
- TZ=Europe/Amsterdam
|
||||
# Optional: Enable notifications
|
||||
# - WATCHTOWER_NOTIFICATIONS=shoutrrr
|
||||
# - WATCHTOWER_NOTIFICATION_URL= # Add notification URL (Discord, Slack, etc.)
|
||||
|
||||
# Optional: Monitor only specific containers
|
||||
# - WATCHTOWER_LABEL_ENABLE=true # Only update containers with label com.centurylinklabs.watchtower.enable=true
|
||||
|
||||
# Schedule Format (cron):
|
||||
# - 0 0 4 * * * = Daily at 4 AM
|
||||
# - 0 0 4 * * SUN = Weekly on Sunday at 4 AM
|
||||
# - 0 0 */6 * * * = Every 6 hours
|
||||
#
|
||||
# Manual Trigger:
|
||||
# docker exec watchtower watchtower --run-once
|
||||
#
|
||||
# Exclude Specific Containers:
|
||||
# Add label to container: com.centurylinklabs.watchtower.enable=false
|
||||
#
|
||||
# Monitor Watchtower Activity:
|
||||
# docker logs watchtower
|
||||
#
|
||||
# Security Note:
|
||||
# Watchtower has full Docker socket access. Review updates in logs.
|
||||
# Consider excluding critical services and updating them manually.
|
||||
Reference in New Issue
Block a user