added app-catalog
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
|||||||
|
# Rendered output (don't commit to catalog repo)
|
||||||
|
/output/
|
||||||
|
|
||||||
|
# Editor files
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Felhom App Catalog
|
||||||
|
|
||||||
|
Central repository for all Felhom customer application deployments.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
felhom-app-catalog/
|
||||||
|
├── templates/ # Docker Compose templates ({{DOMAIN}} placeholder)
|
||||||
|
│ ├── actualbudget/
|
||||||
|
│ ├── docmost/
|
||||||
|
│ ├── filebrowser/
|
||||||
|
│ ├── homebox/
|
||||||
|
│ ├── immich/
|
||||||
|
│ ├── mealie/
|
||||||
|
│ ├── romm/
|
||||||
|
│ ├── stirling-pdf/
|
||||||
|
│ └── vaultwarden/
|
||||||
|
├── customers/ # Per-customer configuration
|
||||||
|
│ ├── demo-felhom.yaml
|
||||||
|
│ └── pi-customer-1.yaml
|
||||||
|
├── scripts/
|
||||||
|
│ └── render.sh # Renders customer repos from templates
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Templates** contain Docker Compose files with `{{DOMAIN}}` placeholders
|
||||||
|
2. **Customer configs** define which apps each customer gets + any version overrides
|
||||||
|
3. **render.sh** generates per-customer Gitea repos with domain-substituted compose files
|
||||||
|
4. **Portainer GitOps** on each customer node pulls from their repo and deploys
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Adding a new app to the catalog
|
||||||
|
1. Create `templates/<appname>/docker-compose.yml` using `{{DOMAIN}}` placeholder
|
||||||
|
2. Add to relevant customer configs in `customers/`
|
||||||
|
3. Run `./scripts/render.sh` to regenerate customer repos
|
||||||
|
|
||||||
|
### Updating an app version
|
||||||
|
1. Edit the image tag in `templates/<appname>/docker-compose.yml`
|
||||||
|
2. Run `./scripts/render.sh` — skips customers with version overrides
|
||||||
|
3. Portainer auto-detects git changes and redeploys (if polling enabled)
|
||||||
|
|
||||||
|
### Adding a new customer
|
||||||
|
1. Create `customers/<customer-id>.yaml`
|
||||||
|
2. Create the Gitea repo: `customers/<customer-id>-stacks`
|
||||||
|
3. Run `./scripts/render.sh --customer <customer-id>`
|
||||||
|
4. Set up Portainer GitOps stacks on the customer node
|
||||||
|
|
||||||
|
## Portainer Stack Setup (per app)
|
||||||
|
|
||||||
|
On the customer's Portainer:
|
||||||
|
1. **Stacks → Add Stack → Repository**
|
||||||
|
2. Repository URL: `https://gitea.felhom.eu/customers/<customer-id>-stacks`
|
||||||
|
3. Compose path: `<appname>/docker-compose.yml`
|
||||||
|
4. Add environment variables (secrets — DB passwords, API keys, etc.)
|
||||||
|
5. Enable **GitOps auto-update** (optional, 5-minute polling)
|
||||||
|
6. Deploy
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Secrets are **never stored in Git**. They live in Portainer's stack environment
|
||||||
|
variables on each customer node. Each template documents required env vars
|
||||||
|
in comments at the top of the compose file.
|
||||||
|
|
||||||
|
## Version Pinning
|
||||||
|
|
||||||
|
In a customer YAML, you can pin specific app versions:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
overrides:
|
||||||
|
immich_version: "v2.4.1" # Don't auto-update Immich for this customer
|
||||||
|
auto_update: false # Skip ALL updates for this customer
|
||||||
|
```
|
||||||
|
|
||||||
|
## App Catalog
|
||||||
|
|
||||||
|
| App | DB Type | RAM Usage | Pi-Compatible | Description |
|
||||||
|
|-----|---------|-----------|---------------|-------------|
|
||||||
|
| ActualBudget | None (file) | ~50MB | ✅ | Personal finance / budgeting |
|
||||||
|
| Docmost | PostgreSQL + Redis | ~200MB | ⚠️ (heavy) | Wiki / documentation (Notion-like) |
|
||||||
|
| FileBrowser | None (file) | ~30MB | ✅ | Web file manager |
|
||||||
|
| Homebox | None (SQLite) | ~50MB | ✅ | Home inventory management |
|
||||||
|
| Immich | PostgreSQL + Redis | ~4GB | ❌ | Photo & video management |
|
||||||
|
| Mealie | None (SQLite) | ~200MB | ✅ | Recipe manager & meal planner |
|
||||||
|
| ROMM | MariaDB + Redis | ~300MB | ⚠️ | ROM / game library manager |
|
||||||
|
| Stirling-PDF | None | ~200MB | ✅ | PDF manipulation toolkit |
|
||||||
|
| Vaultwarden | None (SQLite) | ~50MB | ✅ | Password manager (Bitwarden) |
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Customer: Demo / Test Server (N100 Mini PC)
|
||||||
|
# Hardware: Intel N100, 16GB RAM, 128GB NVMe + 1TB HDD
|
||||||
|
# Network: Local + Cloudflare Tunnel for demo access
|
||||||
|
|
||||||
|
customer_id: demo-felhom
|
||||||
|
domain: demo-felhom.eu
|
||||||
|
gitea_repo: customers/demo-felhom-stacks
|
||||||
|
hardware: n100
|
||||||
|
notes: "Internal demo/test server for validating deployments"
|
||||||
|
|
||||||
|
# Apps to deploy on this node
|
||||||
|
apps:
|
||||||
|
- actualbudget
|
||||||
|
- docmost
|
||||||
|
- filebrowser
|
||||||
|
- homebox
|
||||||
|
- immich
|
||||||
|
- mealie
|
||||||
|
- romm
|
||||||
|
- stirling-pdf
|
||||||
|
- vaultwarden
|
||||||
|
|
||||||
|
# Per-customer overrides (optional)
|
||||||
|
# Uncomment to pin versions or disable auto-updates
|
||||||
|
overrides: {}
|
||||||
|
# immich_version: "v2.5.5" # Pin Immich to specific version
|
||||||
|
# auto_update: false # Skip ALL version updates from catalog
|
||||||
|
|
||||||
|
# Portainer env vars to set (reference only — actual secrets go in Portainer!)
|
||||||
|
# These are documented here so you remember what each stack needs.
|
||||||
|
env_vars_reference:
|
||||||
|
docmost:
|
||||||
|
APP_SECRET: "generate with: openssl rand -hex 32"
|
||||||
|
DB_PASSWORD: "generate secure password"
|
||||||
|
immich:
|
||||||
|
DB_PASSWORD: "generate secure password"
|
||||||
|
romm:
|
||||||
|
DB_PASSWORD: "generate secure password"
|
||||||
|
MYSQL_ROOT_PASSWORD: "generate secure password"
|
||||||
|
ROMM_AUTH_SECRET_KEY: "generate with: openssl rand -hex 32"
|
||||||
|
vaultwarden:
|
||||||
|
ADMIN_TOKEN: "generate with: openssl rand -hex 32"
|
||||||
|
SIGNUPS_ALLOWED: "true (set to false after account creation)"
|
||||||
|
|
||||||
|
# Backup considerations
|
||||||
|
backup_notes:
|
||||||
|
databases:
|
||||||
|
- "docmost: PostgreSQL (docmost-postgres container)"
|
||||||
|
- "immich: PostgreSQL (immich-postgres container)"
|
||||||
|
- "romm: MariaDB (romm-db container)"
|
||||||
|
file_volumes:
|
||||||
|
- "actualbudget_data"
|
||||||
|
- "docmost_storage"
|
||||||
|
- "filebrowser_data"
|
||||||
|
- "homebox_data"
|
||||||
|
- "immich_upload (on HDD: /mnt/hdd_1/storage/immich)"
|
||||||
|
- "mealie_data"
|
||||||
|
- "romm_library"
|
||||||
|
- "stirling_data"
|
||||||
|
- "vaultwarden_data"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Customer: Pi Test Customer #1 (Raspberry Pi)
|
||||||
|
# Hardware: Raspberry Pi 4/5, 4-8GB RAM, SD/USB + External HDD
|
||||||
|
# Network: Local only (.local domain with self-signed cert)
|
||||||
|
|
||||||
|
customer_id: pi-customer-1
|
||||||
|
domain: pi-customer-1.local
|
||||||
|
gitea_repo: customers/pi-customer-1-stacks
|
||||||
|
hardware: rpi
|
||||||
|
notes: "Test customer on Raspberry Pi — lightweight apps only"
|
||||||
|
|
||||||
|
# Apps to deploy on this node (Pi-compatible only)
|
||||||
|
apps:
|
||||||
|
- actualbudget
|
||||||
|
- filebrowser
|
||||||
|
- mealie
|
||||||
|
- stirling-pdf
|
||||||
|
- vaultwarden
|
||||||
|
|
||||||
|
# Per-customer overrides
|
||||||
|
overrides: {}
|
||||||
|
# mealie_version: "v3.10.2" # Pin if needed
|
||||||
|
# auto_update: false
|
||||||
|
|
||||||
|
# Portainer env vars to set (reference only)
|
||||||
|
env_vars_reference:
|
||||||
|
vaultwarden:
|
||||||
|
ADMIN_TOKEN: "generate with: openssl rand -hex 32"
|
||||||
|
SIGNUPS_ALLOWED: "true (set to false after account creation)"
|
||||||
|
|
||||||
|
# Backup considerations
|
||||||
|
backup_notes:
|
||||||
|
databases: [] # No database containers — all apps use SQLite/file storage
|
||||||
|
file_volumes:
|
||||||
|
- "actualbudget_data"
|
||||||
|
- "filebrowser_data"
|
||||||
|
- "mealie_data"
|
||||||
|
- "stirling_data"
|
||||||
|
- "vaultwarden_data"
|
||||||
@@ -0,0 +1,419 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#===============================================================================
|
||||||
|
# Felhom App Catalog - Customer Repo Renderer v1.0
|
||||||
|
#
|
||||||
|
# Reads customer YAML configs and generates per-customer stack directories
|
||||||
|
# by substituting {{DOMAIN}} placeholders in Docker Compose templates.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./render.sh # Render all customers
|
||||||
|
# ./render.sh --customer demo-felhom # Render specific customer
|
||||||
|
# ./render.sh --dry-run # Show what would be done
|
||||||
|
# ./render.sh --push # Render + push to Gitea
|
||||||
|
# ./render.sh --output-dir /tmp/out # Custom output directory
|
||||||
|
# ./render.sh --debug # Verbose output
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - git (for push mode)
|
||||||
|
# - Access to Gitea (for push mode)
|
||||||
|
#
|
||||||
|
#===============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
# Configuration
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
CATALOG_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
TEMPLATES_DIR="${CATALOG_DIR}/templates"
|
||||||
|
CUSTOMERS_DIR="${CATALOG_DIR}/customers"
|
||||||
|
DEFAULT_OUTPUT_DIR="${CATALOG_DIR}/output"
|
||||||
|
|
||||||
|
# Gitea settings (for --push mode)
|
||||||
|
GITEA_URL="${GITEA_URL:-https://gitea.felhom.eu}"
|
||||||
|
GITEA_ORG="${GITEA_ORG:-customers}"
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
# Colors & Logging
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
||||||
|
log_success() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||||
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||||
|
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||||
|
log_debug() { [[ "${DEBUG:-false}" == "true" ]] && echo -e "[DEBUG] $*" || true; }
|
||||||
|
log_step() { echo -e "\n${BOLD}[STEP]${NC} $*"; }
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
# Simple YAML parser (no external dependencies)
|
||||||
|
# Handles our flat customer YAML format
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
yaml_get_value() {
|
||||||
|
# Get a simple key: value from YAML file
|
||||||
|
local file="$1" key="$2"
|
||||||
|
grep -E "^${key}:" "$file" 2>/dev/null | sed "s/^${key}:[[:space:]]*//" | sed 's/^"//' | sed 's/"$//' | sed "s/^'//" | sed "s/'$//" || echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
yaml_get_list() {
|
||||||
|
# Get a YAML list (items starting with " - ")
|
||||||
|
local file="$1" key="$2"
|
||||||
|
local in_section=false
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^${key}: ]]; then
|
||||||
|
in_section=true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if $in_section; then
|
||||||
|
if [[ "$line" =~ ^[[:space:]]*-[[:space:]]+(.*) ]]; then
|
||||||
|
echo "${BASH_REMATCH[1]}" | sed 's/^"//' | sed 's/"$//'
|
||||||
|
elif [[ "$line" =~ ^[a-zA-Z_] ]] || [[ -z "$line" && "$in_section" == true ]]; then
|
||||||
|
# New top-level key or empty line after list = end of section
|
||||||
|
# But only break on a new top-level key, not empty lines within
|
||||||
|
if [[ "$line" =~ ^[a-zA-Z_] ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
yaml_get_override() {
|
||||||
|
# Get a specific override value: overrides.key
|
||||||
|
local file="$1" key="$2"
|
||||||
|
local in_overrides=false
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^overrides: ]]; then
|
||||||
|
in_overrides=true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if $in_overrides; then
|
||||||
|
if [[ "$line" =~ ^[[:space:]]+${key}:[[:space:]]*(.*) ]]; then
|
||||||
|
local val="${BASH_REMATCH[1]}"
|
||||||
|
echo "$val" | sed 's/^"//' | sed 's/"$//' | sed "s/^'//" | sed "s/'$//"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ "$line" =~ ^[a-zA-Z_] ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < "$file"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
# Template rendering
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
render_customer() {
|
||||||
|
local customer_file="$1"
|
||||||
|
local output_base="$2"
|
||||||
|
|
||||||
|
local customer_id domain
|
||||||
|
customer_id=$(yaml_get_value "$customer_file" "customer_id")
|
||||||
|
domain=$(yaml_get_value "$customer_file" "domain")
|
||||||
|
|
||||||
|
if [[ -z "$customer_id" || -z "$domain" ]]; then
|
||||||
|
log_error "Missing customer_id or domain in: $customer_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_step "Rendering customer: ${BOLD}${customer_id}${NC} (${domain})"
|
||||||
|
|
||||||
|
# Check auto_update override
|
||||||
|
local auto_update
|
||||||
|
auto_update=$(yaml_get_override "$customer_file" "auto_update")
|
||||||
|
if [[ "$auto_update" == "false" ]]; then
|
||||||
|
log_warn "auto_update=false — skipping this customer"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get app list
|
||||||
|
local apps=()
|
||||||
|
while IFS= read -r app; do
|
||||||
|
[[ -n "$app" ]] && apps+=("$app")
|
||||||
|
done < <(yaml_get_list "$customer_file" "apps")
|
||||||
|
|
||||||
|
if [[ ${#apps[@]} -eq 0 ]]; then
|
||||||
|
log_warn "No apps configured for $customer_id"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Apps: ${apps[*]}"
|
||||||
|
log_info "Domain: $domain"
|
||||||
|
|
||||||
|
# Output directory for this customer
|
||||||
|
local customer_output="${output_base}/${customer_id}-stacks"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
echo -e " ${CYAN}[DRY-RUN]${NC} Would create: ${customer_output}/"
|
||||||
|
for app in "${apps[@]}"; do
|
||||||
|
echo -e " ${CYAN}[DRY-RUN]${NC} ${app}/docker-compose.yml ({{DOMAIN}} → ${domain})"
|
||||||
|
# Show version override if any
|
||||||
|
local version_override
|
||||||
|
version_override=$(yaml_get_override "$customer_file" "${app}_version")
|
||||||
|
if [[ -n "$version_override" ]]; then
|
||||||
|
echo -e " ${CYAN}[DRY-RUN]${NC} ↳ version pinned to: ${version_override}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create output directory
|
||||||
|
mkdir -p "$customer_output"
|
||||||
|
|
||||||
|
# Generate a README for the customer repo
|
||||||
|
cat > "${customer_output}/README.md" << EOF
|
||||||
|
# ${customer_id} - Application Stacks
|
||||||
|
|
||||||
|
**Domain:** \`${domain}\`
|
||||||
|
**Generated:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')
|
||||||
|
**Source:** felhom-app-catalog (render.sh)
|
||||||
|
|
||||||
|
## Deployed Apps
|
||||||
|
|
||||||
|
| App | Compose Path |
|
||||||
|
|-----|-------------|
|
||||||
|
$(for app in "${apps[@]}"; do echo "| ${app} | \`${app}/docker-compose.yml\` |"; done)
|
||||||
|
|
||||||
|
## Portainer Setup
|
||||||
|
|
||||||
|
For each app, create a stack in Portainer:
|
||||||
|
1. **Stacks → Add Stack → Repository**
|
||||||
|
2. Repository URL: \`${GITEA_URL}/${GITEA_ORG}/${customer_id}-stacks\`
|
||||||
|
3. Compose path: \`<appname>/docker-compose.yml\`
|
||||||
|
4. Add required environment variables (see comments in compose files)
|
||||||
|
5. Deploy
|
||||||
|
|
||||||
|
---
|
||||||
|
*Auto-generated by felhom-app-catalog. Do not edit manually.*
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Process each app
|
||||||
|
local rendered_count=0
|
||||||
|
for app in "${apps[@]}"; do
|
||||||
|
local template="${TEMPLATES_DIR}/${app}/docker-compose.yml"
|
||||||
|
|
||||||
|
if [[ ! -f "$template" ]]; then
|
||||||
|
log_error "Template not found: ${template}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${customer_output}/${app}"
|
||||||
|
|
||||||
|
# Substitute {{DOMAIN}} with customer domain
|
||||||
|
sed "s/{{DOMAIN}}/${domain}/g" "$template" > "${customer_output}/${app}/docker-compose.yml"
|
||||||
|
|
||||||
|
# Apply version override if configured
|
||||||
|
local version_override
|
||||||
|
version_override=$(yaml_get_override "$customer_file" "${app}_version")
|
||||||
|
if [[ -n "$version_override" ]]; then
|
||||||
|
log_info " ${app}: applying version override → ${version_override}"
|
||||||
|
# Find the main image line and replace the tag
|
||||||
|
# This is a simple approach — works for single-image apps and
|
||||||
|
# the first image line in multi-service apps
|
||||||
|
case "$app" in
|
||||||
|
immich)
|
||||||
|
# Immich has server + ML images that should match
|
||||||
|
sed -i "s|ghcr.io/immich-app/immich-server:[^ ]*|ghcr.io/immich-app/immich-server:${version_override}|g" \
|
||||||
|
"${customer_output}/${app}/docker-compose.yml"
|
||||||
|
sed -i "s|ghcr.io/immich-app/immich-machine-learning:[^ ]*|ghcr.io/immich-app/immich-machine-learning:${version_override}|g" \
|
||||||
|
"${customer_output}/${app}/docker-compose.yml"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Generic: replace the first image tag (main service)
|
||||||
|
# Get the first image line and replace its tag
|
||||||
|
local first_image
|
||||||
|
first_image=$(grep -m1 "image:" "${customer_output}/${app}/docker-compose.yml" | sed 's/.*image:[[:space:]]*//')
|
||||||
|
if [[ -n "$first_image" ]]; then
|
||||||
|
local image_name
|
||||||
|
image_name=$(echo "$first_image" | cut -d: -f1)
|
||||||
|
sed -i "s|${first_image}|${image_name}:${version_override}|" \
|
||||||
|
"${customer_output}/${app}/docker-compose.yml"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
rendered_count=$((rendered_count + 1))
|
||||||
|
log_debug " Rendered: ${app}/docker-compose.yml"
|
||||||
|
done
|
||||||
|
|
||||||
|
log_success "Rendered ${rendered_count}/${#apps[@]} apps → ${customer_output}/"
|
||||||
|
}
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
# Git push to Gitea
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
push_to_gitea() {
|
||||||
|
local customer_dir="$1"
|
||||||
|
local customer_id
|
||||||
|
customer_id=$(basename "$customer_dir" | sed 's/-stacks$//')
|
||||||
|
|
||||||
|
local repo_url="${GITEA_URL}/${GITEA_ORG}/${customer_id}-stacks.git"
|
||||||
|
|
||||||
|
log_step "Pushing to Gitea: ${repo_url}"
|
||||||
|
|
||||||
|
cd "$customer_dir"
|
||||||
|
|
||||||
|
if [[ ! -d ".git" ]]; then
|
||||||
|
git init -b main
|
||||||
|
git remote add origin "$repo_url"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
log_info "No changes to push for ${customer_id}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git commit -m "Auto-render: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||||
|
|
||||||
|
if git push origin main 2>/dev/null; then
|
||||||
|
log_success "Pushed to ${repo_url}"
|
||||||
|
else
|
||||||
|
# Try force push if first time or diverged
|
||||||
|
log_warn "Normal push failed, trying force push..."
|
||||||
|
git push -u origin main --force
|
||||||
|
log_success "Force-pushed to ${repo_url}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
# Argument parsing
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
DRY_RUN=false
|
||||||
|
PUSH=false
|
||||||
|
DEBUG=false
|
||||||
|
TARGET_CUSTOMER=""
|
||||||
|
OUTPUT_DIR=""
|
||||||
|
|
||||||
|
print_help() {
|
||||||
|
cat << 'EOF'
|
||||||
|
Felhom App Catalog - Customer Repo Renderer v1.0
|
||||||
|
|
||||||
|
Generates per-customer Docker Compose stacks from templates.
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
./render.sh [OPTIONS]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--customer ID Render only this customer
|
||||||
|
--output-dir DIR Output directory (default: ./output/)
|
||||||
|
--push Push rendered repos to Gitea
|
||||||
|
--dry-run Show what would be done
|
||||||
|
--debug Verbose output
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
ENVIRONMENT VARIABLES:
|
||||||
|
GITEA_URL Gitea base URL (default: https://gitea.felhom.eu)
|
||||||
|
GITEA_ORG Gitea organization (default: customers)
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
./render.sh # Render all customers
|
||||||
|
./render.sh --customer demo-felhom # Render one customer
|
||||||
|
./render.sh --dry-run # Preview changes
|
||||||
|
./render.sh --push # Render + push to Gitea
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--customer) TARGET_CUSTOMER="$2"; shift 2 ;;
|
||||||
|
--output-dir) OUTPUT_DIR="$2"; shift 2 ;;
|
||||||
|
--push) PUSH=true; shift ;;
|
||||||
|
--dry-run) DRY_RUN=true; shift ;;
|
||||||
|
--debug) DEBUG=true; shift ;;
|
||||||
|
-h|--help) print_help; exit 0 ;;
|
||||||
|
*) log_error "Unknown option: $1"; print_help; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
OUTPUT_DIR="${OUTPUT_DIR:-${DEFAULT_OUTPUT_DIR}}"
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
main() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Felhom App Catalog — Renderer${NC}"
|
||||||
|
echo -e "Templates: ${TEMPLATES_DIR}"
|
||||||
|
echo -e "Customers: ${CUSTOMERS_DIR}"
|
||||||
|
echo -e "Output: ${OUTPUT_DIR}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Validate directories exist
|
||||||
|
if [[ ! -d "$TEMPLATES_DIR" ]]; then
|
||||||
|
log_error "Templates directory not found: $TEMPLATES_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -d "$CUSTOMERS_DIR" ]]; then
|
||||||
|
log_error "Customers directory not found: $CUSTOMERS_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find customer configs to process
|
||||||
|
local customer_files=()
|
||||||
|
if [[ -n "$TARGET_CUSTOMER" ]]; then
|
||||||
|
local target_file="${CUSTOMERS_DIR}/${TARGET_CUSTOMER}.yaml"
|
||||||
|
if [[ ! -f "$target_file" ]]; then
|
||||||
|
log_error "Customer config not found: $target_file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
customer_files=("$target_file")
|
||||||
|
else
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
customer_files+=("$f")
|
||||||
|
done < <(find "$CUSTOMERS_DIR" -name "*.yaml" -print0 | sort -z)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#customer_files[@]} -eq 0 ]]; then
|
||||||
|
log_error "No customer configs found in $CUSTOMERS_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Found ${#customer_files[@]} customer(s) to render"
|
||||||
|
|
||||||
|
# Create output directory
|
||||||
|
if [[ "$DRY_RUN" != "true" ]]; then
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Render each customer
|
||||||
|
local success_count=0
|
||||||
|
for customer_file in "${customer_files[@]}"; do
|
||||||
|
if render_customer "$customer_file" "$OUTPUT_DIR"; then
|
||||||
|
success_count=$((success_count + 1))
|
||||||
|
|
||||||
|
# Push to Gitea if requested
|
||||||
|
if [[ "$PUSH" == "true" && "$DRY_RUN" != "true" ]]; then
|
||||||
|
local cid
|
||||||
|
cid=$(yaml_get_value "$customer_file" "customer_id")
|
||||||
|
push_to_gitea "${OUTPUT_DIR}/${cid}-stacks"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo -e "${BOLD}${GREEN} Rendering complete: ${success_count}/${#customer_files[@]} customers${NC}"
|
||||||
|
echo -e "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" != "true" ]]; then
|
||||||
|
echo -e "${BOLD}Output:${NC}"
|
||||||
|
find "$OUTPUT_DIR" -name "docker-compose.yml" -printf " %P\n" | sort
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$PUSH" != "true" && "$DRY_RUN" != "true" ]]; then
|
||||||
|
echo -e "${YELLOW}Tip:${NC} Use --push to automatically push to Gitea repos"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# ActualBudget - Personal Finance / Budgeting
|
||||||
|
# Domain: budget.{{DOMAIN}}
|
||||||
|
# Database: None (file-based)
|
||||||
|
# RAM: ~50MB | Pi-compatible: Yes
|
||||||
|
#
|
||||||
|
# Environment variables (set in Portainer):
|
||||||
|
# (none required — app is self-contained)
|
||||||
|
#
|
||||||
|
# First-time setup:
|
||||||
|
# Create a password on first visit, no default credentials.
|
||||||
|
|
||||||
|
services:
|
||||||
|
actualbudget:
|
||||||
|
image: actualbudget/actual-server:26.1.0
|
||||||
|
container_name: actualbudget
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Budapest
|
||||||
|
volumes:
|
||||||
|
- actualbudget_data:/data
|
||||||
|
networks:
|
||||||
|
- traefik-public
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:5006/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.actualbudget.rule=Host(`budget.{{DOMAIN}}`)"
|
||||||
|
- "traefik.http.routers.actualbudget.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.actualbudget.tls=true"
|
||||||
|
- "traefik.http.services.actualbudget.loadbalancer.server.port=5006"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
actualbudget_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-public:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Docmost - Modern Wiki / Documentation (Notion-like)
|
||||||
|
# Domain: docs.{{DOMAIN}}
|
||||||
|
# Database: PostgreSQL + Redis
|
||||||
|
# RAM: ~200MB | Pi-compatible: Heavy but possible
|
||||||
|
#
|
||||||
|
# Environment variables (set in Portainer):
|
||||||
|
# APP_SECRET - Random secret for session signing (required, generate with: openssl rand -hex 32)
|
||||||
|
# DB_PASSWORD - PostgreSQL password (required)
|
||||||
|
#
|
||||||
|
# First-time setup:
|
||||||
|
# First registered user becomes admin.
|
||||||
|
|
||||||
|
services:
|
||||||
|
docmost:
|
||||||
|
image: docmost/docmost:0.25.3
|
||||||
|
container_name: docmost
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
docmost-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
docmost-redis:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- APP_SECRET=${APP_SECRET}
|
||||||
|
- DATABASE_URL=postgresql://docmost:${DB_PASSWORD}@docmost-postgres:5432/docmost
|
||||||
|
- REDIS_URL=redis://docmost-redis:6379
|
||||||
|
- APP_URL=https://docs.{{DOMAIN}}
|
||||||
|
- STORAGE_DRIVER=local
|
||||||
|
- FILE_UPLOAD_SIZE_LIMIT=50mb
|
||||||
|
volumes:
|
||||||
|
- docmost_storage:/app/data/storage
|
||||||
|
networks:
|
||||||
|
- traefik-public
|
||||||
|
- docmost-internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.docmost.rule=Host(`docs.{{DOMAIN}}`)"
|
||||||
|
- "traefik.http.routers.docmost.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.docmost.tls=true"
|
||||||
|
- "traefik.http.services.docmost.loadbalancer.server.port=3000"
|
||||||
|
|
||||||
|
docmost-postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: docmost-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=docmost
|
||||||
|
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||||
|
- POSTGRES_DB=docmost
|
||||||
|
volumes:
|
||||||
|
- docmost_postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- docmost-internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U docmost -d docmost"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
|
docmost-redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: docmost-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
volumes:
|
||||||
|
- docmost_redis_data:/data
|
||||||
|
networks:
|
||||||
|
- docmost-internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
docmost_storage:
|
||||||
|
docmost_postgres_data:
|
||||||
|
docmost_redis_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-public:
|
||||||
|
external: true
|
||||||
|
docmost-internal:
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# FileBrowser - Simple Web File Manager
|
||||||
|
# Domain: files.{{DOMAIN}}
|
||||||
|
# Database: None (file-based)
|
||||||
|
# RAM: ~30MB | Pi-compatible: Yes
|
||||||
|
#
|
||||||
|
# Environment variables (set in Portainer):
|
||||||
|
# (none required)
|
||||||
|
#
|
||||||
|
# First-time setup:
|
||||||
|
# Default login: admin / admin — change immediately!
|
||||||
|
#
|
||||||
|
# Volume notes:
|
||||||
|
# filebrowser_data is the browsable file area.
|
||||||
|
# For HDD access, override this volume in Portainer:
|
||||||
|
# /mnt/hdd_1:/srv (or a specific subdirectory)
|
||||||
|
|
||||||
|
services:
|
||||||
|
filebrowser:
|
||||||
|
image: filebrowser/filebrowser:v2.32.0
|
||||||
|
container_name: filebrowser
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Budapest
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
volumes:
|
||||||
|
- filebrowser_data:/srv
|
||||||
|
- filebrowser_config:/database
|
||||||
|
networks:
|
||||||
|
- traefik-public
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:80/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.filebrowser.rule=Host(`files.{{DOMAIN}}`)"
|
||||||
|
- "traefik.http.routers.filebrowser.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.filebrowser.tls=true"
|
||||||
|
- "traefik.http.services.filebrowser.loadbalancer.server.port=80"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
filebrowser_data:
|
||||||
|
filebrowser_config:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-public:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Homebox - Home Inventory Management
|
||||||
|
# Domain: inventory.{{DOMAIN}}
|
||||||
|
# Database: None (SQLite, file-based)
|
||||||
|
# RAM: ~50MB | Pi-compatible: Yes
|
||||||
|
#
|
||||||
|
# Environment variables (set in Portainer):
|
||||||
|
# (none required)
|
||||||
|
#
|
||||||
|
# First-time setup:
|
||||||
|
# Register on first visit, first user becomes owner.
|
||||||
|
|
||||||
|
services:
|
||||||
|
homebox:
|
||||||
|
image: ghcr.io/sysadminsmedia/homebox:v0.16.3
|
||||||
|
container_name: homebox
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- HBOX_LOG_LEVEL=info
|
||||||
|
- HBOX_LOG_FORMAT=text
|
||||||
|
- HBOX_WEB_MAX_UPLOAD_SIZE=50
|
||||||
|
- TZ=Europe/Budapest
|
||||||
|
volumes:
|
||||||
|
- homebox_data:/data
|
||||||
|
networks:
|
||||||
|
- traefik-public
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:7745/api/v1/status"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.homebox.rule=Host(`inventory.{{DOMAIN}}`)"
|
||||||
|
- "traefik.http.routers.homebox.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.homebox.tls=true"
|
||||||
|
- "traefik.http.services.homebox.loadbalancer.server.port=7745"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
homebox_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-public:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Immich - Self-hosted Photo & Video Management
|
||||||
|
# Domain: photos.{{DOMAIN}}
|
||||||
|
# Database: PostgreSQL (with VectorChord) + Redis
|
||||||
|
# RAM: ~4GB minimum | Pi-compatible: No (ML too heavy)
|
||||||
|
#
|
||||||
|
# Environment variables (set in Portainer):
|
||||||
|
# DB_PASSWORD - PostgreSQL password (required)
|
||||||
|
#
|
||||||
|
# Volume notes:
|
||||||
|
# immich_upload is the photo/video storage location.
|
||||||
|
# For HDD storage, override in Portainer stack:
|
||||||
|
# /mnt/hdd_1/storage/immich:/usr/src/app/upload
|
||||||
|
#
|
||||||
|
# First-time setup:
|
||||||
|
# Create admin account on first visit.
|
||||||
|
|
||||||
|
services:
|
||||||
|
immich-server:
|
||||||
|
image: ghcr.io/immich-app/immich-server:v2.5.5
|
||||||
|
container_name: immich-server
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
immich-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
immich-redis:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
|
- DB_HOSTNAME=immich-postgres
|
||||||
|
- DB_USERNAME=immich
|
||||||
|
- DB_DATABASE_NAME=immich
|
||||||
|
- REDIS_HOSTNAME=immich-redis
|
||||||
|
- IMMICH_MACHINE_LEARNING_URL=http://immich-machine-learning:3003
|
||||||
|
- TZ=Europe/Budapest
|
||||||
|
volumes:
|
||||||
|
- immich_upload:/usr/src/app/upload
|
||||||
|
networks:
|
||||||
|
- traefik-public
|
||||||
|
- immich-internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:2283/api/server/ping"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.immich.rule=Host(`photos.{{DOMAIN}}`)"
|
||||||
|
- "traefik.http.routers.immich.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.immich.tls=true"
|
||||||
|
- "traefik.http.services.immich.loadbalancer.server.port=2283"
|
||||||
|
|
||||||
|
immich-machine-learning:
|
||||||
|
image: ghcr.io/immich-app/immich-machine-learning:v2.5.5
|
||||||
|
container_name: immich-machine-learning
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Budapest
|
||||||
|
- TRANSFORMERS_CACHE=/cache
|
||||||
|
volumes:
|
||||||
|
- immich_ml_cache:/cache
|
||||||
|
networks:
|
||||||
|
- immich-internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3003/ping')"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 120s
|
||||||
|
|
||||||
|
immich-postgres:
|
||||||
|
image: ghcr.io/immich-app/postgres:16-vectorchord0.3.0
|
||||||
|
container_name: immich-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=immich
|
||||||
|
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||||
|
- POSTGRES_DB=immich
|
||||||
|
- POSTGRES_INITDB_ARGS=--data-checksums
|
||||||
|
volumes:
|
||||||
|
- immich_postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- immich-internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U immich -d immich"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
immich-redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: immich-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
volumes:
|
||||||
|
- immich_redis_data:/data
|
||||||
|
networks:
|
||||||
|
- immich-internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
immich_upload:
|
||||||
|
immich_ml_cache:
|
||||||
|
immich_postgres_data:
|
||||||
|
immich_redis_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-public:
|
||||||
|
external: true
|
||||||
|
immich-internal:
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Mealie - Recipe Manager & Meal Planner
|
||||||
|
# Domain: recipes.{{DOMAIN}}
|
||||||
|
# Database: None (SQLite, built-in)
|
||||||
|
# RAM: ~200MB | Pi-compatible: Yes (arm64 only)
|
||||||
|
#
|
||||||
|
# Environment variables (set in Portainer):
|
||||||
|
# (none required for basic usage)
|
||||||
|
# SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD - for email features (optional)
|
||||||
|
#
|
||||||
|
# First-time setup:
|
||||||
|
# Default login: changeme@example.com / MyPassword
|
||||||
|
# Change immediately after first login!
|
||||||
|
|
||||||
|
services:
|
||||||
|
mealie:
|
||||||
|
image: ghcr.io/mealie-recipes/mealie:v3.10.2
|
||||||
|
container_name: mealie
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- ALLOW_SIGNUP=false
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
- TZ=Europe/Budapest
|
||||||
|
- MAX_WORKERS=1
|
||||||
|
- WEB_CONCURRENCY=1
|
||||||
|
- BASE_URL=https://recipes.{{DOMAIN}}
|
||||||
|
volumes:
|
||||||
|
- mealie_data:/app/data/
|
||||||
|
networks:
|
||||||
|
- traefik-public
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1000M
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:9000/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.mealie.rule=Host(`recipes.{{DOMAIN}}`)"
|
||||||
|
- "traefik.http.routers.mealie.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.mealie.tls=true"
|
||||||
|
- "traefik.http.services.mealie.loadbalancer.server.port=9000"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mealie_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-public:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# ROMM - ROM Manager for Game Libraries
|
||||||
|
# Domain: arcade.{{DOMAIN}}
|
||||||
|
# Database: MariaDB + Redis
|
||||||
|
# RAM: ~300MB | Pi-compatible: Possible but heavy
|
||||||
|
#
|
||||||
|
# Environment variables (set in Portainer):
|
||||||
|
# DB_PASSWORD - MariaDB user password (required)
|
||||||
|
# MYSQL_ROOT_PASSWORD - MariaDB root password (required)
|
||||||
|
# ROMM_AUTH_SECRET_KEY - Auth secret (required, generate with: openssl rand -hex 32)
|
||||||
|
# IGDB_CLIENT_ID - IGDB API client ID (optional, for game metadata)
|
||||||
|
# IGDB_CLIENT_SECRET - IGDB API client secret (optional)
|
||||||
|
# STEAMGRIDDB_API_KEY - SteamGridDB API key (optional, for cover art)
|
||||||
|
#
|
||||||
|
# First-time setup:
|
||||||
|
# Default login: admin / admin — change immediately!
|
||||||
|
|
||||||
|
services:
|
||||||
|
romm:
|
||||||
|
image: rommapp/romm:4.5.0
|
||||||
|
container_name: romm
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
romm-db:
|
||||||
|
condition: service_healthy
|
||||||
|
romm-redis:
|
||||||
|
condition: service_started
|
||||||
|
entrypoint: ["/bin/sh", "-c"]
|
||||||
|
command:
|
||||||
|
- |
|
||||||
|
if [ ! -f /romm/config/config.yml ]; then
|
||||||
|
echo "Creating default config.yml..."
|
||||||
|
cat > /romm/config/config.yml << 'CONF'
|
||||||
|
exclude:
|
||||||
|
platforms: []
|
||||||
|
roms: []
|
||||||
|
system:
|
||||||
|
log_level: INFO
|
||||||
|
CONF
|
||||||
|
fi
|
||||||
|
exec /docker-entrypoint.sh /init
|
||||||
|
environment:
|
||||||
|
- ROMM_AUTH_SECRET_KEY=${ROMM_AUTH_SECRET_KEY}
|
||||||
|
- DB_PASSWD=${DB_PASSWORD}
|
||||||
|
- DB_HOST=romm-db
|
||||||
|
- DB_PORT=3306
|
||||||
|
- DB_NAME=romm
|
||||||
|
- DB_USER=romm
|
||||||
|
- REDIS_HOST=romm-redis
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
- ROMM_PORT=8080
|
||||||
|
- IGDB_CLIENT_ID=${IGDB_CLIENT_ID:-}
|
||||||
|
- IGDB_CLIENT_SECRET=${IGDB_CLIENT_SECRET:-}
|
||||||
|
- STEAMGRIDDB_API_KEY=${STEAMGRIDDB_API_KEY:-}
|
||||||
|
- TZ=Europe/Budapest
|
||||||
|
volumes:
|
||||||
|
- romm_library:/romm/library
|
||||||
|
- romm_resources:/romm/resources
|
||||||
|
- romm_config:/romm/config
|
||||||
|
networks:
|
||||||
|
- traefik-public
|
||||||
|
- romm-internal
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.romm.rule=Host(`arcade.{{DOMAIN}}`)"
|
||||||
|
- "traefik.http.routers.romm.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.romm.tls=true"
|
||||||
|
- "traefik.http.services.romm.loadbalancer.server.port=8080"
|
||||||
|
|
||||||
|
romm-db:
|
||||||
|
image: mariadb:11.4
|
||||||
|
container_name: romm-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
|
||||||
|
- MYSQL_DATABASE=romm
|
||||||
|
- MYSQL_USER=romm
|
||||||
|
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||||
|
- TZ=Europe/Budapest
|
||||||
|
volumes:
|
||||||
|
- romm_db_data:/var/lib/mysql
|
||||||
|
networks:
|
||||||
|
- romm-internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
romm-redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: romm-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
volumes:
|
||||||
|
- romm_redis_data:/data
|
||||||
|
networks:
|
||||||
|
- romm-internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
romm_library:
|
||||||
|
romm_resources:
|
||||||
|
romm_config:
|
||||||
|
romm_db_data:
|
||||||
|
romm_redis_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-public:
|
||||||
|
external: true
|
||||||
|
romm-internal:
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Stirling-PDF - PDF Manipulation Toolkit
|
||||||
|
# Domain: pdf.{{DOMAIN}}
|
||||||
|
# Database: None
|
||||||
|
# RAM: ~200MB | Pi-compatible: Yes
|
||||||
|
#
|
||||||
|
# Environment variables (set in Portainer):
|
||||||
|
# (none required for basic usage)
|
||||||
|
# SECURITY_ENABLELOGIN=true - Enable login (optional)
|
||||||
|
# SECURITY_INITIALLOGIN_USERNAME / SECURITY_INITIALLOGIN_PASSWORD - if login enabled
|
||||||
|
#
|
||||||
|
# First-time setup:
|
||||||
|
# No login by default — accessible immediately.
|
||||||
|
# Enable login via env vars if exposing externally.
|
||||||
|
|
||||||
|
services:
|
||||||
|
stirling-pdf:
|
||||||
|
image: stirlingtools/stirling-pdf:0.45.1
|
||||||
|
container_name: stirling-pdf
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Budapest
|
||||||
|
- DOCKER_ENABLE_SECURITY=false
|
||||||
|
- INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
|
||||||
|
- LANGS=en_GB
|
||||||
|
volumes:
|
||||||
|
- stirling_data:/configs
|
||||||
|
- stirling_training:/usr/share/tessdata
|
||||||
|
networks:
|
||||||
|
- traefik-public
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/api/v1/info/status"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.stirling-pdf.rule=Host(`pdf.{{DOMAIN}}`)"
|
||||||
|
- "traefik.http.routers.stirling-pdf.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.stirling-pdf.tls=true"
|
||||||
|
- "traefik.http.services.stirling-pdf.loadbalancer.server.port=8080"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
stirling_data:
|
||||||
|
stirling_training:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-public:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Vaultwarden - Password Manager (Bitwarden-compatible)
|
||||||
|
# Domain: vault.{{DOMAIN}}
|
||||||
|
# Database: None (SQLite, built-in)
|
||||||
|
# RAM: ~50MB | Pi-compatible: Yes
|
||||||
|
#
|
||||||
|
# Environment variables (set in Portainer):
|
||||||
|
# ADMIN_TOKEN - Admin panel token (optional but recommended, generate with: openssl rand -hex 32)
|
||||||
|
# SIGNUPS_ALLOWED - Set to "false" after creating your account(s)
|
||||||
|
#
|
||||||
|
# First-time setup:
|
||||||
|
# 1. Visit https://vault.{{DOMAIN}} and create an account
|
||||||
|
# 2. Set SIGNUPS_ALLOWED=false in Portainer env vars
|
||||||
|
# 3. Redeploy stack
|
||||||
|
# 4. Admin panel at https://vault.{{DOMAIN}}/admin (if ADMIN_TOKEN set)
|
||||||
|
#
|
||||||
|
# Clients:
|
||||||
|
# Use any Bitwarden client (desktop, mobile, browser extension)
|
||||||
|
# Set server URL to: https://vault.{{DOMAIN}}
|
||||||
|
|
||||||
|
services:
|
||||||
|
vaultwarden:
|
||||||
|
image: vaultwarden/server:1.33.2-alpine
|
||||||
|
container_name: vaultwarden
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- DOMAIN=https://vault.{{DOMAIN}}
|
||||||
|
- SIGNUPS_ALLOWED=${SIGNUPS_ALLOWED:-true}
|
||||||
|
- ADMIN_TOKEN=${ADMIN_TOKEN:-}
|
||||||
|
- WEBSOCKET_ENABLED=true
|
||||||
|
- TZ=Europe/Budapest
|
||||||
|
volumes:
|
||||||
|
- vaultwarden_data:/data
|
||||||
|
networks:
|
||||||
|
- traefik-public
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:80/alive"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.vaultwarden.rule=Host(`vault.{{DOMAIN}}`)"
|
||||||
|
- "traefik.http.routers.vaultwarden.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.vaultwarden.tls=true"
|
||||||
|
- "traefik.http.services.vaultwarden.loadbalancer.server.port=80"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
vaultwarden_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-public:
|
||||||
|
external: true
|
||||||
Reference in New Issue
Block a user