split setup.sh into bootstrap.sh and configure.sh
Provision server / provision (push) Failing after 2s
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:
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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`)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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."
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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."
|
||||||
@@ -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."
|
|
||||||
Reference in New Issue
Block a user