Files
Calbook/deploy.sh

445 lines
15 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${ROOT_DIR}/.env"
ENV_EXAMPLE="${ROOT_DIR}/.env.example"
# ── Colors ──────────────────────────────────────────────
BOLD='\033[1m'
DIM='\033[2m'
GREEN='\033[32m'
BLUE='\033[34m'
YELLOW='\033[33m'
RED='\033[31m'
NC='\033[0m'
section() { echo -e "\n${BOLD}${BLUE}${*}${NC}"; }
success() { echo -e "${GREEN}${NC} ${*}"; }
info() { echo -e " ${DIM}${*}${NC}"; }
warn() { echo -e "${YELLOW}${NC} ${*}"; }
error() { echo -e "${RED}${NC} ${*}" >&2; }
# ── Helpers ──────────────────────────────────────────────
random() {
openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c "${1:-32}"
}
random_password() {
openssl rand -base64 48 | tr -dc 'A-Za-z0-9@%+=_.-' | head -c "${1:-20}"
}
ask() {
local label="$1"; local default="${2:-}"; local answer
if [[ -n "${default}" ]]; then
read -r -p " ${label} [${default}]: " answer
else
read -r -p " ${label}: " answer
fi
echo "${answer:-${default}}"
}
ask_yn() {
local label="$1"; local default_yes="${2:-true}"; local hint answer
[[ "${default_yes}" == "true" ]] && hint="J/n" || hint="j/N"
while true; do
read -r -p " ${label} [${hint}]: " answer
answer="$(echo "${answer}" | tr '[:upper:]' '[:lower:]')"
[[ -z "${answer}" && "${default_yes}" == "true" ]] && return 0
[[ -z "${answer}" && "${default_yes}" == "false" ]] && return 1
[[ "${answer}" =~ ^(j|ja|y|yes)$ ]] && return 0
[[ "${answer}" =~ ^(n|nein|no)$ ]] && return 1
echo " → Bitte mit j oder n antworten."
done
}
ask_choice() {
local label="$1"; local default="$2"; shift 2; local options=("$@"); local value
while true; do
value="$(ask "${label} (${options[*]})" "${default}")"
value="$(echo "${value}" | tr '[:upper:]' '[:lower:]')"
for opt in "${options[@]}"; do
[[ "${value}" == "${opt}" ]] && { echo "${value}"; return; }
done
echo " → Erlaubt: ${options[*]}"
done
}
get_env() {
local key="$1"
if [[ ! -f "${ENV_FILE}" ]]; then echo ""; return; fi
local line val
line="$(grep -E "^${key}=" "${ENV_FILE}" | head -n1 || true)"
[[ -z "${line}" ]] && { echo ""; return; }
val="${line#*=}"
val="${val#\"}"; val="${val%\"}"
val="${val#\'}"; val="${val%\'}"
echo "${val}"
}
set_env() {
local key="$1"; local value="$2"
if grep -qE "^${key}=" "${ENV_FILE}" 2>/dev/null; then
sed -i "s|^${key}=.*|${key}=${value}|" "${ENV_FILE}"
else
echo "${key}=${value}" >> "${ENV_FILE}"
fi
}
is_placeholder() {
local val; val="$(echo "${1:-}" | tr '[:upper:]' '[:lower:]')"
[[ -z "${val}" ]] && return 0
[[ "${val}" == change_me* ]] && return 0
[[ "${val}" == *random-secret* ]] && return 0
[[ "${val}" == *random-key* ]] && return 0
[[ "${val}" == *random-salt* ]] && return 0
[[ "${val}" == bitte-* ]] && return 0
return 1
}
mask() {
local val="${1:-}"
[[ ${#val} -le 8 ]] && { echo "********"; return; }
echo "${val:0:4}...${val: -4}"
}
urlencode() {
local s="${1:-}"; local len="${#s}"; local out="" char c
for ((i=0; i<len; i++)); do
char="${s:i:1}"
case "${char}" in
[a-zA-Z0-9.~_-]) out+="${char}" ;;
*) printf -v c '%%%02X' "'${char}"; out+="${c}" ;;
esac
done
echo "${out}"
}
check_cmd() { command -v "$1" >/dev/null 2>&1; }
# ── Prerequisites ────────────────────────────────────────
section "[1/5] Voraussetzungen prüfen"
if ! check_cmd docker; then
error "Docker ist nicht installiert. Bitte installiere Docker zuerst."
info "→ https://docs.docker.com/engine/install/"
exit 1
fi
if ! docker info >/dev/null 2>&1; then
error "Docker-Daemon läuft nicht oder du hast keine Zugriffsrechte."
exit 1
fi
success "Docker läuft"
if ! check_cmd openssl; then
error "openssl wird benötigt (für Secret-Generierung)."
exit 1
fi
COMPOSE_CMD="docker compose"
if ! docker compose version >/dev/null 2>&1; then
if check_cmd docker-compose; then
COMPOSE_CMD="docker-compose"
else
error "Weder 'docker compose' noch 'docker-compose' gefunden."
exit 1
fi
fi
success "Compose verfügbar (${COMPOSE_CMD})"
# ── Initialise .env ──────────────────────────────────────
ENV_EXISTS=false
[[ -f "${ENV_FILE}" ]] && ENV_EXISTS=true
if ${ENV_EXISTS}; then
if ask_yn ".env existiert bereits. Werte übernehmen?" "true"; then
info "Bestehende .env wird erweitert/aktualisiert."
else
cp "${ENV_EXAMPLE}" "${ENV_FILE}"
info "Neue .env aus Vorlage erstellt."
NEW_ENV=true
fi
else
cp "${ENV_EXAMPLE}" "${ENV_FILE}"
info ".env aus Vorlage erstellt."
NEW_ENV=true
fi
# ── [2/5] Base config ────────────────────────────────────
section "[2/5] Basis-Konfiguration"
if ${ENV_EXISTS} && ! is_placeholder "$(get_env PUBLIC_URL)"; then
_existing_url="$(get_env PUBLIC_URL)"
else
_existing_url="http://localhost:3000"
fi
while true; do
PUBLIC_URL="$(ask "Öffentliche URL der App" "${_existing_url}")"
if [[ "${PUBLIC_URL}" =~ ^https?:// ]]; then
break
fi
echo " → Bitte mit http:// oder https:// beginnen."
done
STACK_NAME="$(ask "Container-Präfix (für docker ps)" "$(get_env STACK_NAME)")"
[[ -z "${STACK_NAME}" ]] && STACK_NAME="calbook"
TIMEZONE="$(ask "Zeitzone" "$(get_env DEFAULT_TIMEZONE)")"
[[ -z "${TIMEZONE}" ]] && TIMEZONE="Europe/Berlin"
DEPLOYMENT_MODE="$(ask_choice "Deployment-Modus" \
"$([[ "$(get_env DEPLOYMENT_MODE)" == "proxy" ]] && echo "proxy" || echo "direct")" \
"direct" "proxy")"
if [[ "${DEPLOYMENT_MODE}" == "direct" ]]; then
COMPOSE_FILE="${ROOT_DIR}/docker-compose.direct.yml"
TRUST_PROXY="false"
else
COMPOSE_FILE="${ROOT_DIR}/docker-compose.proxy.yml"
TRUST_PROXY="true"
TRAEFIK_HOST="$(echo "${PUBLIC_URL}" | sed -E 's#^[a-zA-Z]+://##' | cut -d/ -f1 | cut -d: -f1)"
if [[ -z "${TRAEFIK_HOST}" || "${TRAEFIK_HOST}" == "localhost" || "${TRAEFIK_HOST}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
TRAEFIK_HOST="calbook.local"
fi
TRAEFIK_HOST="$(ask "Domain für Traefik (aus URL abgeleitet)" "${TRAEFIK_HOST}")"
[[ -z "${TRAEFIK_HOST}" ]] && TRAEFIK_HOST="calbook.local"
TRAEFIK_ENTRYPOINTS="$(ask "Traefik Entrypoints" "$(get_env TRAEFIK_ENTRYPOINTS)")"
[[ -z "${TRAEFIK_ENTRYPOINTS}" ]] && TRAEFIK_ENTRYPOINTS="websecure"
if [[ "${PUBLIC_URL}" == https://* ]]; then
TRAEFIK_TLS="true"
else
TRAEFIK_TLS="false"
fi
TRAEFIK_TLS="$(ask_yn "TLS aktivieren?" "${TRAEFIK_TLS}" && echo "true" || echo "false")"
if [[ "${TRAEFIK_TLS}" == "true" ]]; then
TRAEFIK_CERTRESOLVER="$(ask "Certresolver" "$(get_env TRAEFIK_CERTRESOLVER)")"
[[ -z "${TRAEFIK_CERTRESOLVER}" ]] && TRAEFIK_CERTRESOLVER="tls_resolver"
else
TRAEFIK_CERTRESOLVER="$(get_env TRAEFIK_CERTRESOLVER)"
[[ -z "${TRAEFIK_CERTRESOLVER}" ]] && TRAEFIK_CERTRESOLVER="tls_resolver"
fi
TRAEFIK_NETWORK="$(ask "Traefik Docker-Netzwerk" "$(get_env TRAEFIK_DOCKER_NETWORK)")"
[[ -z "${TRAEFIK_NETWORK}" ]] && TRAEFIK_NETWORK="proxy"
TRAEFIK_ROUTER="$(ask "Traefik Router-Name" "$(get_env TRAEFIK_ROUTER_NAME)")"
[[ -z "${TRAEFIK_ROUTER}" ]] && TRAEFIK_ROUTER="calbook"
TRAEFIK_SERVICE="$(ask "Traefik Service-Name" "$(get_env TRAEFIK_SERVICE_NAME)")"
[[ -z "${TRAEFIK_SERVICE}" ]] && TRAEFIK_SERVICE="calbook"
fi
success "URL: ${PUBLIC_URL} | Stack: ${STACK_NAME} | Modus: ${DEPLOYMENT_MODE}"
# ── [3/5] Admin ──────────────────────────────────────────
section "[3/5] Admin-Zugang"
ADMIN_EMAIL="$(ask "Admin E-Mail" "$(get_env ADMIN_EMAIL)")"
[[ -z "${ADMIN_EMAIL}" ]] && ADMIN_EMAIL="admin@calbook.local"
ADMIN_NAME="$(ask "Admin Name" "$(get_env ADMIN_NAME)")"
[[ -z "${ADMIN_NAME}" ]] && ADMIN_NAME="CalBook Admin"
existing_pw="$(get_env ADMIN_PASSWORD)"
if ${ENV_EXISTS} && ! is_placeholder "${existing_pw}"; then
if ask_yn "Admin-Passwort beibehalten?" "true"; then
ADMIN_PASSWORD="${existing_pw}"
else
ADMIN_PASSWORD="$(ask "Neues Admin-Passwort (min. 12 Zeichen)" "")"
[[ -z "${ADMIN_PASSWORD}" ]] && ADMIN_PASSWORD="$(random_password 20)"
fi
else
ADMIN_PASSWORD="$(ask "Admin-Passwort (leer = automatisch generieren)" "")"
[[ -z "${ADMIN_PASSWORD}" ]] && ADMIN_PASSWORD="$(random_password 20)"
fi
success "Admin: ${ADMIN_EMAIL}"
# ── [4/5] SMTP & Jitsi ───────────────────────────────────
section "[4/5] E-Mail & Videokonferenz"
SMTP_MODE="$(ask_choice "SMTP-Modus" \
"$([[ "$(get_env SMTP_HOST)" == "mailhog" || -z "$(get_env SMTP_HOST)" ]] && echo "mailhog" || echo "custom")" \
"mailhog" "custom" "off")"
if [[ "${SMTP_MODE}" == "mailhog" ]]; then
SMTP_HOST="mailhog"
SMTP_PORT="1025"
SMTP_USER=""
SMTP_PASS=""
elif [[ "${SMTP_MODE}" == "custom" ]]; then
SMTP_HOST="$(ask "SMTP-Host" "$(get_env SMTP_HOST)")"
SMTP_PORT="$(ask "SMTP-Port" "$(get_env SMTP_PORT)")"
[[ -z "${SMTP_PORT}" ]] && SMTP_PORT="587"
SMTP_USER="$(ask "SMTP-Benutzer (optional)" "$(get_env SMTP_USER)")"
SMTP_PASS="$(ask "SMTP-Passwort (optional)" "$(get_env SMTP_PASS)")"
else
SMTP_HOST=""
SMTP_PORT="587"
SMTP_USER=""
SMTP_PASS=""
fi
SMTP_FROM_NAME="$(ask "Absendername" "$(get_env SMTP_FROM_NAME)")"
[[ -z "${SMTP_FROM_NAME}" ]] && SMTP_FROM_NAME="${ADMIN_NAME}"
SMTP_FROM="$(ask "Absender-E-Mail" "$(get_env SMTP_FROM)")"
[[ -z "${SMTP_FROM}" ]] && SMTP_FROM="no-reply@calbook.local"
JITSI_MODE="$(ask_choice "Jitsi-Modus" "$(get_env JITSI_MEETING_MODE)" "public" "custom")"
if [[ "${JITSI_MODE}" == "custom" ]]; then
JITSI_URL="$(ask "Jitsi-Basis-URL" "$(get_env JITSI_BASE_URL)")"
else
JITSI_URL="https://meet.jit.si"
fi
JITSI_PREFIX="$(ask "Jitsi-Raum-Präfix" "$(get_env JITSI_ROOM_PREFIX)")"
[[ -z "${JITSI_PREFIX}" ]] && JITSI_PREFIX="calbook"
success "SMTP: ${SMTP_MODE} | Jitsi: ${JITSI_MODE}"
# ── Generate secrets ─────────────────────────────────────
NEXTAUTH_SECRET="$(random 64)"
CRON_SECRET="$(random 48)"
CALDAV_KEY="$(random 64)"
JITSI_SALT="$(random 48)"
POSTGRES_PASSWORD="$(random_password 24)"
# Keep existing strong secrets if present
_secret_keys=(NEXTAUTH_SECRET CRON_SECRET CALDAV_ENCRYPTION_KEY JITSI_ROOM_SALT POSTGRES_PASSWORD)
for key in "${_secret_keys[@]}"; do
existing="$(get_env "${key}")"
if [[ -n "${existing}" ]] && ! is_placeholder "${existing}"; then
declare "${key}=${existing}"
fi
done
POSTGRES_DB="$(get_env POSTGRES_DB)"
[[ -z "${POSTGRES_DB}" ]] && POSTGRES_DB="calbook"
POSTGRES_USER="$(get_env POSTGRES_USER)"
[[ -z "${POSTGRES_USER}" ]] && POSTGRES_USER="calbook"
DATABASE_URL="postgresql://$(urlencode "${POSTGRES_USER}"):$(urlencode "${POSTGRES_PASSWORD}")@db:5432/$(urlencode "${POSTGRES_DB}")?schema=public"
# ── Write .env ───────────────────────────────────────────
set_env "STACK_NAME" "${STACK_NAME}"
set_env "DEPLOYMENT_MODE" "${DEPLOYMENT_MODE}"
set_env "PUBLIC_URL" "${PUBLIC_URL}"
set_env "NEXTAUTH_URL" "${PUBLIC_URL}"
set_env "APP_BASE_URL" "${PUBLIC_URL}"
set_env "NEXTAUTH_SECRET" "${NEXTAUTH_SECRET}"
set_env "CRON_SECRET" "${CRON_SECRET}"
set_env "TRUST_PROXY_HEADERS" "${TRUST_PROXY}"
set_env "DEFAULT_TIMEZONE" "${TIMEZONE}"
set_env "ADMIN_NAME" "${ADMIN_NAME}"
set_env "ADMIN_EMAIL" "${ADMIN_EMAIL}"
set_env "ADMIN_PASSWORD" "${ADMIN_PASSWORD}"
set_env "POSTGRES_DB" "${POSTGRES_DB}"
set_env "POSTGRES_USER" "${POSTGRES_USER}"
set_env "POSTGRES_PASSWORD" "${POSTGRES_PASSWORD}"
set_env "DATABASE_URL" "${DATABASE_URL}"
set_env "CALDAV_ENCRYPTION_KEY" "${CALDAV_KEY}"
set_env "JITSI_ROOM_SALT" "${JITSI_SALT}"
set_env "SMTP_HOST" "${SMTP_HOST}"
set_env "SMTP_PORT" "${SMTP_PORT}"
set_env "SMTP_USER" "${SMTP_USER}"
set_env "SMTP_PASS" "${SMTP_PASS}"
set_env "SMTP_FROM_NAME" "${SMTP_FROM_NAME}"
set_env "SMTP_FROM" "${SMTP_FROM}"
set_env "JITSI_MEETING_MODE" "${JITSI_MODE}"
set_env "JITSI_BASE_URL" "${JITSI_URL}"
set_env "JITSI_ROOM_PREFIX" "${JITSI_PREFIX}"
if [[ "${DEPLOYMENT_MODE}" == "proxy" ]]; then
set_env "ENABLE_TRAEFIK" "true"
set_env "TRAEFIK_HOST" "${TRAEFIK_HOST}"
set_env "TRAEFIK_ENTRYPOINTS" "${TRAEFIK_ENTRYPOINTS}"
set_env "TRAEFIK_TLS" "${TRAEFIK_TLS}"
set_env "TRAEFIK_CERTRESOLVER" "${TRAEFIK_CERTRESOLVER}"
set_env "TRAEFIK_ROUTER_NAME" "${TRAEFIK_ROUTER}"
set_env "TRAEFIK_SERVICE_NAME" "${TRAEFIK_SERVICE}"
set_env "TRAEFIK_DOCKER_NETWORK" "${TRAEFIK_NETWORK}"
else
set_env "ENABLE_TRAEFIK" "false"
fi
info ".env geschrieben."
# ── [5/5] Build & Start ──────────────────────────────────
section "[5/5] Container starten & Datenbank einrichten"
if [[ "${DEPLOYMENT_MODE}" == "proxy" ]]; then
docker network inspect "${TRAEFIK_NETWORK}" >/dev/null 2>&1 || {
info "Erstelle Traefik-Netzwerk: ${TRAEFIK_NETWORK}"
docker network create "${TRAEFIK_NETWORK}"
}
fi
if [[ "${NEW_ENV:-false}" == "true" ]]; then
VOLUME_DIR="${ROOT_DIR}/volumes/postgres-${STACK_NAME}"
if [[ -d "${VOLUME_DIR}" ]]; then
if ask_yn "Alte DB-Daten für Stack '${STACK_NAME}' gefunden. Löschen für Neuaufsetzung?" "true"; then
info "Lösche alte DB-Daten: ${VOLUME_DIR}"
rm -rf "${VOLUME_DIR}"
fi
fi
fi
SERVICES=(db calbook-app)
if [[ "${SMTP_HOST}" == "mailhog" || -z "${SMTP_HOST}" ]]; then
SERVICES+=(mailhog)
fi
info "Starte: ${SERVICES[*]}"
${COMPOSE_CMD} -f "${COMPOSE_FILE}" up -d --build "${SERVICES[@]}"
info "DB einrichten (Prisma + Seed)..."
${COMPOSE_CMD} -f "${COMPOSE_FILE}" build calbook-tools
${COMPOSE_CMD} -f "${COMPOSE_FILE}" run --rm calbook-tools npm run prisma:generate
if ! ${COMPOSE_CMD} -f "${COMPOSE_FILE}" run --rm calbook-tools npm run prisma:migrate; then
info "Migration fehlgeschlagen → prisma:push (Legacy-Fallback)..."
${COMPOSE_CMD} -f "${COMPOSE_FILE}" run --rm calbook-tools npm run prisma:push
fi
if ${COMPOSE_CMD} -f "${COMPOSE_FILE}" run --rm calbook-tools npm run db:seed; then
success "Seed abgeschlossen"
else
warn "Seed fehlgeschlagen Admin existiert möglicherweise bereits."
fi
# ── Summary ──────────────────────────────────────────────
echo
echo -e "${BOLD}${GREEN}══════════════════════════════════════════════${NC}"
echo -e "${BOLD}${GREEN} CalBook läuft!${NC}"
echo
echo -e " URL: ${BOLD}${PUBLIC_URL}${NC}"
echo -e " Login: ${BOLD}/anmelden${NC}"
echo -e " Admin: ${BOLD}${ADMIN_EMAIL}${NC}"
echo -e " Passwort: ${BOLD}${ADMIN_PASSWORD}${NC}"
echo -e " Compose: ${DIM}${COMPOSE_FILE}${NC}"
echo -e " Modus: ${DEPLOYMENT_MODE}"
if [[ "${SMTP_HOST}" == "mailhog" ]]; then
if [[ "${DEPLOYMENT_MODE}" == "direct" ]]; then
echo -e " Mailhog: ${BOLD}http://localhost:8025${NC}"
else
echo -e " Mailhog: ${DIM}intern (kein Host-Port)${NC}"
fi
fi
echo -e "${BOLD}${GREEN}══════════════════════════════════════════════${NC}"
echo
echo "Logs: ${COMPOSE_CMD} -f ${COMPOSE_FILE} logs -f calbook-app db"
echo