diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 8b13789..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ - diff --git a/.gitea/workflows/deploy-garage.yml b/.gitea/workflows/deploy-garage.yml new file mode 100644 index 0000000..38a5a74 --- /dev/null +++ b/.gitea/workflows/deploy-garage.yml @@ -0,0 +1,18 @@ +name: Deploy garage + +on: + push: + branches: [main] + paths: + - 'docker/garage/**' + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Deploy via SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh -o StrictHostKeyChecking=accept-new -i ~/.ssh/deploy_key deploy@${{ vars.DEPLOY_HOST }} deploy-garage diff --git a/.gitea/workflows/provision.yml b/.gitea/workflows/provision.yml new file mode 100644 index 0000000..c4bc655 --- /dev/null +++ b/.gitea/workflows/provision.yml @@ -0,0 +1,18 @@ +name: Provision server + +on: + push: + branches: [main] + paths: + - 'scripts/configure.sh' + +jobs: + provision: + runs-on: ubuntu-latest + steps: + - name: Provision via SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh -o StrictHostKeyChecking=accept-new -i ~/.ssh/deploy_key deploy@${{ vars.DEPLOY_HOST }} provision diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0286305..13d8a7c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -7,7 +7,7 @@ The production application server. Runs Rocky Linux 9 on Vultr. - Hosts all app containers and the nginx reverse proxy via Docker Compose -- Provisioned by `setup.sh` in this repo +- Provisioned by `bootstrap.sh` + `configure.sh` in this repo - This repo is cloned to `/opt/hantim` - The `deploy` user receives SSH commands from CI to trigger deploys - Certbot runs on the host (not in Docker) and manages Let's Encrypt certs @@ -25,13 +25,13 @@ A separate server running the Gitea instance. Provides: Bitwarden free tier is used as the secret store. Stores: -- `hantim-git-crypt-key` -- git-crypt symmetric key (Secure Note, base64) - `hantim-ci-registry-push` -- Gitea API token for registry access (Secure Note) - `hantim-server-deploy` -- deploy user SSH key pair (SSH Key) - `hantim-new-app-script` -- Gitea API token for creating repos (Secure Note) - `hantim-vultr-api-key` -- Vultr API key for DNS management (Secure Note) +- `argento-garage` -- Garage rpc_secret, argento_node_id, S3 keys (Secure Note) -The Bitwarden CLI (`bw`) is used in `setup.sh` (on the server) and +The Bitwarden CLI (`bw`) is used in `bootstrap.sh` (on the server) and `new-app.sh` (on dev machines) to fetch secrets at runtime. ## Repos @@ -44,15 +44,21 @@ This repo. Contains server provisioning, Docker Compose configs, nginx configs, deploy scripts, and Gitea Actions workflows. ``` -setup.sh # Server provisioning (idempotent) -scripts/deploy.sh # Receives deploy + cert commands via SSH -scripts/new-app.sh # Scaffolds + deploys a new app (single command) +scripts/bootstrap.sh # First-time server setup (manual, uses Bitwarden) +scripts/configure.sh # Idempotent server config (CI-safe) +scripts/deploy.sh # Receives deploy + cert + provision commands via SSH +scripts/new-app.sh # Scaffolds + deploys a new static site (single command) +scripts/new-service.sh # Scaffolds a new Docker Compose service docker/nginx/ # nginx reverse proxy compose.yml # nginx:alpine container nginx.conf # Main nginx config conf.d/.conf # Per-app server blocks docker//compose.yml # Per-app compose files +docker/garage/ # Garage S3-compatible object storage + compose.yml + garage.toml.template .gitea/workflows/deploy-.yml # Per-app deploy workflows +.gitea/workflows/provision.yml # Triggers configure.sh on setup changes ``` ### static-site-template @@ -111,6 +117,15 @@ Developer pushes to hantim-server -> (nginx only): test config with nginx -t before applying ``` +### Setup change (push to hantim-server) + +``` +Developer pushes change to scripts/configure.sh + -> provision.yml triggers + -> SSH to deploy@hantim as provision + -> deploy.sh: git pull, configure.sh (firewall, certbot, start services) +``` + ## Conventions - **App name = domain** (e.g., `hcsuzuki.net`) diff --git a/CLAUDE.md b/CLAUDE.md index 2e53bac..c2b6c57 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,12 +9,15 @@ Actions workflows. ## Key files -- `setup.sh` -- Server provisioning (idempotent, run as root) -- `scripts/deploy.sh` -- SSH-triggered deploy + cert issuance (deploy-* and cert-* commands) -- `scripts/new-app.sh` -- Single-command new app (DNS + Gitea repo + cert + deploy + verify) +- `scripts/bootstrap.sh` -- First-time server setup (manual, uses Bitwarden) +- `scripts/configure.sh` -- Idempotent server config (firewall, certbot, start services; CI-safe) +- `scripts/deploy.sh` -- SSH-triggered deploy + cert issuance + provision (deploy-*, cert-*, provision commands) +- `scripts/new-app.sh` -- Single-command new static site (DNS + Gitea repo + cert + deploy + verify) +- `scripts/new-service.sh` -- Scaffold a new Docker Compose service (compose + workflow) - `docker/nginx/` -- Reverse proxy (nginx:alpine + certbot on host) - `docker//compose.yml` -- Per-app compose files - `.gitea/workflows/deploy-.yml` -- Per-app deploy workflows +- `.gitea/workflows/provision.yml` -- Triggers configure.sh on setup changes ## Conventions @@ -28,11 +31,11 @@ Actions workflows. ## Secrets Stored in Bitwarden (free tier), fetched via `bw` CLI: -- `hantim-git-crypt-key` -- Secure Note (base64) - `hantim-ci-registry-push` -- Secure Note (Gitea registry token) - `hantim-server-deploy` -- SSH Key (deploy user key pair) - `hantim-new-app-script` -- Secure Note (Gitea API token for creating repos) - `hantim-vultr-api-key` -- Secure Note (Vultr API key for DNS management) +- `argento-garage` -- Secure Note (Garage rpc_secret, argento_node_id, S3 keys) Gitea org-level (`hantim`) secrets/variables: - `DEPLOY_HOST` -- Variable (server IP) @@ -41,16 +44,21 @@ Gitea org-level (`hantim`) secrets/variables: ## Working with this repo -### Adding a new app +### Adding a new static site Run `./scripts/new-app.sh ` from dev machine. Handles everything: DNS, Gitea repo, SSL cert, commit/push, build trigger, and verification. See USECASES.md for full details. +### Adding a new Docker service + +Run `./scripts/new-service.sh ` to scaffold the compose file and deploy +workflow, then edit the compose file and commit. + ### Editing scripts All shell scripts use `set -euo pipefail`. Keep all steps idempotent. -`setup.sh` must be safe to re-run on an existing server. +`configure.sh` must be safe to re-run from CI. ### Deploy flow @@ -60,6 +68,9 @@ App repo push -> `build.yml` builds image, pushes to registry, SSHes to server Infrastructure push (this repo) -> `deploy-.yml` triggers on path match -> same `deploy.sh` flow. nginx deploys test config with `nginx -t` first. +Setup change push -> `provision.yml` triggers -> `deploy.sh provision` +-> `configure.sh` (firewall, certbot, start services). + ## Build and test No build step. No tests. Verify changes by: diff --git a/README.md b/README.md index 2fd0584..bf7e19b 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,15 @@ docker/ compose.yml timothykim.net/ # timothykim.net static site compose.yml + garage/ # Garage S3-compatible object storage + compose.yml + garage.toml.template scripts/ - deploy.sh # Deploy + cert issuance script (called via SSH) - new-app.sh # Scaffolding script to add a new app -setup.sh # Server provisioning script (idempotent) + bootstrap.sh # First-time server setup (manual, uses Bitwarden) + configure.sh # Idempotent server config (CI-safe) + deploy.sh # Deploy + cert issuance + provision (called via SSH) + new-app.sh # Scaffolding script to add a new static site + new-service.sh # Scaffolding script to add a new Docker service .gitea/workflows/ # Per-app deploy workflows triggered by path changes ``` @@ -28,23 +33,26 @@ On a fresh Rocky Linux 9 install: ```bash dnf install -y git git clone https://git.timothykim.net/hantim/hantim-server.git /opt/hantim -bash /opt/hantim/setup.sh +bash /opt/hantim/scripts/bootstrap.sh ``` -`setup.sh` is fully automated and idempotent. It: +`bootstrap.sh` runs once manually. It: -1. Installs system dependencies (git-crypt, jq, certbot, Node.js 20, Docker) +1. Installs system dependencies (jq, certbot, Node.js 20, Docker) 2. Installs and authenticates the Bitwarden CLI -3. Unlocks git-crypt using a key stored in Bitwarden -4. Fetches the Docker registry token and deploy SSH public key from Bitwarden -5. Logs into the Gitea Docker registry -6. Creates the `deploy` user with restricted SSH access and sudo -7. Opens firewall ports (HTTP/HTTPS) -8. Issues SSL certificates via certbot for all configured domains -9. Starts all services (nginx first, then all apps) +3. Fetches the Docker registry token and deploy SSH public key from Bitwarden +4. Logs into the Gitea Docker registry +5. Creates the `deploy` user with restricted SSH access and sudo +6. Runs `configure.sh` (firewall, certbot, start services) + +`configure.sh` is idempotent and CI-safe. It: + +1. Opens firewall ports (HTTP, HTTPS, Garage RPC) +2. Sets up certbot and issues SSL certificates for all configured domains +3. Starts all services (nginx first, then all apps) **Prerequisites**: DNS for all configured domains must point to the server -before running setup. +before running bootstrap. ## Adding a new app @@ -85,18 +93,17 @@ git add . && git commit -m "initial content" && git push ## Restoring from backup -Run the same three commands as provisioning. `setup.sh` is idempotent: +Run the same three commands as provisioning. `bootstrap.sh` is idempotent: - Existing packages are skipped - Existing certs are skipped (or re-issued if the server is new) - All services are started ## Secrets -The following are stored in Bitwarden and fetched automatically by `setup.sh`: +The following are stored in Bitwarden and fetched automatically by `bootstrap.sh`: | Bitwarden item | Type | Purpose | |---|---|---| -| `hantim-git-crypt-key` | Secure Note | git-crypt symmetric key (base64) | | `hantim-ci-registry-push` | Secure Note | Gitea token for Docker registry | | `hantim-server-deploy` | SSH Key | Deploy user's SSH key | diff --git a/USECASES.md b/USECASES.md index de8e0dd..56cbf3d 100644 --- a/USECASES.md +++ b/USECASES.md @@ -7,22 +7,22 @@ Set up a fresh Rocky Linux 9 server from scratch. ```bash dnf install -y git git clone https://git.timothykim.net/hantim/hantim-server.git /opt/hantim -bash /opt/hantim/setup.sh +bash /opt/hantim/scripts/bootstrap.sh ``` **What happens:** -- Installs all dependencies (git-crypt, jq, certbot, Node.js 20, Docker, bw CLI) -- Fetches secrets from Bitwarden (git-crypt key, registry token, deploy SSH key) -- Unlocks git-crypt, logs into Docker registry +- Installs all dependencies (jq, certbot, Node.js 20, Docker, bw CLI) +- Fetches secrets from Bitwarden (registry token, deploy SSH key) +- Logs into Docker registry - Creates deploy user with restricted SSH + sudo -- Opens firewall ports (HTTP/HTTPS) -- Issues SSL certs for all domains in `docker/nginx/conf.d/` -- Starts nginx and all app containers +- Runs `configure.sh`: + - Opens firewall ports (HTTP, HTTPS, Garage RPC) + - Issues SSL certs for all domains in `docker/nginx/conf.d/` + - Starts nginx and all app containers **Prerequisites:** - DNS for all configured domains must point to the server -- Bitwarden vault must contain: `hantim-git-crypt-key`, `hantim-ci-registry-push`, - `hantim-server-deploy` +- Bitwarden vault must contain: `hantim-ci-registry-push`, `hantim-server-deploy` - Docker images must exist in the Gitea registry (or apps will start on next push) ## 2. Restore from backup @@ -30,7 +30,7 @@ bash /opt/hantim/setup.sh Identical to provisioning a new server. Run the same three commands on a fresh server. -- `setup.sh` is idempotent -- safe to re-run +- `bootstrap.sh` and `configure.sh` are idempotent -- safe to re-run - SSL certs are re-issued automatically (Let's Encrypt) - Docker images are pulled from the Gitea registry - No data migration needed for stateless static sites @@ -123,16 +123,19 @@ git push running nginx is untouched 4. Runs `docker compose up -d` and reloads nginx -## 7. Re-run setup on existing server +## 7. Re-run configure on existing server -Safe to do at any time: +Safe to do at any time, manually or via CI: ```bash cd /opt/hantim git pull -bash setup.sh +bash scripts/configure.sh ``` +Or push a change to `scripts/configure.sh` — the `provision.yml` workflow +triggers automatically. + - All steps are idempotent - Existing certs are skipped - Brief nginx downtime (seconds) while certbot checks run diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100644 index 0000000..3e2e2f2 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,96 @@ +#!/bin/bash +set -euo pipefail + +# First-time server setup for a fresh Rocky Linux 9 (or compatible) install. +# Run as root after cloning the repo to /opt/hantim. +# Calls configure.sh at the end. +# +# Usage: +# 1. dnf install -y git +# 2. git clone https://git.timothykim.net/hantim/hantim-server.git /opt/hantim +# 3. bash /opt/hantim/scripts/bootstrap.sh +# + +REPO_DIR="/opt/hantim" + +# --- Prerequisites --- + +if [ "$(id -u)" -ne 0 ]; then + echo "ERROR: Not running as root." + exit 1 +fi + +if [ "$(dirname "$(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 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 + +# --- Fetch secrets from Bitwarden --- + +echo "==> Fetching secrets from 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" +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 < Running configure.sh..." +bash "$REPO_DIR/scripts/configure.sh" diff --git a/scripts/configure.sh b/scripts/configure.sh new file mode 100644 index 0000000..28988be --- /dev/null +++ b/scripts/configure.sh @@ -0,0 +1,76 @@ +#!/bin/bash +set -euo pipefail + +# Idempotent server configuration. Safe to run from CI or manually. +# No secrets required — only configures firewall, certbot, and starts services. + +REPO_DIR="/opt/hantim" + +if [ "$(id -u)" -ne 0 ]; then + echo "ERROR: Not running as root." + exit 1 +fi + +echo "==> Making scripts executable..." +chmod +x "$REPO_DIR/scripts/"*.sh + +echo "==> Opening firewall ports..." +firewall-cmd --permanent --add-service=http +firewall-cmd --permanent --add-service=https +firewall-cmd --permanent --add-port=3901/tcp +firewall-cmd --reload + +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." diff --git a/scripts/deploy.sh b/scripts/deploy.sh index e746551..d18de4e 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -32,6 +32,13 @@ NGINXCONF exit 0 fi +if [ "$CMD" = "provision" ]; then + cd /opt/hantim + git pull + bash scripts/configure.sh + exit 0 +fi + if ! [[ "$CMD" =~ ^deploy-[a-zA-Z0-9._-]+$ ]]; then echo "Unknown command: $CMD" exit 1 diff --git a/scripts/new-service.sh b/scripts/new-service.sh new file mode 100644 index 0000000..1521356 --- /dev/null +++ b/scripts/new-service.sh @@ -0,0 +1,66 @@ +#!/bin/bash +set -euo pipefail + +if [ -z "${1:-}" ]; then + echo "Usage: ./scripts/new-service.sh " + echo " Example: ./scripts/new-service.sh garage" + echo "" + echo "Creates docker//compose.yml and .gitea/workflows/deploy-.yml" + exit 1 +fi + +APP="$1" + +if ! [[ "$APP" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then + echo "Error: name must be alphanumeric (hyphens, dots, underscores allowed)." + exit 1 +fi + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +if [ -d "$REPO_ROOT/docker/$APP" ]; then + echo "Error: docker/$APP already exists." + exit 1 +fi + +echo "Creating docker/$APP/compose.yml..." +mkdir -p "$REPO_ROOT/docker/$APP" +cat > "$REPO_ROOT/docker/$APP/compose.yml" < "$REPO_ROOT/.gitea/workflows/deploy-$APP.yml" <<'OUTER' +name: Deploy APP_PLACEHOLDER + +on: + push: + branches: [main] + paths: + - 'docker/APP_PLACEHOLDER/**' + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Deploy via SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh -o StrictHostKeyChecking=accept-new -i ~/.ssh/deploy_key deploy@${{ vars.DEPLOY_HOST }} deploy-APP_PLACEHOLDER +OUTER +sed -i "s/APP_PLACEHOLDER/$APP/g" "$REPO_ROOT/.gitea/workflows/deploy-$APP.yml" + +echo "Done. Edit docker/$APP/compose.yml, then commit and push." diff --git a/setup.sh b/setup.sh deleted file mode 100755 index 1dd2542..0000000 --- a/setup.sh +++ /dev/null @@ -1,166 +0,0 @@ -#!/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 "==> Opening firewall ports..." -firewall-cmd --permanent --add-service=http -firewall-cmd --permanent --add-service=https -firewall-cmd --reload - -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."