initial commit
Deploy nginx / deploy (push) Failing after 1s
Deploy timothykim.net / deploy (push) Failing after 1s

This commit is contained in:
2026-03-12 21:16:04 -04:00
commit a3fb085fdb
51 changed files with 573 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
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
+19
View File
@@ -0,0 +1,19 @@
name: Deploy nginx
on:
push:
branches: [main]
paths:
- 'docker/nginx/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: deploy
key: ${{ secrets.DEPLOY_KEY }}
script: deploy-nginx
+19
View File
@@ -0,0 +1,19 @@
name: Deploy timothykim.net
on:
push:
branches: [main]
paths:
- 'docker/timothykim.net/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: deploy
key: ${{ secrets.DEPLOY_KEY }}
script: deploy-timothykim
+3
View File
@@ -0,0 +1,3 @@
containerd/
vultr/
docker/nginx/data/logs/
+89
View File
@@ -0,0 +1,89 @@
# hantim-server
Server provisioning and app management for the hantim webserver. This repo
lives at `/opt` on the server and contains Docker Compose configs, deploy
scripts, 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
scripts/
deploy-dispatch.sh # SSH command dispatcher (routes to per-app deploy scripts)
deploy-nginx.sh # Deploy script for nginx
deploy-timothykim.sh # Deploy script for timothykim.net
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
```
## Initial server setup
On a fresh Rocky Linux 9 (or compatible) install:
```bash
dnf install -y git git-crypt
git clone <repo-url> /opt
git-crypt unlock /path/to/git-crypt-key
/opt/setup.sh
```
`setup.sh` installs Docker, creates a `deploy` user, sets up the shared Docker
network, and starts all services. See the script for the full list of post-setup
steps (registry login, Nginx Proxy Manager config, etc.).
## How deploys work
Each app has three components:
1. **Deploy script** (`scripts/deploy-<app>.sh`) -- pulls the latest repo
changes, then runs `docker compose pull && up -d` for that app.
2. **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.
3. **SSH dispatcher** (`scripts/deploy-dispatch.sh`) -- the `deploy` user's
`authorized_keys` is locked to this script via a `command=` directive. It
reads `SSH_ORIGINAL_COMMAND` to route to the correct per-app deploy script.
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.
## Adding a new app
Run the scaffolding script:
```bash
./scripts/new-app.sh <app-name>
```
This creates:
- `docker/<app-name>/compose.yml` -- a starter compose file on the `shared`
network
- `scripts/deploy-<app-name>.sh` -- the deploy script
- `.gitea/workflows/deploy-<app-name>.yml` -- the Gitea Actions workflow
- A new case in `scripts/deploy-dispatch.sh`
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.
## 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 secrets must be configured in the Gitea repo settings for
deploys to work:
- `DEPLOY_HOST` -- the server's IP or hostname
- `DEPLOY_KEY` -- the SSH private key for the `deploy` user
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
View File
@@ -0,0 +1 @@
../../archive/npm-1/cert1.pem
+1
View File
@@ -0,0 +1 @@
../../archive/npm-1/chain1.pem
+1
View File
@@ -0,0 +1 @@
../../archive/npm-1/fullchain1.pem
+1
View File
@@ -0,0 +1 @@
../../archive/npm-1/privkey1.pem
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
../../archive/npm-2/cert1.pem
+1
View File
@@ -0,0 +1 @@
../../archive/npm-2/chain1.pem
+1
View File
@@ -0,0 +1 @@
../../archive/npm-2/fullchain1.pem
+1
View File
@@ -0,0 +1 @@
../../archive/npm-2/privkey1.pem
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
../../archive/npm-3/cert1.pem
+1
View File
@@ -0,0 +1 @@
../../archive/npm-3/chain1.pem
+1
View File
@@ -0,0 +1 @@
../../archive/npm-3/fullchain1.pem
+1
View File
@@ -0,0 +1 @@
../../archive/npm-3/privkey1.pem
Binary file not shown.
Binary file not shown.
Binary file not shown.
+17
View File
@@ -0,0 +1,17 @@
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
networks:
- shared
ports:
- '80:80'
- '443:443'
- '81:81'
volumes:
- ./data:/data
- ./certs:/etc/letsencrypt
networks:
shared:
name: shared
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,20 @@
# ------------------------------------------------------------
# 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;
}
}
+97
View File
@@ -0,0 +1,97 @@
# ------------------------------------------------------------
# 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;
}
+98
View File
@@ -0,0 +1,98 @@
# ------------------------------------------------------------
# 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;
}
+10
View File
@@ -0,0 +1,10 @@
services:
static:
image: git.timothykim.net/timothykim/static:latest
restart: unless-stopped
networks:
- shared
networks:
shared:
external: true
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
set -euo pipefail
case "$SSH_ORIGINAL_COMMAND" in
deploy-nginx) /opt/scripts/deploy-nginx.sh ;;
deploy-timothykim) /opt/scripts/deploy-timothykim.sh ;;
*)
echo "Unknown command: $SSH_ORIGINAL_COMMAND"
echo "Available: deploy-nginx, deploy-timothykim"
exit 1
;;
esac
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
cd /opt && git pull && cd docker/nginx && docker compose pull && docker compose up -d
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
cd /opt && git pull && cd docker/timothykim.net && docker compose pull && docker compose up -d
+79
View File
@@ -0,0 +1,79 @@
#!/bin/bash
set -euo pipefail
if [ -z "${1:-}" ]; then
echo "Usage: ./scripts/new-app.sh <app-name>"
echo "Example: ./scripts/new-app.sh my-blog"
exit 1
fi
APP="$1"
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: git.timothykim.net/timothykim/$APP:latest
restart: unless-stopped
networks:
- shared
networks:
shared:
external: true
EOF
echo "Creating scripts/deploy-$APP.sh..."
cat > "$REPO_ROOT/scripts/deploy-$APP.sh" <<EOF
#!/bin/bash
cd /opt && git pull && cd docker/$APP && docker compose pull && docker compose up -d
EOF
chmod +x "$REPO_ROOT/scripts/deploy-$APP.sh"
echo "Adding $APP to deploy-dispatch.sh..."
sed -i "/^ \*)$/i\\ deploy-$APP) /opt/scripts/deploy-$APP.sh ;;" \
"$REPO_ROOT/scripts/deploy-dispatch.sh"
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
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: deploy
key: ${{ secrets.DEPLOY_KEY }}
script: deploy-APP_PLACEHOLDER
OUTER
sed -i "s/APP_PLACEHOLDER/$APP/g" "$REPO_ROOT/.gitea/workflows/deploy-$APP.yml"
echo ""
echo "Done! Files created:"
echo " - docker/$APP/compose.yml"
echo " - scripts/deploy-$APP.sh"
echo " - .gitea/workflows/deploy-$APP.yml"
echo " - Updated scripts/deploy-dispatch.sh"
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"
+90
View File
@@ -0,0 +1,90 @@
#!/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/.
#
# Usage:
# 1. dnf install -y git git-crypt
# 2. git clone <repo-url> /opt
# 3. git-crypt unlock /path/to/git-crypt-key
# 4. /opt/setup.sh
#
errors=()
if [ "$(id -u)" -ne 0 ]; then
errors+=("Not running as root. Please run this script as root.")
fi
if ! command -v git &>/dev/null; then
errors+=("git is not installed. Run: dnf install -y git git-crypt")
fi
if ! command -v git-crypt &>/dev/null; then
errors+=("git-crypt is not installed. Run: dnf install -y git git-crypt")
fi
if [ "$(pwd)" != "/opt" ] && [ "$(dirname "$(readlink -f "$0")")" != "/opt" ]; then
errors+=("Repo does not appear to be cloned to /opt. Run: git clone <repo-url> /opt")
fi
if git-crypt status &>/dev/null && git-crypt status 2>/dev/null | grep -q '^\*\*\*ENCRYPTED\*\*\*'; then
errors+=("git-crypt has not been unlocked. Run: git-crypt unlock /path/to/git-crypt-key")
fi
if [ ${#errors[@]} -gt 0 ]; then
echo "ERROR: Prerequisites not met."
echo ""
echo "Please complete the initial setup before running this script:"
echo " 1. dnf install -y git git-crypt"
echo " 2. git clone <repo-url> /opt"
echo " 3. git-crypt unlock /path/to/git-crypt-key"
echo " 4. /opt/setup.sh"
echo ""
echo "Issues found:"
for err in "${errors[@]}"; do
echo " - $err"
done
exit 1
fi
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
systemctl enable --now docker
echo "==> Creating deploy user..."
useradd -r -s /usr/sbin/nologin deploy
usermod -aG docker deploy
echo "==> Setting up deploy SSH key..."
mkdir -p /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
if [ ! -f /home/deploy/.ssh/authorized_keys ]; then
echo "WARNING: Add the CI public key manually:"
echo ' command="/opt/scripts/deploy-dispatch.sh",no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 <KEY>'
echo " into /home/deploy/.ssh/authorized_keys"
fi
chown -R deploy:deploy /home/deploy/.ssh
echo "==> Making scripts executable..."
chmod +x /opt/scripts/*.sh
echo "==> Creating shared Docker network..."
docker network create shared || true
echo "==> Logging into Gitea registry..."
echo "Run manually: docker login git.timothykim.net"
echo "==> Starting services..."
cd /opt/docker/nginx && docker compose up -d
cd /opt/docker/timothykim.net && docker compose up -d
echo "==> Done. Don't forget to:"
echo " 1. Add the deploy SSH public key to /home/deploy/.ssh/authorized_keys"
echo " 2. Log into the Gitea Docker registry: docker login git.timothykim.net"
echo " 3. Configure Nginx Proxy Manager at http://<server-ip>:81"
echo " 4. Restore NPM data/certs backup if migrating"