The GrampsWeb service worker was serving index.html for ALL navigation requests (including Django app routes), hijacking the entire domain. Patched sw.js at startup to: - Use subpath-prefixed index.html in createHandlerBoundToURL - Update denylist regex to match subpath API routes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
281 lines
10 KiB
YAML
281 lines
10 KiB
YAML
# Production Docker Compose Configuration
|
||
# This file is used for production deployment via GitHub Actions
|
||
# For local development, use: docker-compose -f compose.dev.yml up
|
||
#
|
||
# IMPORTANT: This configuration requires ALL environment variables to be
|
||
# provided via the production server's .env file. No fallback values are
|
||
# included for security reasons.
|
||
|
||
services:
|
||
db:
|
||
image: postgres:16-alpine
|
||
environment:
|
||
POSTGRES_DB: ${POSTGRES_DB}
|
||
POSTGRES_USER: ${POSTGRES_USER}
|
||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||
volumes:
|
||
- dbdata:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
redis:
|
||
image: redis:7-alpine
|
||
|
||
web:
|
||
build:
|
||
context: ./app
|
||
args:
|
||
APP_VERSION: ${APP_VERSION:-unknown}
|
||
depends_on:
|
||
db:
|
||
condition: service_healthy
|
||
redis:
|
||
condition: service_started
|
||
environment:
|
||
- POSTGRES_DB=${POSTGRES_DB}
|
||
- POSTGRES_USER=${POSTGRES_USER}
|
||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||
- DB_HOST=${DB_HOST}
|
||
- DB_PORT=${DB_PORT}
|
||
- DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
|
||
- DJANGO_DEBUG=${DJANGO_DEBUG}
|
||
- DJANGO_ALLOWED_HOSTS=${DJANGO_ALLOWED_HOSTS}
|
||
- LANGUAGE_CODE=${LANGUAGE_CODE}
|
||
- TIME_ZONE=${TIME_ZONE}
|
||
- REDIS_URL=${REDIS_URL}
|
||
- SESSION_COOKIE_NAME=${SESSION_COOKIE_NAME}
|
||
- CSRF_COOKIE_NAME=${CSRF_COOKIE_NAME}
|
||
- GRAMPS_URL=${GRAMPS_URL}
|
||
- GRAMPS_USERNAME=${GRAMPS_USERNAME}
|
||
- GRAMPS_PASSWORD=${GRAMPS_PASSWORD}
|
||
- GRAMPS_API_TOKEN=${GRAMPS_API_TOKEN}
|
||
- IMAP_HOST=${IMAP_HOST}
|
||
- IMAP_PORT=${IMAP_PORT}
|
||
- IMAP_USER=${IMAP_USER}
|
||
- IMAP_PASSWORD=${IMAP_PASSWORD}
|
||
- IMAP_FOLDER=${IMAP_FOLDER}
|
||
- IMAP_USE_SSL=${IMAP_USE_SSL}
|
||
ports:
|
||
- "8081:8000"
|
||
volumes:
|
||
- ./app:/app
|
||
command: ["gunicorn", "core.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]
|
||
|
||
worker:
|
||
build:
|
||
context: ./app
|
||
args:
|
||
APP_VERSION: ${APP_VERSION:-unknown}
|
||
environment:
|
||
- POSTGRES_DB=${POSTGRES_DB}
|
||
- POSTGRES_USER=${POSTGRES_USER}
|
||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||
- DB_HOST=${DB_HOST}
|
||
- DB_PORT=${DB_PORT}
|
||
- DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
|
||
- DJANGO_DEBUG=${DJANGO_DEBUG}
|
||
- REDIS_URL=${REDIS_URL}
|
||
- GRAMPS_URL=${GRAMPS_URL}
|
||
- GRAMPS_USERNAME=${GRAMPS_USERNAME}
|
||
- GRAMPS_PASSWORD=${GRAMPS_PASSWORD}
|
||
- GRAMPS_API_TOKEN=${GRAMPS_API_TOKEN}
|
||
- IMAP_HOST=${IMAP_HOST}
|
||
- IMAP_PORT=${IMAP_PORT}
|
||
- IMAP_USER=${IMAP_USER}
|
||
- IMAP_PASSWORD=${IMAP_PASSWORD}
|
||
- IMAP_FOLDER=${IMAP_FOLDER}
|
||
- IMAP_USE_SSL=${IMAP_USE_SSL}
|
||
depends_on:
|
||
- redis
|
||
- db
|
||
command: ["celery", "-A", "core", "worker", "-l", "info"]
|
||
|
||
beat:
|
||
build:
|
||
context: ./app
|
||
args:
|
||
APP_VERSION: ${APP_VERSION:-unknown}
|
||
environment:
|
||
- POSTGRES_DB=${POSTGRES_DB}
|
||
- POSTGRES_USER=${POSTGRES_USER}
|
||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||
- DB_HOST=${DB_HOST}
|
||
- DB_PORT=${DB_PORT}
|
||
- DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
|
||
- DJANGO_DEBUG=${DJANGO_DEBUG}
|
||
- REDIS_URL=${REDIS_URL}
|
||
- GRAMPS_URL=${GRAMPS_URL}
|
||
- GRAMPS_USERNAME=${GRAMPS_USERNAME}
|
||
- GRAMPS_PASSWORD=${GRAMPS_PASSWORD}
|
||
- GRAMPS_API_TOKEN=${GRAMPS_API_TOKEN}
|
||
- IMAP_HOST=${IMAP_HOST}
|
||
- IMAP_PORT=${IMAP_PORT}
|
||
- IMAP_USER=${IMAP_USER}
|
||
- IMAP_PASSWORD=${IMAP_PASSWORD}
|
||
- IMAP_FOLDER=${IMAP_FOLDER}
|
||
- IMAP_USE_SSL=${IMAP_USE_SSL}
|
||
depends_on:
|
||
- redis
|
||
- db
|
||
command: ["celery", "-A", "core", "beat", "-l", "info"]
|
||
|
||
mcp:
|
||
build:
|
||
context: ./app
|
||
args:
|
||
APP_VERSION: ${APP_VERSION:-unknown}
|
||
depends_on:
|
||
db:
|
||
condition: service_healthy
|
||
environment:
|
||
- POSTGRES_DB=${POSTGRES_DB}
|
||
- POSTGRES_USER=${POSTGRES_USER}
|
||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||
- DB_HOST=${DB_HOST}
|
||
- DB_PORT=${DB_PORT}
|
||
- DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
|
||
- DJANGO_DEBUG=0
|
||
- DJANGO_ALLOWED_HOSTS=localhost
|
||
- LANGUAGE_CODE=${LANGUAGE_CODE}
|
||
- TIME_ZONE=${TIME_ZONE}
|
||
- MCP_TOKEN_READONLY=${MCP_TOKEN_READONLY}
|
||
- MCP_TOKEN_EDITOR=${MCP_TOKEN_EDITOR}
|
||
- MCP_TOKEN_ADMIN=${MCP_TOKEN_ADMIN}
|
||
# Kein Port-Mapping – nur internes Netz
|
||
# Start via: docker compose run --rm -e MCP_AUTH_TOKEN=<token> mcp
|
||
stdin_open: true
|
||
command: ["python", "-m", "mcp_server"]
|
||
|
||
ollama:
|
||
image: ollama/ollama:latest
|
||
# Kein externes Port-Mapping — nur über internes Docker-Netzwerk erreichbar
|
||
# Django-App: http://ollama:11434
|
||
environment:
|
||
- OLLAMA_MAX_LOADED_MODELS=1
|
||
- OLLAMA_NUM_PARALLEL=1
|
||
- OLLAMA_DEFAULT_MODEL=${OLLAMA_DEFAULT_MODEL:-qwen2.5:3b}
|
||
volumes:
|
||
- ollama_data:/root/.ollama
|
||
restart: unless-stopped
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "curl -sf http://localhost:11434/api/tags || exit 1"]
|
||
interval: 30s
|
||
timeout: 10s
|
||
retries: 5
|
||
start_period: 60s
|
||
# Beim ersten Start: Ollama starten, dann Modell laden (falls nicht vorhanden)
|
||
entrypoint: >
|
||
sh -c "
|
||
ollama serve &
|
||
OLLAMA_PID=$$!
|
||
echo '[ollama] Warte auf API...'
|
||
RETRIES=0
|
||
until curl -sf http://localhost:11434/api/tags > /dev/null 2>&1; do
|
||
RETRIES=$$((RETRIES + 1))
|
||
[ $$RETRIES -ge 60 ] && echo '[ollama] FEHLER: API nicht bereit.' && exit 1
|
||
sleep 1
|
||
done
|
||
MODEL=$${OLLAMA_DEFAULT_MODEL:-qwen2.5:3b}
|
||
if ollama list | grep -q \"$$MODEL\"; then
|
||
echo \"[ollama] Modell '$$MODEL' bereits vorhanden.\"
|
||
else
|
||
echo \"[ollama] Lade Modell '$$MODEL'...\"
|
||
ollama pull \"$$MODEL\"
|
||
fi
|
||
wait $$OLLAMA_PID
|
||
"
|
||
|
||
grampsweb:
|
||
image: ghcr.io/gramps-project/grampsweb:latest
|
||
ports:
|
||
- "8090:5000"
|
||
environment:
|
||
- GRAMPSWEB_SECRET_KEY=${GRAMPSWEB_SECRET_KEY:-dev-grampsweb-secret-key-not-for-production}
|
||
- GRAMPSWEB_ADMIN_EMAIL=${GRAMPSWEB_ADMIN_EMAIL:-admin@localhost}
|
||
- GRAMPSWEB_ADMIN_PASSWORD=${GRAMPSWEB_ADMIN_PASSWORD:-gramps_dev_password}
|
||
- GRAMPSWEB_TREE=${GRAMPSWEB_TREE:-Stiftung}
|
||
- GRAMPSWEB_BASE_URL=${GRAMPSWEB_BASE_URL:-http://localhost:8090}
|
||
- GRAMPSWEB_CELERY_CONFIG__broker_url=redis://redis:6379/0
|
||
- GRAMPSWEB_CELERY_CONFIG__result_backend=redis://redis:6379/0
|
||
- GRAMPSWEB_RATELIMIT_STORAGE_URI=redis://redis:6379/1
|
||
- GRAMPSWEB_NEW_DB_BACKEND=sqlite
|
||
- GRAMPSWEB_SUBPATH=${GRAMPSWEB_SUBPATH:-/ahnenforschung}
|
||
command:
|
||
- sh
|
||
- -c
|
||
- |
|
||
if [ -n "$$GRAMPSWEB_SUBPATH" ] && [ "$$GRAMPSWEB_SUBPATH" != "/" ]; then
|
||
SUBPATH="$$GRAMPSWEB_SUBPATH"
|
||
case "$$SUBPATH" in */) ;; *) SUBPATH="$${SUBPATH}/" ;; esac
|
||
echo "[grampsweb] Patching static files for subpath $$SUBPATH ..."
|
||
find / -name index.html -path "*/gramps*" -o -name index.html -path "*/static/*" 2>/dev/null | while read f; do
|
||
if grep -q '<base href="/">' "$$f" 2>/dev/null; then
|
||
sed -i "s|<base href=\"/\">|<base href=\"$$SUBPATH\">|g" "$$f"
|
||
echo "[grampsweb] patched base href: $$f"
|
||
fi
|
||
done
|
||
for f in /app/static/*.js; do
|
||
if [ -f "$$f" ] && grep -q '/api/' "$$f" 2>/dev/null; then
|
||
sed -i "s|\"/api/|\"$${SUBPATH}api/|g" "$$f"
|
||
sed -i 's|`/api/|`'"$${SUBPATH}"'api/|g' "$$f"
|
||
sed -i "s|\"/lang/|\"$${SUBPATH}lang/|g" "$$f"
|
||
sed -i 's|`/lang/|`'"$${SUBPATH}"'lang/|g' "$$f"
|
||
sed -i "s|\"/fonts/|\"$${SUBPATH}fonts/|g" "$$f"
|
||
sed -i 's|`/fonts/|`'"$${SUBPATH}"'fonts/|g' "$$f"
|
||
sed -i "s|\"/assets/|\"$${SUBPATH}assets/|g" "$$f"
|
||
sed -i 's|`/assets/|`'"$${SUBPATH}"'assets/|g' "$$f"
|
||
sed -i "s|location\.href=\"/\"|location.href=\"$$SUBPATH\"|g" "$$f"
|
||
sed -i "s|document\.location\.href=\"/\"|document.location.href=\"$$SUBPATH\"|g" "$$f"
|
||
echo "[grampsweb] patched JS paths: $$f"
|
||
fi
|
||
done
|
||
if [ -f /app/static/sw.js ]; then
|
||
sed -i "s|createHandlerBoundToURL(\"/index.html\")|createHandlerBoundToURL(\"$${SUBPATH}index.html\")|g" /app/static/sw.js
|
||
SUBPATH_BS=$$(echo "$$SUBPATH" | sed "s|/|\\\\\\\\/|g")
|
||
sed -i "s|\\^\\\\/api|\\^$${SUBPATH_BS}api|g" /app/static/sw.js
|
||
echo "[grampsweb] patched sw.js navigation routes"
|
||
fi
|
||
find /app/static -name '*.css' 2>/dev/null | while read f; do
|
||
if grep -q '\.\./fonts/' "$$f" 2>/dev/null; then
|
||
sed -i "s|'../fonts/|'fonts/|g" "$$f"
|
||
sed -i "s|\"../fonts/|\"fonts/|g" "$$f"
|
||
echo "[grampsweb] patched CSS font paths: $$f"
|
||
fi
|
||
done
|
||
echo "[grampsweb] Done."
|
||
fi
|
||
echo "[grampsweb] Ensuring admin user exists ..."
|
||
python3 << 'PYEOF' 2>&1 | grep -v Gtk
|
||
from gramps_webapi.app import create_app
|
||
from gramps_webapi.auth import add_user, get_number_users, ROLE_OWNER
|
||
import os
|
||
email = os.environ.get('GRAMPSWEB_ADMIN_EMAIL', '')
|
||
pw = os.environ.get('GRAMPSWEB_ADMIN_PASSWORD', '')
|
||
if email and pw:
|
||
app = create_app()
|
||
with app.app_context():
|
||
if get_number_users() == 0:
|
||
add_user(name='Admin', email=email, password=pw, role=ROLE_OWNER)
|
||
print('[grampsweb] Admin user created')
|
||
else:
|
||
print('[grampsweb] Users already exist, skipping')
|
||
else:
|
||
print('[grampsweb] No admin credentials configured, skipping')
|
||
PYEOF
|
||
exec gunicorn -w $${GUNICORN_NUM_WORKERS:-8} -b 0.0.0.0:5000 \
|
||
gramps_webapi.wsgi:app --timeout $${GUNICORN_TIMEOUT:-120} \
|
||
--limit-request-line 8190
|
||
volumes:
|
||
- gramps_data:/app/data
|
||
depends_on:
|
||
- db
|
||
- redis
|
||
|
||
volumes:
|
||
dbdata:
|
||
gramps_data:
|
||
ollama_data:
|