diff --git a/deploy_script.sh b/deploy_script.sh new file mode 100644 index 0000000..e7ed756 --- /dev/null +++ b/deploy_script.sh @@ -0,0 +1,378 @@ +#!/usr/bin/env bash +############################################################################### +# PaperClip — Master Deploy Script +# Fully idempotent. Safe to re-run at any time. +# Usage: sudo bash /opt/PaperClip/deploy_script.sh +############################################################################### +set -euo pipefail + +REPO_DIR="/opt/PaperClip" +GIT_REPO_URL="https://github.com/orfelorfel23/PaperClip.git" +DATA_BASE="/opt/PaperClip-Data" +DOCKER_NETWORK="paperclip-net" +SERVICES_DIR="${REPO_DIR}/services" +AGE_PRIVATE_KEY="${REPO_DIR}/keys/age-key.txt" +CADDY_FRAGMENTS_DIR="${DATA_BASE}/caddy/fragments" +POSTGRES_CONTAINER="postgres" + +# Infrastructure services that are started in a specific order +INFRA_SERVICES=("postgres" "caddy") + +ACTIVE_SERVICES=() +SKIPPED_SERVICES=() + +############################################################################### +# Helpers +############################################################################### +log() { echo -e "\e[32m[DEPLOY]\e[0m $*"; } +warn() { echo -e "\e[33m[WARN]\e[0m $*"; } +err() { echo -e "\e[31m[ERROR]\e[0m $*" >&2; } +die() { err "$@"; exit 1; } + +on_deploy_failure() { + err "Deployment GEFEHLT. Prüfe die Server-Logs." + disable_maintenance_mode 2>/dev/null || true +} + +is_active_service() { + local svc_dir="$1" + [[ -f "${svc_dir}/docker-compose.yml" && -f "${svc_dir}/service.enabled" ]] +} + +############################################################################### +# 1. Install missing packages +############################################################################### +install_packages() { + log "Checking required packages..." + + # Docker + if ! command -v docker &>/dev/null; then + log "Installing Docker..." + apt-get update -qq + apt-get install -y -qq ca-certificates curl gnupg lsb-release + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ + gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ + https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \ + tee /etc/apt/sources.list.d/docker.list > /dev/null + apt-get update -qq + apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + systemctl enable --now docker + else + log "Docker already installed." + fi + + # age + if ! command -v age &>/dev/null; then + log "Installing age..." + apt-get update -qq + apt-get install -y -qq age + else + log "age already installed." + fi + + # fail2ban + if ! command -v fail2ban-client &>/dev/null; then + log "Installing fail2ban..." + apt-get update -qq + apt-get install -y -qq fail2ban + else + log "fail2ban already installed." + fi +} + +############################################################################### +# 2. Configure System (Docker, fail2ban, swap, timezone, cron) +############################################################################### +configure_system() { + log "Configuring Docker daemon log limits..." + mkdir -p /etc/docker + if ! grep -q "max-file" /etc/docker/daemon.json 2>/dev/null; then + cat > /etc/docker/daemon.json <<'EOF' +{ + "log-driver": "json-file", + "log-opts": { + "max-size": "50m", + "max-file": "3" + } +} +EOF + systemctl restart docker || true + fi + + log "Configuring fail2ban for SSH..." + cat > /etc/fail2ban/jail.local <<'EOF' +[sshd] +enabled = true +port = ssh +filter = sshd +logpath = /var/log/auth.log +maxretry = 5 +bantime = 3600 +findtime = 600 +EOF + systemctl enable --now fail2ban + systemctl restart fail2ban + + local SWAP_FILE="/swapfile" + local SWAP_SIZE="4G" + if ! swapon --show | grep -q "${SWAP_FILE}"; then + log "Setting up ${SWAP_SIZE} swap file..." + fallocate -l "${SWAP_SIZE}" "${SWAP_FILE}" + chmod 600 "${SWAP_FILE}" + mkswap "${SWAP_FILE}" + swapon "${SWAP_FILE}" + if ! grep -q "${SWAP_FILE}" /etc/fstab; then + echo "${SWAP_FILE} none swap sw 0 0" >> /etc/fstab + fi + fi + + local TARGET_TZ="Europe/Berlin" + if ! timedatectl status | grep -q "${TARGET_TZ}"; then + timedatectl set-timezone "${TARGET_TZ}" + fi + + log "Configuring daily automatic deployment (cron) for 03:00..." + local CRON_JOB="0 3 * * * cd /opt/PaperClip && git fetch origin && git reset --hard origin/main && /bin/bash /opt/PaperClip/deploy_script.sh >> /var/log/paperclip-deploy.log 2>&1" + crontab -l 2>/dev/null | grep -v "/opt/PaperClip/deploy_script.sh" | crontab - || true + (crontab -l 2>/dev/null; echo "$CRON_JOB") | crontab - +} + +############################################################################### +# 3. Handle AGE private key +############################################################################### +handle_age_key() { + log "Checking AGE private key..." + mkdir -p "$(dirname "${AGE_PRIVATE_KEY}")" + + if [[ ! -f "${AGE_PRIVATE_KEY}" ]]; then + warn "AGE private key not found." + echo -n "Please paste your AGE private key now (input hidden): " + read -rs age_key_input + echo "" + if [[ -z "${age_key_input}" ]]; then + die "No key provided. Aborting." + fi + echo "${age_key_input}" > "${AGE_PRIVATE_KEY}" + chmod 600 "${AGE_PRIVATE_KEY}" + fi + + if ! head -1 "${AGE_PRIVATE_KEY}" | grep -q "^AGE-SECRET-KEY-"; then + die "Invalid AGE private key format. Must start with AGE-SECRET-KEY-" + fi + chmod 600 "${AGE_PRIVATE_KEY}" +} + +############################################################################### +# 4. Git clone / pull +############################################################################### +update_repo() { + log "Updating repository..." + if [[ ! -d "${REPO_DIR}/.git" ]]; then + log "No .git directory found — cloning repository..." + local tmp_dir + tmp_dir="$(mktemp -d)" + git clone "${GIT_REPO_URL}" "${tmp_dir}/repo" + rsync -a --ignore-existing "${tmp_dir}/repo/" "${REPO_DIR}/" + cp -a "${tmp_dir}/repo/.git" "${REPO_DIR}/.git" + rm -rf "${tmp_dir}" + cd "${REPO_DIR}" + git checkout -- . 2>/dev/null || true + else + cd "${REPO_DIR}" + git fetch origin + git reset --hard origin/main || git reset --hard origin/master || true + fi + chmod 700 "${REPO_DIR}/deploy_script.sh" 2>/dev/null || true +} + +############################################################################### +# 5. Docker network & Discover services +############################################################################### +ensure_network() { + if ! docker network inspect "${DOCKER_NETWORK}" &>/dev/null; then + log "Creating Docker network: ${DOCKER_NETWORK}" + docker network create "${DOCKER_NETWORK}" + fi +} + +discover_services() { + ACTIVE_SERVICES=() + for svc_dir in "${SERVICES_DIR}"/*/; do + svc_name="$(basename "${svc_dir}")" + if is_active_service "${svc_dir}"; then + ACTIVE_SERVICES+=("${svc_name}") + fi + done + log "Active services: ${ACTIVE_SERVICES[*]}" +} + +cleanup_deactivated() { + log "Checking for deactivated services..." + local running_containers + running_containers=$(docker ps --format '{{.Names}}' 2>/dev/null || true) + for svc_dir in "${SERVICES_DIR}"/*/; do + svc_name="$(basename "${svc_dir}")" + if ! is_active_service "${svc_dir}" && [[ -f "${svc_dir}/docker-compose.yml" ]]; then + if echo "${running_containers}" | grep -q "${svc_name}"; then + log "Stopping deactivated service: ${svc_name}" + cd "${svc_dir}" + docker compose down --remove-orphans 2>/dev/null || true + fi + fi + done +} + +############################################################################### +# 6. Maintenance Mode +############################################################################### +enable_maintenance_mode() { + log "Enabling maintenance mode on all active subdomains..." + local all_domains + all_domains=$(grep -hEo "([a-zA-Z0-9.-]+\.orfel\.de|orfel\.de)" "${SERVICES_DIR}"/*/*.caddy 2>/dev/null | sort -u | tr '\n' ' ' | sed 's/ $//') + if [[ -z "${all_domains}" ]]; then return; fi + + mkdir -p "${CADDY_FRAGMENTS_DIR}" + find "${CADDY_FRAGMENTS_DIR}" -name '*.caddy' -type f -delete 2>/dev/null || true + + cat << 'EOF' > "${CADDY_FRAGMENTS_DIR}/maintenance.caddy" +DOMAINS_PLACEHOLDER { + header Content-Type text/html + respond "
Das System wird gerade aktualisiert.
Die Seite ist in wenigen Minuten wieder erreichbar.