#!/bin/bash # Phase 2: Recover compose files, deploy services, start Gitea. # Pipeable — run after setup.sh and after rsync'ing the repo. # # Usage (from workstation): # ssh root@ 'bash -s' < bootstrap.sh # # Prerequisites: # - setup.sh already run (infra_service user, dirs, uv) # - Repo rsync'd to /opt/server-infra/ # # What it does: # 1. Validates prerequisites # 2. Recovers compose files from running containers (umami, reverse-proxy) # 3. Creates data directories # 4. Sets correct ownership # 5. Prints next steps (start Gitea, add proxy host, push repo to Gitea) set -euo pipefail SERVICE_USER="infra_service" REPO_DIR="/opt/server-infra" DATA_DIR="/data/server-infra" UV="/home/${SERVICE_USER}/.local/bin/uv" [ "$(id -u)" = "0" ] || { echo "ERROR: Run as root"; exit 1; } log() { echo "$(date '+%H:%M:%S') ==> $*"; } warn() { echo "$(date '+%H:%M:%S') ==> WARNING: $*" >&2; } # ── Preflight checks ─────────────────────────────────────────────────────────── if ! id "${SERVICE_USER}" >/dev/null 2>&1; then echo "ERROR: ${SERVICE_USER} user not found. Run setup.sh first." exit 1 fi if [ ! -d "${REPO_DIR}/.git" ]; then echo "ERROR: Repo not found at ${REPO_DIR}. Rsync the repo first:" echo " rsync -av --chown=root:root ~/Projects/server-infra/ root@:${REPO_DIR}/" exit 1 fi command -v docker >/dev/null 2>&1 || { echo "ERROR: docker not found"; exit 1; } [ -f "${UV}" ] || { echo "ERROR: uv not found at ${UV}. Run setup.sh first."; exit 1; } # ── Inline autocompose script (replaces abandoned docker-autocompose package) ── # Uses modern docker SDK. Written to /tmp, run via uv (fetches deps on demand). cat > /tmp/server-infra-autocompose.py << 'PYEOF' # /// script # requires-python = ">=3.10" # dependencies = ["docker>=7.0", "pyyaml>=6.0"] # /// """Generate a docker-compose services block from running containers.""" import sys import docker import yaml def container_to_service(container): attrs = container.attrs config = attrs["Config"] host_config = attrs["HostConfig"] service = {"image": config["Image"]} restart = (host_config.get("RestartPolicy") or {}).get("Name", "no") if restart and restart not in ("no", ""): service["restart"] = restart env = config.get("Env") or [] if env: service["environment"] = env port_bindings = host_config.get("PortBindings") or {} if port_bindings: ports = [] for container_port, bindings in port_bindings.items(): for binding in (bindings or []): host_ip = binding.get("HostIp", "") host_port = binding.get("HostPort", "") if host_ip and host_ip not in ("0.0.0.0", "::"): ports.append(f"{host_ip}:{host_port}:{container_port}") elif host_port: ports.append(f"{host_port}:{container_port}") else: ports.append(container_port) if ports: service["ports"] = ports volumes = [] for mount in attrs.get("Mounts") or []: dst = mount["Destination"] if mount["Type"] == "bind": src = mount["Source"] mode = mount.get("Mode", "rw") volumes.append(f"{src}:{dst}" if mode == "rw" else f"{src}:{dst}:{mode}") elif mount["Type"] == "volume": name = mount.get("Name", "") if name: volumes.append(f"{name}:{dst}") if volumes: service["volumes"] = volumes return service def main(): if len(sys.argv) < 2: print("Usage: autocompose.py [...]", file=sys.stderr) sys.exit(1) client = docker.from_env() services = {} for name in sys.argv[1:]: try: container = client.containers.get(name) labels = (container.attrs["Config"].get("Labels") or {}) service_name = labels.get("com.docker.compose.service", name) services[service_name] = container_to_service(container) except docker.errors.NotFound: print(f"WARNING: Container {name} not found", file=sys.stderr) print(yaml.dump({"services": services}, default_flow_style=False, sort_keys=False)) if __name__ == "__main__": main() PYEOF # ── Recover compose files from running containers ────────────────────────────── recover_project() { local outfile="$1" shift local containers=("$@") local running=0 for c in "${containers[@]}"; do if docker inspect "${c}" >/dev/null 2>&1; then running=1 break fi done if [ "${running}" = "0" ]; then warn "None of [${containers[*]}] are running — skipping recovery." return 0 fi log "Recovering: ${containers[*]}" sudo -u "${SERVICE_USER}" \ "${UV}" run /tmp/server-infra-autocompose.py "${containers[@]}" > "${outfile}" log " Saved to ${outfile}" } log "Recovering compose files from running containers..." recover_project \ "${REPO_DIR}/umami/docker-compose.yml" \ umami-umami-1 umami-db-1 recover_project \ "${REPO_DIR}/reverse-proxy/docker-compose.yml" \ reverse_proxy-app-1 # ── Data directories ─────────────────────────────────────────────────────────── log "Creating data directories..." mkdir -p "${DATA_DIR}/gitea" # ── Ownership ────────────────────────────────────────────────────────────────── log "Setting ownership..." chown -R "${SERVICE_USER}:${SERVICE_USER}" "${REPO_DIR}" chown -R "${SERVICE_USER}:${SERVICE_USER}" "${DATA_DIR}" # ── Summary ──────────────────────────────────────────────────────────────────── SERVER_IP=$(hostname -I | awk '{print $1}') echo "" echo "==================================================================" echo "" echo " Bootstrap complete." echo "" echo " Services ready in ${REPO_DIR}/:" [ -f "${REPO_DIR}/umami/docker-compose.yml" ] && echo " umami (recovered)" [ -f "${REPO_DIR}/reverse-proxy/docker-compose.yml" ] && echo " reverse-proxy (recovered)" [ -f "${REPO_DIR}/gitea/docker-compose.yml" ] && echo " gitea (from repo)" echo "" echo " Service user: ${SERVICE_USER}" echo " Data dir: ${DATA_DIR}/" echo "" echo "==================================================================" echo "" echo " Next steps:" echo "" echo " 1. Start Gitea:" echo " sudo -u ${SERVICE_USER} docker compose -f ${REPO_DIR}/gitea/docker-compose.yml up -d" echo " # Web installer at http://${SERVER_IP}:3000" echo " # Set ROOT_URL to your public domain (e.g. https://git.yourdomain.com)" echo "" echo " 2. Add proxy host in nginx proxy manager:" echo " Forward hostname → ${SERVER_IP}:3000" echo "" echo " 3. Commit recovered compose files and push to Gitea:" echo " cd ${REPO_DIR}" echo " sudo -u ${SERVICE_USER} git add umami/docker-compose.yml reverse-proxy/docker-compose.yml" echo " sudo -u ${SERVICE_USER} git commit -m 'chore: recover compose files'" echo " sudo -u ${SERVICE_USER} git remote add origin https://git.yourdomain.com/youruser/server-infra.git" echo " sudo -u ${SERVICE_USER} git push -u origin master" echo "" echo "==================================================================" echo ""