#!/bin/bash set -euo pipefail # Server setup script for a fresh Rocky Linux 9 (or compatible) install. # Run as root after cloning the repo to /opt/hantim. # Safe to re-run — all steps are idempotent. # # Usage: # 1. dnf install -y git # 2. git clone https://git.timothykim.net/hantim/hantim-server.git /opt/hantim # 3. bash /opt/hantim/setup.sh # REPO_DIR="/opt/hantim" # --- Prerequisites --- if [ "$(id -u)" -ne 0 ]; then echo "ERROR: Not running as root." exit 1 fi if [ "$(dirname "$(readlink -f "$0")")" != "$REPO_DIR" ]; then echo "ERROR: Repo does not appear to be cloned to $REPO_DIR." echo " git clone https://git.timothykim.net/hantim/hantim-server.git $REPO_DIR" exit 1 fi # --- Install dependencies --- echo "==> Upgrading system packages..." dnf upgrade -y echo "==> Installing dependencies..." dnf install -y git-crypt jq epel-release dnf install -y certbot dnf module reset -y nodejs dnf module install -y nodejs:20/common BW_CLI="$(npm config get prefix)/bin/bw" if [ ! -x "$BW_CLI" ]; then echo "==> Installing Bitwarden CLI..." npm install -g @bitwarden/cli fi # --- Unlock git-crypt via Bitwarden --- echo "==> Restoring git-tracked files..." cd "$REPO_DIR" git checkout -- . echo "==> Unlocking git-crypt via Bitwarden..." echo " Log in to Bitwarden when prompted." if "$BW_CLI" status 2>/dev/null | grep -q '"status":"unauthenticated"'; then "$BW_CLI" login fi BW_SESSION=$("$BW_CLI" unlock --raw) "$BW_CLI" sync --session "$BW_SESSION" "$BW_CLI" get notes hantim-git-crypt-key --session "$BW_SESSION" | base64 -d > /tmp/git-crypt-key cd "$REPO_DIR" git-crypt unlock /tmp/git-crypt-key rm /tmp/git-crypt-key echo "==> Fetching secrets from Bitwarden..." REGISTRY_TOKEN=$("$BW_CLI" get notes hantim-ci-registry-push --session "$BW_SESSION") DEPLOY_PUBKEY=$("$BW_CLI" get item hantim-server-deploy --session "$BW_SESSION" | jq -r '.sshKey.publicKey') "$BW_CLI" lock # --- Install Docker --- if ! command -v docker &>/dev/null; then echo "==> Installing Docker..." dnf install -y dnf-plugins-core dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin fi systemctl enable --now docker echo "==> Logging into Gitea Docker registry..." echo "$REGISTRY_TOKEN" | docker login git.timothykim.net -u timothykim --password-stdin # --- Create deploy user --- if ! id deploy &>/dev/null; then echo "==> Creating deploy user..." useradd -r -s /usr/sbin/nologin deploy fi usermod -aG docker deploy echo "==> Setting up deploy SSH key..." mkdir -p /home/deploy/.ssh chmod 700 /home/deploy/.ssh echo "command=\"sudo /opt/hantim/scripts/deploy.sh \$SSH_ORIGINAL_COMMAND\",no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty $DEPLOY_PUBKEY" > /home/deploy/.ssh/authorized_keys chmod 600 /home/deploy/.ssh/authorized_keys chown -R deploy:deploy /home/deploy/.ssh echo "==> Configuring sudo for deploy user..." cat > /etc/sudoers.d/deploy < Making scripts executable..." chmod +x "$REPO_DIR/scripts/"*.sh echo "==> Setting up certbot..." mkdir -p "$REPO_DIR/docker/nginx/certbot/www" mkdir -p /etc/letsencrypt/renewal-hooks/deploy cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'HOOK' #!/bin/bash docker exec nginx nginx -s reload HOOK chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh if systemctl list-unit-files certbot-renew.timer 2>/dev/null | grep -q certbot-renew; then systemctl enable --now certbot-renew.timer else echo "0 3 * * * root certbot renew --quiet" > /etc/cron.d/certbot-renew fi echo "==> Issuing SSL certificates..." # Stop nginx if running so certbot can bind to port 80 docker stop nginx 2>/dev/null || true for conf in "$REPO_DIR"/docker/nginx/conf.d/*.conf; do # Derive cert name from config filename (e.g. timothykim.net.conf -> timothykim.net) cert_name=$(basename "$conf" .conf) domains=$(grep -oP 'server_name\s+\K[^;]+' "$conf" | tr ' ' '\n' | sort -u) if [ -z "$domains" ]; then continue fi if [ -d "/etc/letsencrypt/live/$cert_name" ]; then echo " Cert for $cert_name already exists, skipping." continue fi domain_args="" for d in $domains; do domain_args="$domain_args -d $d" done echo " Issuing cert for: $cert_name ($domains)" certbot certonly --standalone --non-interactive --agree-tos --register-unsafely-without-email --cert-name "$cert_name" $domain_args done echo "==> Starting services..." # Start nginx first (creates the shared network) cd "$REPO_DIR/docker/nginx" docker compose up -d # Start all other apps for app in "$REPO_DIR"/docker/*/; do if [ "$(basename "$app")" = "nginx" ]; then continue fi app_name=$(basename "$app") echo " Starting $app_name..." cd "$app" if ! docker compose up -d; then echo " WARNING: Failed to start $app_name (image may not exist yet). It will start on first deploy." fi done echo "==> Done."