split setup.sh into bootstrap.sh and configure.sh
Provision server / provision (push) Failing after 2s

bootstrap.sh handles first-time setup (manual, Bitwarden).
configure.sh handles idempotent config (CI-safe).
Add provision workflow, deploy-garage workflow, new-service.sh.
Remove git-crypt references and empty .gitattributes.
This commit is contained in:
2026-03-18 14:51:19 -04:00
parent 3b4ff1f291
commit 919c5353bc
12 changed files with 359 additions and 209 deletions
-1
View File
@@ -1 +0,0 @@
+18
View File
@@ -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
+18
View File
@@ -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
+21 -6
View File
@@ -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/<domain>.conf # Per-app server blocks
docker/<domain>/compose.yml # Per-app compose files
docker/garage/ # Garage S3-compatible object storage
compose.yml
garage.toml.template
.gitea/workflows/deploy-<app>.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`)
+17 -6
View File
@@ -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/<domain>/compose.yml` -- Per-app compose files
- `.gitea/workflows/deploy-<app>.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 <domain>` 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 <name>` 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-<app>.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:
+24 -17
View File
@@ -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 |
+16 -13
View File
@@ -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
+96
View File
@@ -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 <<SUDOERS
deploy ALL=(root) NOPASSWD: /opt/hantim/scripts/deploy.sh
SUDOERS
chmod 440 /etc/sudoers.d/deploy
# --- Run configure ---
echo "==> Running configure.sh..."
bash "$REPO_DIR/scripts/configure.sh"
+76
View File
@@ -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."
+7
View File
@@ -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
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
set -euo pipefail
if [ -z "${1:-}" ]; then
echo "Usage: ./scripts/new-service.sh <name>"
echo " Example: ./scripts/new-service.sh garage"
echo ""
echo "Creates docker/<name>/compose.yml and .gitea/workflows/deploy-<name>.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" <<EOF
services:
$APP:
image: TODO
restart: unless-stopped
container_name: $APP
networks:
- shared
networks:
shared:
external: true
EOF
echo "Creating .gitea/workflows/deploy-$APP.yml..."
mkdir -p "$REPO_ROOT/.gitea/workflows"
cat > "$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."
-166
View File
@@ -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 <<SUDOERS
deploy ALL=(root) NOPASSWD: /opt/hantim/scripts/deploy.sh
SUDOERS
chmod 440 /etc/sudoers.d/deploy
# --- Start services ---
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 --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."