replace NPM with vanilla nginx + certbot
- 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:
@@ -1,4 +1 @@
|
||||
docker/nginx/certs/** filter=git-crypt diff=git-crypt
|
||||
docker/nginx/data/keys.json filter=git-crypt diff=git-crypt
|
||||
docker/nginx/data/database.sqlite filter=git-crypt diff=git-crypt
|
||||
|
||||
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
containerd/
|
||||
vultr/
|
||||
docker/nginx/data/logs/
|
||||
docker/nginx/data/
|
||||
docker/nginx/certs/
|
||||
docker/nginx/certbot/
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
# Architecture
|
||||
|
||||
## Servers
|
||||
|
||||
### Hantim server (hantim)
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
### Gitea server (git.timothykim.net)
|
||||
|
||||
A separate server running the Gitea instance. Provides:
|
||||
|
||||
- Git hosting for all repos
|
||||
- Docker container registry (`git.timothykim.net/hantim/<app>`)
|
||||
- Gitea Actions CI/CD (runner registered at site level)
|
||||
- Organization-level secrets and variables shared across repos
|
||||
|
||||
### Bitwarden (cloud)
|
||||
|
||||
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)
|
||||
|
||||
The Bitwarden CLI (`bw`) is used in `setup.sh` (on the server) and
|
||||
`new-app.sh` (on dev machines) to fetch secrets at runtime.
|
||||
|
||||
## Repos
|
||||
|
||||
All repos live under the `hantim` organization on Gitea.
|
||||
|
||||
### hantim-server
|
||||
|
||||
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 commands via SSH
|
||||
scripts/new-app.sh # Scaffolds a new app (Gitea repo + local files)
|
||||
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
|
||||
.gitea/workflows/deploy-<app>.yml # Per-app deploy workflows
|
||||
```
|
||||
|
||||
### static-site-template
|
||||
|
||||
A Gitea template repo used by `new-app.sh` to scaffold new static sites.
|
||||
Contains a Dockerfile, nginx config, placeholder HTML, and a `build.yml`
|
||||
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)
|
||||
|
||||
Each app is its own repo created from `static-site-template`. Contains the
|
||||
app source code and a `build.yml` workflow.
|
||||
|
||||
## Network topology
|
||||
|
||||
```
|
||||
Internet
|
||||
|
|
||||
[Hantim Server]
|
||||
|
|
||||
nginx (port 80/443)
|
||||
| |
|
||||
timothykim_net hcsuzuki_net ...
|
||||
(shared Docker network)
|
||||
```
|
||||
|
||||
- nginx is the only container with host port bindings (80, 443)
|
||||
- All app containers are on the `shared` Docker network
|
||||
- nginx proxies `https://www.<domain>` to the app's container by name
|
||||
- The `shared` network is created by the nginx compose file and referenced
|
||||
as `external: true` by app compose files
|
||||
|
||||
## Deploy flow
|
||||
|
||||
### App code change (push to app repo)
|
||||
|
||||
```
|
||||
Developer pushes to app repo (e.g., timothykim.net)
|
||||
-> build.yml workflow runs on Gitea runner
|
||||
-> docker build + push to git.timothykim.net registry
|
||||
-> SSH to deploy@hantim as deploy-<domain>
|
||||
-> deploy.sh: git pull, docker compose pull, docker compose up -d
|
||||
-> (if image not yet in registry, skips gracefully)
|
||||
```
|
||||
|
||||
### Infrastructure change (push to hantim-server)
|
||||
|
||||
```
|
||||
Developer pushes to hantim-server
|
||||
-> deploy-<app>.yml triggers (based on changed paths under docker/<app>/)
|
||||
-> SSH to deploy@hantim as deploy-<app>
|
||||
-> deploy.sh: git pull, docker compose pull, docker compose up -d
|
||||
-> (nginx only): test config with nginx -t before applying
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- **App name = domain** (e.g., `hcsuzuki.net`)
|
||||
- **Container name** = domain with dots replaced by underscores (e.g.,
|
||||
`hcsuzuki_net`)
|
||||
- **Cert name** = domain (stored at `/etc/letsencrypt/live/<domain>/`)
|
||||
- **Image name** = `git.timothykim.net/hantim/<domain>:latest`
|
||||
- All HTTP traffic redirects to `https://www.<domain>`
|
||||
- Bare domain HTTPS redirects to `https://www.<domain>`
|
||||
@@ -0,0 +1,72 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## Project overview
|
||||
|
||||
Server provisioning and app management for the hantim webserver. This repo
|
||||
lives at `/opt/hantim` on the production server (Rocky Linux 9 on Vultr) and
|
||||
manages Docker Compose configs, deploy scripts, nginx configs, and Gitea
|
||||
Actions workflows.
|
||||
|
||||
## Key files
|
||||
|
||||
- `setup.sh` -- Server provisioning (idempotent, run as root)
|
||||
- `scripts/deploy.sh` -- SSH-triggered deploy (validates command, pulls, restarts)
|
||||
- `scripts/new-app.sh` -- Scaffolds a new static site (Gitea repo + compose + workflow + nginx conf)
|
||||
- `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
|
||||
|
||||
## Conventions
|
||||
|
||||
- **App name = domain** (e.g., `hcsuzuki.net`)
|
||||
- **Container name** = domain with dots replaced by underscores (e.g., `hcsuzuki_net`)
|
||||
- **Image** = `git.timothykim.net/hantim/<domain>:latest`
|
||||
- **Cert name** = domain (at `/etc/letsencrypt/live/<domain>/`)
|
||||
- All HTTP redirects to HTTPS; bare domain redirects to `www.<domain>`
|
||||
- The `shared` Docker network is created by nginx compose and used by all apps
|
||||
|
||||
## 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)
|
||||
|
||||
Gitea org-level (`hantim`) secrets/variables:
|
||||
- `DEPLOY_HOST` -- Variable (server IP)
|
||||
- `DEPLOY_SSH_KEY` -- Secret (deploy SSH private key)
|
||||
- `CI_REGISTRY_TOKEN` -- Secret (registry token)
|
||||
|
||||
## Working with this repo
|
||||
|
||||
### Adding a new app
|
||||
|
||||
Run `./scripts/new-app.sh <domain>` from dev machine, then issue SSL cert on
|
||||
server **before** pushing. See USECASES.md for full steps.
|
||||
|
||||
### 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.
|
||||
|
||||
### Deploy flow
|
||||
|
||||
App repo push -> `build.yml` builds image, pushes to registry, SSHes to server
|
||||
-> `deploy.sh` pulls repo, pulls image, restarts container.
|
||||
|
||||
Infrastructure push (this repo) -> `deploy-<app>.yml` triggers on path match
|
||||
-> same `deploy.sh` flow. nginx deploys test config with `nginx -t` first.
|
||||
|
||||
## Build and test
|
||||
|
||||
No build step. No tests. Verify changes by:
|
||||
1. Reading scripts and checking idempotency
|
||||
2. Walking through each use case in USECASES.md
|
||||
3. Cross-checking documentation (README.md, ARCHITECTURE.md, USECASES.md) against scripts
|
||||
|
||||
## Commit style
|
||||
|
||||
- Short, lowercase commit messages (e.g., "add hcsuzuki.net", "fix deploy command")
|
||||
- Do NOT include "Co-Authored-By" lines
|
||||
- Commit only when explicitly asked
|
||||
@@ -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 |
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
# Use Cases
|
||||
|
||||
## 1. Provision a new server
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
**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
|
||||
- Creates deploy user with restricted SSH + sudo
|
||||
- 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`
|
||||
- Docker images must exist in the Gitea registry (or apps will start on next push)
|
||||
|
||||
## 2. Restore from backup
|
||||
|
||||
Identical to provisioning a new server. Run the same three commands on a
|
||||
fresh server.
|
||||
|
||||
- `setup.sh` is 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
|
||||
|
||||
## 3. Add a new static site
|
||||
|
||||
From your dev machine:
|
||||
|
||||
```bash
|
||||
./scripts/new-app.sh hcsuzuki.net
|
||||
```
|
||||
|
||||
Then on the server, issue the SSL cert **before** pushing:
|
||||
|
||||
```bash
|
||||
docker stop nginx
|
||||
certbot certonly --standalone --non-interactive --agree-tos \
|
||||
--register-unsafely-without-email --cert-name hcsuzuki.net \
|
||||
-d hcsuzuki.net -d www.hcsuzuki.net
|
||||
docker start nginx
|
||||
```
|
||||
|
||||
Then commit and push this repo:
|
||||
|
||||
```bash
|
||||
git add docker/hcsuzuki.net/ .gitea/workflows/deploy-hcsuzuki.net.yml docker/nginx/conf.d/hcsuzuki.net.conf
|
||||
git commit -m "add hcsuzuki.net"
|
||||
git push
|
||||
```
|
||||
|
||||
Finally, clone the new app repo, customize it, and push:
|
||||
|
||||
```bash
|
||||
git clone git@git.timothykim.net:hantim/hcsuzuki.net.git
|
||||
cd hcsuzuki.net
|
||||
# edit static/index.html, etc.
|
||||
git add . && git commit -m "initial content" && git push
|
||||
```
|
||||
|
||||
**What the script creates:**
|
||||
- Gitea repo `hantim/hcsuzuki.net` (from `static-site-template`)
|
||||
- `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)
|
||||
|
||||
## 4. Update app code
|
||||
|
||||
Push a change to the app repo (e.g., `timothykim.net`):
|
||||
|
||||
```bash
|
||||
cd timothykim.net
|
||||
# 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`
|
||||
3. `deploy.sh` pulls latest hantim-server, pulls new image, restarts container
|
||||
|
||||
No manual steps needed.
|
||||
|
||||
## 5. Update Docker Compose config
|
||||
|
||||
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"
|
||||
git push
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. `deploy-timothykim.yml` workflow triggers (path match)
|
||||
2. `deploy.sh` pulls latest code, pulls image, runs `docker compose up -d`
|
||||
|
||||
## 6. Update nginx config
|
||||
|
||||
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
|
||||
git commit -m "update nginx config"
|
||||
git push
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. `deploy-nginx.yml` workflow triggers (path match)
|
||||
2. `deploy.sh` pulls latest code
|
||||
3. Tests nginx config with `nginx -t` -- if it fails, deploy aborts and the
|
||||
running nginx is untouched
|
||||
4. Runs `docker compose up -d` and reloads nginx
|
||||
|
||||
## 7. Re-run setup on existing server
|
||||
|
||||
Safe to do at any time:
|
||||
|
||||
```bash
|
||||
cd /opt/hantim
|
||||
git pull
|
||||
bash setup.sh
|
||||
```
|
||||
|
||||
- All steps are idempotent
|
||||
- Existing certs are skipped
|
||||
- Brief nginx downtime (seconds) while certbot checks run
|
||||
- Services are restarted
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
../../archive/npm-1/cert1.pem
|
||||
@@ -1 +0,0 @@
|
||||
../../archive/npm-1/chain1.pem
|
||||
@@ -1 +0,0 @@
|
||||
../../archive/npm-1/fullchain1.pem
|
||||
@@ -1 +0,0 @@
|
||||
../../archive/npm-1/privkey1.pem
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
../../archive/npm-2/cert1.pem
|
||||
@@ -1 +0,0 @@
|
||||
../../archive/npm-2/chain1.pem
|
||||
@@ -1 +0,0 @@
|
||||
../../archive/npm-2/fullchain1.pem
|
||||
@@ -1 +0,0 @@
|
||||
../../archive/npm-2/privkey1.pem
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
../../archive/npm-3/cert1.pem
|
||||
@@ -1 +0,0 @@
|
||||
../../archive/npm-3/chain1.pem
|
||||
@@ -1 +0,0 @@
|
||||
../../archive/npm-3/fullchain1.pem
|
||||
@@ -1 +0,0 @@
|
||||
../../archive/npm-3/privkey1.pem
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,17 +1,18 @@
|
||||
services:
|
||||
app:
|
||||
image: 'jc21/nginx-proxy-manager:latest'
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
restart: unless-stopped
|
||||
container_name: npm
|
||||
container_name: nginx
|
||||
networks:
|
||||
- shared
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
- '81:81'
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./certs:/etc/letsencrypt
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./conf.d:/etc/nginx/conf.d:ro
|
||||
- /etc/letsencrypt:/etc/letsencrypt:ro
|
||||
- ./certbot/www:/var/www/certbot:ro
|
||||
|
||||
networks:
|
||||
shared:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Redirect HTTP to HTTPS, bare domain to www
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name timothykim.net www.timothykim.net;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://www.timothykim.net$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# Redirect bare HTTPS domain to www
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name timothykim.net;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/timothykim.net/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/timothykim.net/privkey.pem;
|
||||
|
||||
return 301 https://www.timothykim.net$request_uri;
|
||||
}
|
||||
|
||||
# Main site
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name www.timothykim.net;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/timothykim.net/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/timothykim.net/privkey.pem;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=63072000; preload" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://timothykim_net:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,20 +0,0 @@
|
||||
# ------------------------------------------------------------
|
||||
# Default Site
|
||||
# ------------------------------------------------------------
|
||||
|
||||
server {
|
||||
listen 80 default;
|
||||
listen [::]:80 default;
|
||||
|
||||
server_name default-host.localhost;
|
||||
access_log /data/logs/default-host_access.log combined;
|
||||
error_log /data/logs/default-host_error.log warn;
|
||||
|
||||
|
||||
include conf.d/include/letsencrypt-acme-challenge.conf;
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# ------------------------------------------------------------
|
||||
# www.timothykim.net
|
||||
# ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
map $scheme $hsts_header {
|
||||
https "max-age=63072000; preload";
|
||||
}
|
||||
|
||||
server {
|
||||
set $forward_scheme http;
|
||||
set $server "static";
|
||||
set $port 80;
|
||||
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
|
||||
|
||||
server_name www.timothykim.net;
|
||||
http2 off;
|
||||
|
||||
|
||||
# Let's Encrypt SSL
|
||||
include conf.d/include/letsencrypt-acme-challenge.conf;
|
||||
include conf.d/include/ssl-cache.conf;
|
||||
include conf.d/include/ssl-ciphers.conf;
|
||||
ssl_certificate /etc/letsencrypt/live/npm-3/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/npm-3/privkey.pem;
|
||||
|
||||
|
||||
|
||||
|
||||
# Asset Caching
|
||||
include conf.d/include/assets.conf;
|
||||
|
||||
|
||||
# Block Exploits
|
||||
include conf.d/include/block-exploits.conf;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Force SSL
|
||||
|
||||
set $trust_forwarded_proto "F";
|
||||
|
||||
include conf.d/include/force-ssl.conf;
|
||||
|
||||
|
||||
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $http_connection;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
|
||||
access_log /data/logs/proxy-host-1_access.log proxy;
|
||||
error_log /data/logs/proxy-host-1_error.log warn;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
location / {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $http_connection;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
|
||||
# Proxy!
|
||||
include conf.d/include/proxy.conf;
|
||||
}
|
||||
|
||||
|
||||
# Custom
|
||||
include /data/nginx/custom/server_proxy[.]conf;
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
# ------------------------------------------------------------
|
||||
# timothykim.net
|
||||
# ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
map $scheme $hsts_header {
|
||||
https "max-age=63072000; preload";
|
||||
}
|
||||
|
||||
server {
|
||||
set $forward_scheme http;
|
||||
set $server "static";
|
||||
set $port 80;
|
||||
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
|
||||
|
||||
server_name timothykim.net;
|
||||
|
||||
http2 on;
|
||||
|
||||
|
||||
# Let's Encrypt SSL
|
||||
include conf.d/include/letsencrypt-acme-challenge.conf;
|
||||
include conf.d/include/ssl-cache.conf;
|
||||
include conf.d/include/ssl-ciphers.conf;
|
||||
ssl_certificate /etc/letsencrypt/live/npm-2/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/npm-2/privkey.pem;
|
||||
|
||||
|
||||
|
||||
|
||||
# Asset Caching
|
||||
include conf.d/include/assets.conf;
|
||||
|
||||
|
||||
# Block Exploits
|
||||
include conf.d/include/block-exploits.conf;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Force SSL
|
||||
|
||||
set $trust_forwarded_proto "F";
|
||||
|
||||
include conf.d/include/force-ssl.conf;
|
||||
|
||||
|
||||
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $http_connection;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
|
||||
access_log /data/logs/proxy-host-2_access.log proxy;
|
||||
error_log /data/logs/proxy-host-2_error.log warn;
|
||||
|
||||
return 301 https://www.timothykim.net$request_uri;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
location / {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $http_connection;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
|
||||
# Proxy!
|
||||
include conf.d/include/proxy.conf;
|
||||
}
|
||||
|
||||
|
||||
# Custom
|
||||
include /data/nginx/custom/server_proxy[.]conf;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
worker_processes auto;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-Frame-Options DENY;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ services:
|
||||
static:
|
||||
image: git.timothykim.net/hantim/static:latest
|
||||
restart: unless-stopped
|
||||
container_name: timothykim-static
|
||||
container_name: timothykim_net
|
||||
networks:
|
||||
- shared
|
||||
|
||||
|
||||
+14
-1
@@ -12,5 +12,18 @@ APP="${CMD#deploy-}"
|
||||
cd /opt/hantim
|
||||
git pull
|
||||
cd "docker/$APP"
|
||||
docker compose pull
|
||||
if ! docker compose pull; then
|
||||
echo "Image not yet available for $APP — skipping. It will deploy when the app repo is first pushed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$APP" = "nginx" ]; then
|
||||
# Test config before applying — a bad config (e.g. missing cert) would take down all sites
|
||||
docker compose run --rm -T nginx nginx -t
|
||||
fi
|
||||
|
||||
docker compose up -d
|
||||
|
||||
if [ "$APP" = "nginx" ]; then
|
||||
docker exec nginx nginx -s reload
|
||||
fi
|
||||
|
||||
+123
-5
@@ -1,13 +1,18 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
GITEA_URL="https://git.timothykim.net"
|
||||
GITEA_ORG="hantim"
|
||||
TEMPLATE_REPO="static-site-template"
|
||||
|
||||
if [ -z "${1:-}" ]; then
|
||||
echo "Usage: ./scripts/new-app.sh <app-name>"
|
||||
echo "Example: ./scripts/new-app.sh my-blog"
|
||||
echo "Usage: ./scripts/new-app.sh <domain>"
|
||||
echo "Example: ./scripts/new-app.sh hcsuzuki.net"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
APP="$1"
|
||||
CONTAINER_NAME="${APP//./_}"
|
||||
|
||||
if ! [[ "$APP" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then
|
||||
echo "Error: app name must be alphanumeric (hyphens, dots, underscores allowed)."
|
||||
@@ -21,6 +26,60 @@ if [ -d "$REPO_ROOT/docker/$APP" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Check dependencies ---
|
||||
|
||||
if ! command -v bw &>/dev/null; then
|
||||
echo "Error: Bitwarden CLI (bw) is not installed."
|
||||
echo " npm install -g @bitwarden/cli"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq &>/dev/null; then
|
||||
echo "Error: jq is not installed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Get Gitea API token from Bitwarden ---
|
||||
|
||||
if bw status 2>/dev/null | grep -q '"status":"unauthenticated"'; then
|
||||
echo "Log in to Bitwarden:"
|
||||
bw login
|
||||
fi
|
||||
|
||||
if bw status 2>/dev/null | grep -q '"status":"locked"'; then
|
||||
BW_SESSION=$(bw unlock --raw)
|
||||
export BW_SESSION
|
||||
fi
|
||||
|
||||
bw sync --session "${BW_SESSION:-}"
|
||||
GITEA_TOKEN=$(bw get notes hantim-new-app-script --session "${BW_SESSION:-}")
|
||||
|
||||
# --- Create Gitea repo from template ---
|
||||
|
||||
echo "Creating Gitea repo $GITEA_ORG/$APP from template $TEMPLATE_REPO..."
|
||||
HTTP_CODE=$(curl -s -o /tmp/new-app-response.json -w "%{http_code}" \
|
||||
-X POST "$GITEA_URL/api/v1/repos/$GITEA_ORG/$TEMPLATE_REPO/generate" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"owner\": \"$GITEA_ORG\",
|
||||
\"name\": \"$APP\",
|
||||
\"git_content\": true,
|
||||
\"labels\": false,
|
||||
\"webhooks\": false
|
||||
}")
|
||||
|
||||
if [ "$HTTP_CODE" != "201" ]; then
|
||||
echo "Error: Failed to create repo (HTTP $HTTP_CODE)"
|
||||
cat /tmp/new-app-response.json
|
||||
rm -f /tmp/new-app-response.json
|
||||
exit 1
|
||||
fi
|
||||
rm -f /tmp/new-app-response.json
|
||||
echo " Created: $GITEA_URL/$GITEA_ORG/$APP"
|
||||
|
||||
# --- Create local files in hantim-server ---
|
||||
|
||||
echo "Creating docker/$APP/compose.yml..."
|
||||
mkdir -p "$REPO_ROOT/docker/$APP"
|
||||
cat > "$REPO_ROOT/docker/$APP/compose.yml" <<EOF
|
||||
@@ -28,6 +87,7 @@ services:
|
||||
app:
|
||||
image: git.timothykim.net/hantim/$APP:latest
|
||||
restart: unless-stopped
|
||||
container_name: $CONTAINER_NAME
|
||||
networks:
|
||||
- shared
|
||||
|
||||
@@ -60,12 +120,70 @@ jobs:
|
||||
OUTER
|
||||
sed -i "s/APP_PLACEHOLDER/$APP/g" "$REPO_ROOT/.gitea/workflows/deploy-$APP.yml"
|
||||
|
||||
echo "Creating docker/nginx/conf.d/$APP.conf..."
|
||||
cat > "$REPO_ROOT/docker/nginx/conf.d/$APP.conf" <<NGINX
|
||||
# Redirect HTTP to HTTPS, bare domain to www
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name $APP www.$APP;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://www.$APP\$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# Redirect bare HTTPS domain to www
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name $APP;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/$APP/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/$APP/privkey.pem;
|
||||
|
||||
return 301 https://www.$APP\$request_uri;
|
||||
}
|
||||
|
||||
# Main site
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name www.$APP;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/$APP/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/$APP/privkey.pem;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=63072000; preload" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://$CONTAINER_NAME:80;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
}
|
||||
}
|
||||
NGINX
|
||||
|
||||
echo ""
|
||||
echo "Done! Files created:"
|
||||
echo " - Gitea repo: $GITEA_URL/$GITEA_ORG/$APP"
|
||||
echo " - docker/$APP/compose.yml"
|
||||
echo " - .gitea/workflows/deploy-$APP.yml"
|
||||
echo " - docker/nginx/conf.d/$APP.conf"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Edit docker/$APP/compose.yml to fit your app"
|
||||
echo " 2. Commit and push to deploy"
|
||||
echo " 3. Configure the proxy host in Nginx Proxy Manager"
|
||||
echo " 1. Issue SSL cert on the server (BEFORE pushing nginx config):"
|
||||
echo " docker stop nginx"
|
||||
echo " certbot certonly --standalone --non-interactive --agree-tos --register-unsafely-without-email --cert-name $APP -d $APP -d www.$APP"
|
||||
echo " docker start nginx"
|
||||
echo " 2. Commit and push hantim-server to deploy"
|
||||
echo " 3. Clone $GITEA_URL/$GITEA_ORG/$APP and customize it"
|
||||
echo " 4. Edit docker/$APP/compose.yml if needed"
|
||||
|
||||
@@ -32,7 +32,8 @@ echo "==> Upgrading system packages..."
|
||||
dnf upgrade -y
|
||||
|
||||
echo "==> Installing dependencies..."
|
||||
dnf install -y git-crypt jq
|
||||
dnf install -y git-crypt jq epel-release
|
||||
dnf install -y certbot
|
||||
dnf module reset -y nodejs
|
||||
dnf module install -y nodejs:20/common
|
||||
|
||||
@@ -104,12 +105,57 @@ chmod 440 /etc/sudoers.d/deploy
|
||||
echo "==> Making scripts executable..."
|
||||
chmod +x "$REPO_DIR/scripts/"*.sh
|
||||
|
||||
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
|
||||
cd "$REPO_DIR/docker/timothykim.net"
|
||||
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. Don't forget to:"
|
||||
echo " 1. Configure Nginx Proxy Manager at http://<server-ip>:81"
|
||||
echo " 2. Restore NPM data/certs backup if migrating"
|
||||
echo "==> Done."
|
||||
|
||||
Reference in New Issue
Block a user