move dev scripts to tools/, migrate to bws, use example.com in docs
Provision server / provision (push) Successful in 7s

- Move new-app.sh, new-service.sh, remove-app.sh from scripts/ to tools/
- Migrate new-app.sh and remove-app.sh from bw to bws
- Replace real domains with example.com in documentation and help text
This commit is contained in:
2026-03-19 11:10:14 -04:00
parent 5a8d9a8aa0
commit 037b78734f
8 changed files with 127 additions and 109 deletions
+8 -7
View File
@@ -59,8 +59,9 @@ configs, deploy scripts, and Gitea Actions workflows.
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
tools/new-app.sh # Scaffolds + deploys a new static site (run from dev machine)
tools/new-service.sh # Scaffolds a new Docker Compose service (run from dev machine)
tools/remove-app.sh # Removes a static site (run from dev machine)
docker/nginx/ # nginx reverse proxy
compose.yml # nginx:alpine container
nginx.conf # Main nginx config
@@ -82,7 +83,7 @@ workflow that builds/pushes a Docker image and triggers a deploy.
Uses Gitea template variables (`${REPO_NAME}`) so the image tag and deploy
command are automatically set to the repo name.
### App repos (e.g., timothykim.net)
### App repos (e.g., example.com)
Each app is its own repo created from `static-site-template`. Contains the
app source code and a `build.yml` workflow.
@@ -96,7 +97,7 @@ app source code and a `build.yml` workflow.
|
nginx (port 80/443)
| |
timothykim_net hcsuzuki_net ...
example_com example_org ...
(shared Docker network)
```
@@ -111,7 +112,7 @@ app source code and a `build.yml` workflow.
### App code change (push to app repo)
```
Developer pushes to app repo (e.g., timothykim.net)
Developer pushes to app repo (e.g., example.com)
-> build.yml workflow runs on Gitea runner
-> docker build + push to git.timothykim.net registry
-> SSH to deploy@hantim as deploy-<domain>
@@ -140,9 +141,9 @@ Developer pushes change to scripts/configure.sh
## Conventions
- **App name = domain** (e.g., `hcsuzuki.net`)
- **App name = domain** (e.g., `example.com`)
- **Container name** = domain with dots replaced by underscores (e.g.,
`hcsuzuki_net`)
`example_com`)
- **Cert name** = config filename without `.conf` (stored at `/etc/letsencrypt/live/<name>/`)
- **One domain per conf file** -- `deploy.sh` derives the cert name from the conf
filename and collects all `server_name` values for that cert. Putting unrelated
+8 -7
View File
@@ -12,8 +12,9 @@ Actions workflows.
- `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)
- `tools/new-app.sh` -- Single-command new static site (DNS + Gitea repo + cert + deploy + verify)
- `tools/new-service.sh` -- Scaffold a new Docker Compose service (compose + workflow)
- `tools/remove-app.sh` -- Remove a static site (local files + Gitea repo)
- `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
@@ -21,8 +22,8 @@ Actions workflows.
## Conventions
- **App name = domain** (e.g., `hcsuzuki.net`)
- **Container name** = domain with dots replaced by underscores (e.g., `hcsuzuki_net`)
- **App name = domain** (e.g., `example.com`)
- **Container name** = domain with dots replaced by underscores (e.g., `example_com`)
- **Image** = `git.timothykim.net/hantim/<domain>:latest`
- **Cert name** = config filename without `.conf` (at `/etc/letsencrypt/live/<name>/`)
- **One domain per conf file** -- deploy.sh derives cert name from filename
@@ -51,13 +52,13 @@ Gitea org-level (`hantim`) secrets/variables:
### Adding a new static site
Run `./scripts/new-app.sh <domain>` from dev machine. Handles everything:
Run `./tools/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
Run `./tools/new-service.sh <name>` to scaffold the compose file and deploy
workflow, then edit the compose file and commit.
### Editing scripts
@@ -85,6 +86,6 @@ No build step. No tests. Verify changes by:
## Commit style
- Short, lowercase commit messages (e.g., "add hcsuzuki.net", "fix deploy command")
- Short, lowercase commit messages (e.g., "add example.com", "fix deploy command")
- Do NOT include "Co-Authored-By" lines
- Commit only when explicitly asked
+12 -10
View File
@@ -10,9 +10,9 @@ scripts, nginx configs, and Gitea Actions workflows for each app.
docker/
nginx/ # Reverse proxy (nginx:alpine) + SSL config
nginx.conf # Main nginx config
conf.d/ # Per-app server blocks (e.g. timothykim.net.conf)
conf.d/ # Per-app server blocks (e.g. example.com.conf)
compose.yml
timothykim.net/ # timothykim.net static site
<domain>/ # Per-app static site (e.g. example.com)
compose.yml
garage/ # Garage S3-compatible object storage
compose.yml
@@ -21,8 +21,10 @@ scripts/
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
tools/
new-app.sh # Add a new static site (run from dev machine)
new-service.sh # Add a new Docker service (run from dev machine)
remove-app.sh # Remove a static site (run from dev machine)
.gitea/workflows/ # Per-app deploy workflows triggered by path changes
```
@@ -59,19 +61,19 @@ bash /opt/hantim/scripts/bootstrap.sh
## Adding a new app
```bash
./scripts/new-app.sh <domain>
./tools/new-app.sh <domain>
```
Example: `./scripts/new-app.sh hcsuzuki.net`
Example: `./tools/new-app.sh example.com`
This single command handles everything: creates DNS records on Vultr, creates
the Gitea repo from `static-site-template`, generates all config files, issues
the SSL cert (zero downtime), commits and pushes, triggers the first build,
and verifies the site is live.
**Prerequisites:** domain nameservers pointed to Vultr, `bw`/`jq`/`dig`
installed. Uses the deploy SSH key from Bitwarden -- no personal server
access needed. See USECASES.md for full details.
**Prerequisites:** domain nameservers pointed to Vultr, `bws`/`jq`/`dig`
installed. Uses the deploy SSH key from Bitwarden Secrets Manager -- no
personal server access needed. See USECASES.md for full details.
After the site is live, clone the app repo and customize it:
@@ -84,7 +86,7 @@ git add . && git commit -m "initial content" && git push
## Deploying changes
**App code change** (push to an app repo like `timothykim.net`):
**App code change** (push to an app repo like `example.com`):
- The app's `build.yml` builds the Docker image, pushes to the registry, and
SSHes into the server to trigger a deploy
+24 -24
View File
@@ -41,52 +41,53 @@ fresh server.
From your dev machine (single command):
```bash
./scripts/new-app.sh hcsuzuki.net
./tools/new-app.sh example.com
```
**Prerequisites:**
- Bitwarden vault must contain `hantim-new-app-script`, `hantim-vultr-api-key`,
and `hantim-server-deploy`
- Bitwarden Secrets Manager must contain `hantim-new-app-script`,
`hantim-vultr-api-key`, and `hantim-deploy-ssh-private-key`
- Domain nameservers must be pointed to Vultr (configured on directnic.com)
- Dependencies: `bw`, `jq`, `dig`
- Dependencies: `bws`, `jq`, `dig`
- `BWS_ACCESS_TOKEN` env var or token saved at `~/.config/hantim/bws-token`
**What happens (fully automated):**
1. Resolves server IP from `hantim.net`
2. Fetches Gitea API token, Vultr API key, and deploy SSH key from Bitwarden
2. Fetches Gitea API token, Vultr API key, and deploy SSH key from Bitwarden Secrets Manager
3. Creates DNS zone on Vultr (if needed) and A records for bare + www
4. Waits for DNS to propagate
5. Creates Gitea repo `hantim/hcsuzuki.net` from `static-site-template`
5. Creates Gitea repo `hantim/example.com` from `static-site-template`
6. Creates local files:
- `docker/hcsuzuki.net/compose.yml` (container name: `hcsuzuki_net`)
- `.gitea/workflows/deploy-hcsuzuki.net.yml`
- `docker/nginx/conf.d/hcsuzuki.net.conf` (HTTP->HTTPS, bare->www, proxy)
- `docker/example.com/compose.yml` (container name: `example_com`)
- `.gitea/workflows/deploy-example.com.yml`
- `docker/nginx/conf.d/example.com.conf` (HTTP->HTTPS, bare->www, proxy)
7. SSHes to server as deploy user, issues SSL cert via webroot (zero downtime)
8. Commits and pushes hantim-server (triggers deploy)
9. Triggers initial build of the app repo via Gitea API
10. Polls `https://www.hcsuzuki.net` until it responds (up to 3 minutes)
10. Polls `https://www.example.com` until it responds (up to 3 minutes)
After the site is live, clone the app repo and customize:
```bash
git clone git@git.timothykim.net:hantim/hcsuzuki.net.git
cd hcsuzuki.net
git clone git@git.timothykim.net:hantim/example.com.git
cd example.com
# edit static/index.html, etc.
git add . && git commit -m "initial content" && git push
```
## 4. Update app code
Push a change to the app repo (e.g., `timothykim.net`):
Push a change to the app repo (e.g., `example.com`):
```bash
cd timothykim.net
cd example.com
# make changes
git add . && git commit -m "update content" && git push
```
**What happens:**
1. Gitea Actions runs `build.yml`: builds Docker image, pushes to registry
2. SSHes into server, runs `deploy-timothykim.net`
2. SSHes into server, runs `deploy-example.com`
3. `deploy.sh` pulls latest hantim-server, pulls new image, restarts container
No manual steps needed.
@@ -96,14 +97,14 @@ No manual steps needed.
Edit a compose file in this repo and push:
```bash
vim docker/timothykim.net/compose.yml
git add docker/timothykim.net/compose.yml
git commit -m "update timothykim.net config"
vim docker/example.com/compose.yml
git add docker/example.com/compose.yml
git commit -m "update example.com config"
git push
```
**What happens:**
1. `deploy-timothykim.yml` workflow triggers (path match)
1. `deploy-example.com.yml` workflow triggers (path match)
2. `deploy.sh` pulls latest code, pulls image, runs `docker compose up -d`
## 6. Update nginx config
@@ -111,8 +112,8 @@ git push
Edit an nginx config in this repo and push:
```bash
vim docker/nginx/conf.d/timothykim.net.conf
git add docker/nginx/conf.d/timothykim.net.conf
vim docker/nginx/conf.d/example.com.conf
git add docker/nginx/conf.d/example.com.conf
git commit -m "update nginx config"
git push
```
@@ -127,9 +128,8 @@ git push
5. Runs `docker compose up -d` and reloads nginx
**Note:** Each conf file must contain server blocks for only one domain. The
cert name is derived from the filename (e.g. `hcsuzuki.net.conf` -> cert
`hcsuzuki.net`). Redirect-only domains like `hcsuzukiviolin.com` get their
own conf file.
cert name is derived from the filename (e.g. `example.com.conf` -> cert
`example.com`). Redirect-only domains get their own conf file.
## 7. Re-run configure on existing server
+1 -1
View File
@@ -38,7 +38,7 @@ 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)
# Derive cert name from config filename (e.g. example.com.conf -> example.com)
cert_name=$(basename "$conf" .conf)
domains=$(grep -oP 'server_name\s+\K[^;]+' "$conf" | tr ' ' '\n' | sort -u)
if [ -z "$domains" ]; then
+40 -35
View File
@@ -17,10 +17,10 @@ spin_wait() {
}
if [ -z "${1:-}" ]; then
echo "Usage: ./scripts/new-app.sh <domain>"
echo " Example: ./scripts/new-app.sh hcsuzuki.net"
echo "Usage: ./tools/new-app.sh <domain>"
echo " Example: ./tools/new-app.sh example.com"
echo ""
echo "Requires: bw, jq, dig"
echo "Requires: bws, jq, dig"
exit 1
fi
@@ -47,9 +47,9 @@ fi
# --- Check dependencies ---
if ! command -v bw &>/dev/null; then
echo "Error: Bitwarden CLI (bw) is not installed."
echo " npm install -g @bitwarden/cli"
if ! command -v bws &>/dev/null; then
echo "Error: Bitwarden Secrets Manager CLI (bws) is not installed."
echo " https://github.com/bitwarden/sdk-sm/releases"
exit 1
fi
@@ -65,41 +65,46 @@ if ! command -v dig &>/dev/null; then
exit 1
fi
# --- Get secrets (cached in file, or fetch from Bitwarden) ---
# --- Get bws access token ---
SECRETS_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/hantim-secrets"
BWS_TOKEN_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hantim/bws-token"
if [ -f "$SECRETS_CACHE" ]; then
echo "Using cached secrets."
# shellcheck source=/dev/null
source "$SECRETS_CACHE"
GITEA_TOKEN="$HANTIM_GITEA_TOKEN"
VULTR_API_KEY="$HANTIM_VULTR_API_KEY"
DEPLOY_KEY="$HANTIM_DEPLOY_KEY"
if [ -z "${BWS_ACCESS_TOKEN:-}" ]; then
if [ -f "$BWS_TOKEN_FILE" ]; then
BWS_ACCESS_TOKEN=$(cat "$BWS_TOKEN_FILE")
else
if bw status 2>/dev/null | grep -q '"status":"unauthenticated"'; then
echo "Log in to Bitwarden:"
bw login
echo "==> Bitwarden Secrets Manager access token required."
echo " Generate one for your machine account at:"
echo " https://vault.bitwarden.com/#/sm/machine-accounts"
read -rp " Paste access token: " BWS_ACCESS_TOKEN
if [ -z "$BWS_ACCESS_TOKEN" ]; then
echo "ERROR: Access token cannot be empty."
exit 1
fi
if bw status 2>/dev/null | grep -q '"status":"locked"'; then
BW_SESSION=$(bw unlock --raw)
export BW_SESSION
mkdir -p "$(dirname "$BWS_TOKEN_FILE")"
echo "$BWS_ACCESS_TOKEN" > "$BWS_TOKEN_FILE"
chmod 600 "$BWS_TOKEN_FILE"
fi
bw sync --session "${BW_SESSION:-}"
GITEA_TOKEN=$(bw get notes hantim-new-app-script --session "${BW_SESSION:-}")
VULTR_API_KEY=$(bw get notes hantim-vultr-api-key --session "${BW_SESSION:-}")
DEPLOY_KEY=$(bw get item hantim-server-deploy --session "${BW_SESSION:-}" | jq -r '.sshKey.privateKey')
mkdir -p "$(dirname "$SECRETS_CACHE")"
cat > "$SECRETS_CACHE" <<CACHE
HANTIM_GITEA_TOKEN='$(echo "$GITEA_TOKEN" | sed "s/'/'\\\\''/g")'
HANTIM_VULTR_API_KEY='$(echo "$VULTR_API_KEY" | sed "s/'/'\\\\''/g")'
HANTIM_DEPLOY_KEY='$(echo "$DEPLOY_KEY" | sed "s/'/'\\\\''/g")'
CACHE
chmod 600 "$SECRETS_CACHE"
fi
export BWS_ACCESS_TOKEN
# --- Fetch secrets from Bitwarden Secrets Manager ---
bws_get() {
local name="$1"
local value
value=$(bws secret list 2>/dev/null | jq -r --arg name "$name" '.[] | select(.key == $name) | .value')
if [ -z "$value" ]; then
echo "ERROR: Secret '$name' not found in Bitwarden Secrets Manager." >&2
exit 1
fi
echo "$value"
}
echo "==> Fetching secrets from Bitwarden Secrets Manager..."
GITEA_TOKEN=$(bws_get "hantim-new-app-script")
VULTR_API_KEY=$(bws_get "hantim-vultr-api-key")
DEPLOY_KEY=$(bws_get "hantim-deploy-ssh-private-key")
DEPLOY_KEY_FILE=$(mktemp)
echo "$DEPLOY_KEY" > "$DEPLOY_KEY_FILE"
@@ -2,8 +2,8 @@
set -euo pipefail
if [ -z "${1:-}" ]; then
echo "Usage: ./scripts/new-service.sh <name>"
echo " Example: ./scripts/new-service.sh garage"
echo "Usage: ./tools/new-service.sh <name>"
echo " Example: ./tools/new-service.sh garage"
echo ""
echo "Creates docker/<name>/compose.yml and .gitea/workflows/deploy-<name>.yml"
exit 1
+29 -20
View File
@@ -5,8 +5,8 @@ GITEA_URL="https://git.timothykim.net"
GITEA_ORG="hantim"
if [ -z "${1:-}" ]; then
echo "Usage: ./scripts/remove-app.sh <domain>"
echo " Example: ./scripts/remove-app.sh hcsuzuki.net"
echo "Usage: ./tools/remove-app.sh <domain>"
echo " Example: ./tools/remove-app.sh example.com"
echo ""
echo "Removes: local files (compose, workflow, nginx conf), Gitea repo."
echo "Does NOT remove: DNS records, SSL certs."
@@ -23,33 +23,42 @@ fi
# --- Check dependencies ---
if ! command -v bw &>/dev/null; then
echo "Error: Bitwarden CLI (bw) is not installed."
if ! command -v bws &>/dev/null; then
echo "Error: Bitwarden Secrets Manager CLI (bws) is not installed."
echo " https://github.com/bitwarden/sdk-sm/releases"
exit 1
fi
# --- Get secrets (cached in file, or fetch from Bitwarden) ---
# --- Get bws access token ---
SECRETS_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/hantim-secrets"
BWS_TOKEN_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hantim/bws-token"
if [ -f "$SECRETS_CACHE" ]; then
echo "Using cached secrets."
# shellcheck source=/dev/null
source "$SECRETS_CACHE"
GITEA_TOKEN="$HANTIM_GITEA_TOKEN"
if [ -z "${BWS_ACCESS_TOKEN:-}" ]; then
if [ -f "$BWS_TOKEN_FILE" ]; then
BWS_ACCESS_TOKEN=$(cat "$BWS_TOKEN_FILE")
else
if bw status 2>/dev/null | grep -q '"status":"unauthenticated"'; then
echo "Log in to Bitwarden:"
bw login
echo "==> Bitwarden Secrets Manager access token required."
echo " Generate one for your machine account at:"
echo " https://vault.bitwarden.com/#/sm/machine-accounts"
read -rp " Paste access token: " BWS_ACCESS_TOKEN
if [ -z "$BWS_ACCESS_TOKEN" ]; then
echo "ERROR: Access token cannot be empty."
exit 1
fi
if bw status 2>/dev/null | grep -q '"status":"locked"'; then
BW_SESSION=$(bw unlock --raw)
export BW_SESSION
mkdir -p "$(dirname "$BWS_TOKEN_FILE")"
echo "$BWS_ACCESS_TOKEN" > "$BWS_TOKEN_FILE"
chmod 600 "$BWS_TOKEN_FILE"
fi
fi
export BWS_ACCESS_TOKEN
bw sync --session "${BW_SESSION:-}"
GITEA_TOKEN=$(bw get notes hantim-new-app-script --session "${BW_SESSION:-}")
# --- Fetch secrets from Bitwarden Secrets Manager ---
echo "==> Fetching secrets from Bitwarden Secrets Manager..."
GITEA_TOKEN=$(bws secret list 2>/dev/null | jq -r '.[] | select(.key == "hantim-new-app-script") | .value')
if [ -z "$GITEA_TOKEN" ]; then
echo "ERROR: Secret 'hantim-new-app-script' not found in Bitwarden Secrets Manager."
exit 1
fi
# --- Delete Gitea repo ---