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. The production application server. Runs Rocky Linux 9 on Vultr.
- Hosts all app containers and the nginx reverse proxy via Docker Compose - 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` - This repo is cloned to `/opt/hantim`
- The `deploy` user receives SSH commands from CI to trigger deploys - 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 - 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: 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-ci-registry-push` -- Gitea API token for registry access (Secure Note)
- `hantim-server-deploy` -- deploy user SSH key pair (SSH Key) - `hantim-server-deploy` -- deploy user SSH key pair (SSH Key)
- `hantim-new-app-script` -- Gitea API token for creating repos (Secure Note) - `hantim-new-app-script` -- Gitea API token for creating repos (Secure Note)
- `hantim-vultr-api-key` -- Vultr API key for DNS management (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. `new-app.sh` (on dev machines) to fetch secrets at runtime.
## Repos ## Repos
@@ -44,15 +44,21 @@ This repo. Contains server provisioning, Docker Compose configs, nginx
configs, deploy scripts, and Gitea Actions workflows. configs, deploy scripts, and Gitea Actions workflows.
``` ```
setup.sh # Server provisioning (idempotent) scripts/bootstrap.sh # First-time server setup (manual, uses Bitwarden)
scripts/deploy.sh # Receives deploy + cert commands via SSH scripts/configure.sh # Idempotent server config (CI-safe)
scripts/new-app.sh # Scaffolds + deploys a new app (single command) 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 docker/nginx/ # nginx reverse proxy
compose.yml # nginx:alpine container compose.yml # nginx:alpine container
nginx.conf # Main nginx config nginx.conf # Main nginx config
conf.d/<domain>.conf # Per-app server blocks conf.d/<domain>.conf # Per-app server blocks
docker/<domain>/compose.yml # Per-app compose files 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/deploy-<app>.yml # Per-app deploy workflows
.gitea/workflows/provision.yml # Triggers configure.sh on setup changes
``` ```
### static-site-template ### static-site-template
@@ -111,6 +117,15 @@ Developer pushes to hantim-server
-> (nginx only): test config with nginx -t before applying -> (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 ## Conventions
- **App name = domain** (e.g., `hcsuzuki.net`) - **App name = domain** (e.g., `hcsuzuki.net`)
+17 -6
View File
@@ -9,12 +9,15 @@ Actions workflows.
## Key files ## Key files
- `setup.sh` -- Server provisioning (idempotent, run as root) - `scripts/bootstrap.sh` -- First-time server setup (manual, uses Bitwarden)
- `scripts/deploy.sh` -- SSH-triggered deploy + cert issuance (deploy-* and cert-* commands) - `scripts/configure.sh` -- Idempotent server config (firewall, certbot, start services; CI-safe)
- `scripts/new-app.sh` -- Single-command new app (DNS + Gitea repo + cert + deploy + verify) - `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/nginx/` -- Reverse proxy (nginx:alpine + certbot on host)
- `docker/<domain>/compose.yml` -- Per-app compose files - `docker/<domain>/compose.yml` -- Per-app compose files
- `.gitea/workflows/deploy-<app>.yml` -- Per-app deploy workflows - `.gitea/workflows/deploy-<app>.yml` -- Per-app deploy workflows
- `.gitea/workflows/provision.yml` -- Triggers configure.sh on setup changes
## Conventions ## Conventions
@@ -28,11 +31,11 @@ Actions workflows.
## Secrets ## Secrets
Stored in Bitwarden (free tier), fetched via `bw` CLI: 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-ci-registry-push` -- Secure Note (Gitea registry token)
- `hantim-server-deploy` -- SSH Key (deploy user key pair) - `hantim-server-deploy` -- SSH Key (deploy user key pair)
- `hantim-new-app-script` -- Secure Note (Gitea API token for creating repos) - `hantim-new-app-script` -- Secure Note (Gitea API token for creating repos)
- `hantim-vultr-api-key` -- Secure Note (Vultr API key for DNS management) - `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: Gitea org-level (`hantim`) secrets/variables:
- `DEPLOY_HOST` -- Variable (server IP) - `DEPLOY_HOST` -- Variable (server IP)
@@ -41,16 +44,21 @@ Gitea org-level (`hantim`) secrets/variables:
## Working with this repo ## Working with this repo
### Adding a new app ### Adding a new static site
Run `./scripts/new-app.sh <domain>` from dev machine. Handles everything: Run `./scripts/new-app.sh <domain>` from dev machine. Handles everything:
DNS, Gitea repo, SSL cert, commit/push, build trigger, and verification. DNS, Gitea repo, SSL cert, commit/push, build trigger, and verification.
See USECASES.md for full details. 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 ### Editing scripts
All shell scripts use `set -euo pipefail`. Keep all steps idempotent. 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 ### 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 Infrastructure push (this repo) -> `deploy-<app>.yml` triggers on path match
-> same `deploy.sh` flow. nginx deploys test config with `nginx -t` first. -> 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 ## Build and test
No build step. No tests. Verify changes by: No build step. No tests. Verify changes by:
+24 -17
View File
@@ -14,10 +14,15 @@ docker/
compose.yml compose.yml
timothykim.net/ # timothykim.net static site timothykim.net/ # timothykim.net static site
compose.yml compose.yml
garage/ # Garage S3-compatible object storage
compose.yml
garage.toml.template
scripts/ scripts/
deploy.sh # Deploy + cert issuance script (called via SSH) bootstrap.sh # First-time server setup (manual, uses Bitwarden)
new-app.sh # Scaffolding script to add a new app configure.sh # Idempotent server config (CI-safe)
setup.sh # Server provisioning script (idempotent) 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 .gitea/workflows/ # Per-app deploy workflows triggered by path changes
``` ```
@@ -28,23 +33,26 @@ On a fresh Rocky Linux 9 install:
```bash ```bash
dnf install -y git dnf install -y git
git clone https://git.timothykim.net/hantim/hantim-server.git /opt/hantim 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 2. Installs and authenticates the Bitwarden CLI
3. Unlocks git-crypt using a key stored in Bitwarden 3. Fetches the Docker registry token and deploy SSH public key from Bitwarden
4. Fetches the Docker registry token and deploy SSH public key from Bitwarden 4. Logs into the Gitea Docker registry
5. Logs into the Gitea Docker registry 5. Creates the `deploy` user with restricted SSH access and sudo
6. Creates the `deploy` user with restricted SSH access and sudo 6. Runs `configure.sh` (firewall, certbot, start services)
7. Opens firewall ports (HTTP/HTTPS)
8. Issues SSL certificates via certbot for all configured domains `configure.sh` is idempotent and CI-safe. It:
9. Starts all services (nginx first, then all apps)
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 **Prerequisites**: DNS for all configured domains must point to the server
before running setup. before running bootstrap.
## Adding a new app ## Adding a new app
@@ -85,18 +93,17 @@ git add . && git commit -m "initial content" && git push
## Restoring from backup ## 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 packages are skipped
- Existing certs are skipped (or re-issued if the server is new) - Existing certs are skipped (or re-issued if the server is new)
- All services are started - All services are started
## Secrets ## 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 | | 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-ci-registry-push` | Secure Note | Gitea token for Docker registry |
| `hantim-server-deploy` | SSH Key | Deploy user's SSH key | | `hantim-server-deploy` | SSH Key | Deploy user's SSH key |
+14 -11
View File
@@ -7,22 +7,22 @@ Set up a fresh Rocky Linux 9 server from scratch.
```bash ```bash
dnf install -y git dnf install -y git
git clone https://git.timothykim.net/hantim/hantim-server.git /opt/hantim 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:** **What happens:**
- Installs all dependencies (git-crypt, jq, certbot, Node.js 20, Docker, bw CLI) - Installs all dependencies (jq, certbot, Node.js 20, Docker, bw CLI)
- Fetches secrets from Bitwarden (git-crypt key, registry token, deploy SSH key) - Fetches secrets from Bitwarden (registry token, deploy SSH key)
- Unlocks git-crypt, logs into Docker registry - Logs into Docker registry
- Creates deploy user with restricted SSH + sudo - Creates deploy user with restricted SSH + sudo
- Opens firewall ports (HTTP/HTTPS) - Runs `configure.sh`:
- Opens firewall ports (HTTP, HTTPS, Garage RPC)
- Issues SSL certs for all domains in `docker/nginx/conf.d/` - Issues SSL certs for all domains in `docker/nginx/conf.d/`
- Starts nginx and all app containers - Starts nginx and all app containers
**Prerequisites:** **Prerequisites:**
- DNS for all configured domains must point to the server - DNS for all configured domains must point to the server
- Bitwarden vault must contain: `hantim-git-crypt-key`, `hantim-ci-registry-push`, - Bitwarden vault must contain: `hantim-ci-registry-push`, `hantim-server-deploy`
`hantim-server-deploy`
- Docker images must exist in the Gitea registry (or apps will start on next push) - Docker images must exist in the Gitea registry (or apps will start on next push)
## 2. Restore from backup ## 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 Identical to provisioning a new server. Run the same three commands on a
fresh server. 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) - SSL certs are re-issued automatically (Let's Encrypt)
- Docker images are pulled from the Gitea registry - Docker images are pulled from the Gitea registry
- No data migration needed for stateless static sites - No data migration needed for stateless static sites
@@ -123,16 +123,19 @@ git push
running nginx is untouched running nginx is untouched
4. Runs `docker compose up -d` and reloads nginx 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 ```bash
cd /opt/hantim cd /opt/hantim
git pull 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 - All steps are idempotent
- Existing certs are skipped - Existing certs are skipped
- Brief nginx downtime (seconds) while certbot checks run - 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 exit 0
fi 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 if ! [[ "$CMD" =~ ^deploy-[a-zA-Z0-9._-]+$ ]]; then
echo "Unknown command: $CMD" echo "Unknown command: $CMD"
exit 1 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."