replace NPM with vanilla nginx + certbot
Deploy nginx / deploy (push) Failing after 3s
Deploy timothykim.net / deploy (push) Successful in 3s

- nginx:alpine replaces nginx-proxy-manager
- certbot on host with standalone issuance and auto-renewal
- per-site nginx configs in conf.d/ (HTTP->HTTPS, bare->www)
- container names use underscores (timothykim_net)
- setup.sh: automated cert issuance, dynamic app startup
- deploy.sh: nginx -t guard, graceful image-not-found
- new-app.sh: Gitea API repo creation, nginx conf generation
- add ARCHITECTURE.md, USECASES.md, CLAUDE.md
- remove old NPM data/certs from tracking
This commit is contained in:
2026-03-16 00:55:13 -04:00
parent 31c6235726
commit 5c38b0631a
52 changed files with 681 additions and 307 deletions
+79 -57
View File
@@ -2,24 +2,28 @@
Server provisioning and app management for the hantim webserver. This repo
lives at `/opt/hantim` on the server and contains Docker Compose configs, deploy
scripts, and Gitea Actions workflows for each app.
scripts, nginx configs, and Gitea Actions workflows for each app.
## Repo structure
```
docker/ # One directory per app, each with a compose.yml
nginx/ # Nginx Proxy Manager (reverse proxy + TLS)
timothykim.net/ # timothykim.net static site
docker/
nginx/ # Reverse proxy (nginx:alpine) + SSL config
nginx.conf # Main nginx config
conf.d/ # Per-app server blocks (e.g. timothykim.net.conf)
compose.yml
timothykim.net/ # timothykim.net static site
compose.yml
scripts/
deploy.sh # Deploy script (validates command, git pull, docker compose up)
new-app.sh # Scaffolding script to add a new app
setup.sh # One-time server provisioning script
.gitea/workflows/ # Per-app deploy workflows triggered by path changes
deploy.sh # Deploy script (validates command, git pull, docker compose up)
new-app.sh # Scaffolding script to add a new app
setup.sh # Server provisioning script (idempotent)
.gitea/workflows/ # Per-app deploy workflows triggered by path changes
```
## Initial server setup
## Provisioning a new server
On a fresh Rocky Linux 9 (or compatible) install:
On a fresh Rocky Linux 9 install:
```bash
dnf install -y git
@@ -27,69 +31,87 @@ git clone https://git.timothykim.net/hantim/hantim-server.git /opt/hantim
bash /opt/hantim/setup.sh
```
`setup.sh` handles everything else: installs git-crypt and the Bitwarden CLI,
fetches the git-crypt key from your Bitwarden vault (stored as a base64-encoded
secure note named `hantim-git-crypt-key`), unlocks the repo, installs Docker,
creates the `deploy` user, and starts all services.
`setup.sh` is fully automated and idempotent. It:
To save the git-crypt key to Bitwarden (one-time, from your dev machine):
1. Installs system dependencies (git-crypt, 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. Issues SSL certificates via certbot for all configured domains
8. Starts all services (nginx first, then all apps)
```bash
base64 /path/to/git-crypt-key
# Save the output as a Bitwarden secure note named "hantim-git-crypt-key"
```
## How deploys work
Each app has two components:
1. **Gitea Actions workflow** (`.gitea/workflows/deploy-<app>.yml`) -- triggers
on pushes to `main` when files under `docker/<app>/` change. SSHes into the
server as the `deploy` user to trigger the deploy.
2. **Deploy script** (`scripts/deploy.sh`) -- the `deploy` user's
`authorized_keys` is locked to this script via a `command=` directive. It
validates the command, pulls the latest repo changes, and runs
`docker compose pull && up -d` for that app.
This means:
- Pushing a change to `docker/nginx/compose.yml` only deploys nginx.
- Pushing a change to `scripts/` or `setup.sh` does not trigger any deploy.
- Each app deploys independently.
**Prerequisites**: DNS for all configured domains must point to the server
before running setup.
## Adding a new app
Run the scaffolding script:
```bash
./scripts/new-app.sh <app-name>
./scripts/new-app.sh <domain>
```
Example: `./scripts/new-app.sh hcsuzuki.net`
This creates:
- `docker/<app-name>/compose.yml` -- a starter compose file on the `shared`
network
- `.gitea/workflows/deploy-<app-name>.yml` -- the Gitea Actions workflow
- A Gitea repo (`hantim/<domain>`) from the `static-site-template`
- `docker/<domain>/compose.yml` -- app compose file
- `.gitea/workflows/deploy-<domain>.yml` -- deploy workflow
- `docker/nginx/conf.d/<domain>.conf` -- nginx proxy config (HTTP->HTTPS,
bare domain->www)
After running the script:
1. Edit `docker/<app-name>/compose.yml` to fit your app (image, ports,
volumes, environment variables, etc.).
2. Commit and push to `main`.
3. Configure the proxy host in Nginx Proxy Manager to route traffic to the new
service.
1. Issue the SSL cert on the server (**before** pushing):
```bash
docker stop nginx
certbot certonly --standalone --non-interactive --agree-tos \
--register-unsafely-without-email --cert-name <domain> \
-d <domain> -d www.<domain>
docker start nginx
```
2. Commit and push this repo to deploy
3. Clone the new app repo, customize it, and push to build/deploy
## Deploying changes
**App code change** (push to an app repo like `timothykim.net`):
- The app's `build.yml` builds the Docker image, pushes to the registry, and
SSHes into the server to trigger a deploy
**Compose or nginx config change** (push to this repo):
- Gitea Actions workflows trigger based on which `docker/<app>/` paths changed
- `deploy.sh` pulls the latest code and runs `docker compose up -d`
- For nginx deploys, the config is tested before applying to prevent downtime
## Restoring from backup
Run the same three commands as provisioning. `setup.sh` is idempotent:
- Existing packages are skipped
- Existing certs are skipped (or re-issued if the server is new)
- All services are started
## Secrets
Nginx Proxy Manager certs and Let's Encrypt account keys are encrypted with
git-crypt (see `.gitattributes`). You need the git-crypt key to unlock them.
The following are stored in Bitwarden and fetched automatically by `setup.sh`:
The following must be configured in the `hantim` Gitea org settings and are
shared across all repos:
| 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 |
Variables (Settings > Actions > Variables):
- `DEPLOY_HOST` -- the server's IP or hostname
The following are configured in the `hantim` Gitea org settings:
Secrets (Settings > Actions > Secrets):
- `DEPLOY_SSH_KEY` -- the SSH private key for the `deploy` user
- `CI_REGISTRY_TOKEN` -- Gitea personal access token for Docker registry login
| Name | Type | Purpose |
|---|---|---|
| `DEPLOY_HOST` | Variable | Server IP or hostname |
| `DEPLOY_SSH_KEY` | Secret | SSH private key for the deploy user |
| `CI_REGISTRY_TOKEN` | Secret | Gitea token for Docker registry login |
The following is stored in Bitwarden and used by `new-app.sh`:
| Bitwarden item | Type | Purpose |
|---|---|---|
| `hantim-new-app-script` | Secure Note | Gitea API token for creating repos |