commit a3fb085fdb200c66fe86f581af8a915636984e9e Author: Timothy Kim Date: Thu Mar 12 21:16:04 2026 -0400 initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..69319bd --- /dev/null +++ b/.gitattributes @@ -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 + diff --git a/.gitea/workflows/deploy-nginx.yml b/.gitea/workflows/deploy-nginx.yml new file mode 100644 index 0000000..c6b36f7 --- /dev/null +++ b/.gitea/workflows/deploy-nginx.yml @@ -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 diff --git a/.gitea/workflows/deploy-timothykim.yml b/.gitea/workflows/deploy-timothykim.yml new file mode 100644 index 0000000..5fdfadc --- /dev/null +++ b/.gitea/workflows/deploy-timothykim.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f0dac19 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +containerd/ +vultr/ +docker/nginx/data/logs/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..922b21a --- /dev/null +++ b/README.md @@ -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 /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-.sh`) -- pulls the latest repo + changes, then runs `docker compose pull && up -d` for that app. +2. **Gitea Actions workflow** (`.gitea/workflows/deploy-.yml`) -- triggers + on pushes to `main` when files under `docker//` 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 +``` + +This creates: + +- `docker//compose.yml` -- a starter compose file on the `shared` + network +- `scripts/deploy-.sh` -- the deploy script +- `.gitea/workflows/deploy-.yml` -- the Gitea Actions workflow +- A new case in `scripts/deploy-dispatch.sh` + +After running the script: + +1. Edit `docker//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 diff --git a/docker/nginx/certs/accounts/acme-v02.api.letsencrypt.org/directory/ba6ee99ad413477b2770a56352d98492/meta.json b/docker/nginx/certs/accounts/acme-v02.api.letsencrypt.org/directory/ba6ee99ad413477b2770a56352d98492/meta.json new file mode 100644 index 0000000..8fc35ff Binary files /dev/null and b/docker/nginx/certs/accounts/acme-v02.api.letsencrypt.org/directory/ba6ee99ad413477b2770a56352d98492/meta.json differ diff --git a/docker/nginx/certs/accounts/acme-v02.api.letsencrypt.org/directory/ba6ee99ad413477b2770a56352d98492/private_key.json b/docker/nginx/certs/accounts/acme-v02.api.letsencrypt.org/directory/ba6ee99ad413477b2770a56352d98492/private_key.json new file mode 100644 index 0000000..ba7ac57 Binary files /dev/null and b/docker/nginx/certs/accounts/acme-v02.api.letsencrypt.org/directory/ba6ee99ad413477b2770a56352d98492/private_key.json differ diff --git a/docker/nginx/certs/accounts/acme-v02.api.letsencrypt.org/directory/ba6ee99ad413477b2770a56352d98492/regr.json b/docker/nginx/certs/accounts/acme-v02.api.letsencrypt.org/directory/ba6ee99ad413477b2770a56352d98492/regr.json new file mode 100644 index 0000000..03cf353 Binary files /dev/null and b/docker/nginx/certs/accounts/acme-v02.api.letsencrypt.org/directory/ba6ee99ad413477b2770a56352d98492/regr.json differ diff --git a/docker/nginx/certs/archive/npm-1/cert1.pem b/docker/nginx/certs/archive/npm-1/cert1.pem new file mode 100644 index 0000000..87291d6 Binary files /dev/null and b/docker/nginx/certs/archive/npm-1/cert1.pem differ diff --git a/docker/nginx/certs/archive/npm-1/chain1.pem b/docker/nginx/certs/archive/npm-1/chain1.pem new file mode 100644 index 0000000..3045de3 Binary files /dev/null and b/docker/nginx/certs/archive/npm-1/chain1.pem differ diff --git a/docker/nginx/certs/archive/npm-1/fullchain1.pem b/docker/nginx/certs/archive/npm-1/fullchain1.pem new file mode 100644 index 0000000..1bcb336 Binary files /dev/null and b/docker/nginx/certs/archive/npm-1/fullchain1.pem differ diff --git a/docker/nginx/certs/archive/npm-1/privkey1.pem b/docker/nginx/certs/archive/npm-1/privkey1.pem new file mode 100644 index 0000000..843f29e Binary files /dev/null and b/docker/nginx/certs/archive/npm-1/privkey1.pem differ diff --git a/docker/nginx/certs/archive/npm-2/cert1.pem b/docker/nginx/certs/archive/npm-2/cert1.pem new file mode 100644 index 0000000..6001c98 Binary files /dev/null and b/docker/nginx/certs/archive/npm-2/cert1.pem differ diff --git a/docker/nginx/certs/archive/npm-2/chain1.pem b/docker/nginx/certs/archive/npm-2/chain1.pem new file mode 100644 index 0000000..cab35f4 Binary files /dev/null and b/docker/nginx/certs/archive/npm-2/chain1.pem differ diff --git a/docker/nginx/certs/archive/npm-2/fullchain1.pem b/docker/nginx/certs/archive/npm-2/fullchain1.pem new file mode 100644 index 0000000..2e7c9cf Binary files /dev/null and b/docker/nginx/certs/archive/npm-2/fullchain1.pem differ diff --git a/docker/nginx/certs/archive/npm-2/privkey1.pem b/docker/nginx/certs/archive/npm-2/privkey1.pem new file mode 100644 index 0000000..9032d66 Binary files /dev/null and b/docker/nginx/certs/archive/npm-2/privkey1.pem differ diff --git a/docker/nginx/certs/archive/npm-3/cert1.pem b/docker/nginx/certs/archive/npm-3/cert1.pem new file mode 100644 index 0000000..8304cf4 Binary files /dev/null and b/docker/nginx/certs/archive/npm-3/cert1.pem differ diff --git a/docker/nginx/certs/archive/npm-3/chain1.pem b/docker/nginx/certs/archive/npm-3/chain1.pem new file mode 100644 index 0000000..3045de3 Binary files /dev/null and b/docker/nginx/certs/archive/npm-3/chain1.pem differ diff --git a/docker/nginx/certs/archive/npm-3/fullchain1.pem b/docker/nginx/certs/archive/npm-3/fullchain1.pem new file mode 100644 index 0000000..74c0ee3 Binary files /dev/null and b/docker/nginx/certs/archive/npm-3/fullchain1.pem differ diff --git a/docker/nginx/certs/archive/npm-3/privkey1.pem b/docker/nginx/certs/archive/npm-3/privkey1.pem new file mode 100644 index 0000000..6473fb8 Binary files /dev/null and b/docker/nginx/certs/archive/npm-3/privkey1.pem differ diff --git a/docker/nginx/certs/live/README b/docker/nginx/certs/live/README new file mode 100644 index 0000000..8bdfa31 Binary files /dev/null and b/docker/nginx/certs/live/README differ diff --git a/docker/nginx/certs/live/npm-1/README b/docker/nginx/certs/live/npm-1/README new file mode 100644 index 0000000..63f542b Binary files /dev/null and b/docker/nginx/certs/live/npm-1/README differ diff --git a/docker/nginx/certs/live/npm-1/cert.pem b/docker/nginx/certs/live/npm-1/cert.pem new file mode 120000 index 0000000..d7e65ed --- /dev/null +++ b/docker/nginx/certs/live/npm-1/cert.pem @@ -0,0 +1 @@ +../../archive/npm-1/cert1.pem \ No newline at end of file diff --git a/docker/nginx/certs/live/npm-1/chain.pem b/docker/nginx/certs/live/npm-1/chain.pem new file mode 120000 index 0000000..8133afd --- /dev/null +++ b/docker/nginx/certs/live/npm-1/chain.pem @@ -0,0 +1 @@ +../../archive/npm-1/chain1.pem \ No newline at end of file diff --git a/docker/nginx/certs/live/npm-1/fullchain.pem b/docker/nginx/certs/live/npm-1/fullchain.pem new file mode 120000 index 0000000..8f20152 --- /dev/null +++ b/docker/nginx/certs/live/npm-1/fullchain.pem @@ -0,0 +1 @@ +../../archive/npm-1/fullchain1.pem \ No newline at end of file diff --git a/docker/nginx/certs/live/npm-1/privkey.pem b/docker/nginx/certs/live/npm-1/privkey.pem new file mode 120000 index 0000000..916bbaf --- /dev/null +++ b/docker/nginx/certs/live/npm-1/privkey.pem @@ -0,0 +1 @@ +../../archive/npm-1/privkey1.pem \ No newline at end of file diff --git a/docker/nginx/certs/live/npm-2/README b/docker/nginx/certs/live/npm-2/README new file mode 100644 index 0000000..63f542b Binary files /dev/null and b/docker/nginx/certs/live/npm-2/README differ diff --git a/docker/nginx/certs/live/npm-2/cert.pem b/docker/nginx/certs/live/npm-2/cert.pem new file mode 120000 index 0000000..711bc75 --- /dev/null +++ b/docker/nginx/certs/live/npm-2/cert.pem @@ -0,0 +1 @@ +../../archive/npm-2/cert1.pem \ No newline at end of file diff --git a/docker/nginx/certs/live/npm-2/chain.pem b/docker/nginx/certs/live/npm-2/chain.pem new file mode 120000 index 0000000..689045b --- /dev/null +++ b/docker/nginx/certs/live/npm-2/chain.pem @@ -0,0 +1 @@ +../../archive/npm-2/chain1.pem \ No newline at end of file diff --git a/docker/nginx/certs/live/npm-2/fullchain.pem b/docker/nginx/certs/live/npm-2/fullchain.pem new file mode 120000 index 0000000..742cc5e --- /dev/null +++ b/docker/nginx/certs/live/npm-2/fullchain.pem @@ -0,0 +1 @@ +../../archive/npm-2/fullchain1.pem \ No newline at end of file diff --git a/docker/nginx/certs/live/npm-2/privkey.pem b/docker/nginx/certs/live/npm-2/privkey.pem new file mode 120000 index 0000000..0c8edcc --- /dev/null +++ b/docker/nginx/certs/live/npm-2/privkey.pem @@ -0,0 +1 @@ +../../archive/npm-2/privkey1.pem \ No newline at end of file diff --git a/docker/nginx/certs/live/npm-3/README b/docker/nginx/certs/live/npm-3/README new file mode 100644 index 0000000..63f542b Binary files /dev/null and b/docker/nginx/certs/live/npm-3/README differ diff --git a/docker/nginx/certs/live/npm-3/cert.pem b/docker/nginx/certs/live/npm-3/cert.pem new file mode 120000 index 0000000..70976c4 --- /dev/null +++ b/docker/nginx/certs/live/npm-3/cert.pem @@ -0,0 +1 @@ +../../archive/npm-3/cert1.pem \ No newline at end of file diff --git a/docker/nginx/certs/live/npm-3/chain.pem b/docker/nginx/certs/live/npm-3/chain.pem new file mode 120000 index 0000000..6f845d5 --- /dev/null +++ b/docker/nginx/certs/live/npm-3/chain.pem @@ -0,0 +1 @@ +../../archive/npm-3/chain1.pem \ No newline at end of file diff --git a/docker/nginx/certs/live/npm-3/fullchain.pem b/docker/nginx/certs/live/npm-3/fullchain.pem new file mode 120000 index 0000000..6a89319 --- /dev/null +++ b/docker/nginx/certs/live/npm-3/fullchain.pem @@ -0,0 +1 @@ +../../archive/npm-3/fullchain1.pem \ No newline at end of file diff --git a/docker/nginx/certs/live/npm-3/privkey.pem b/docker/nginx/certs/live/npm-3/privkey.pem new file mode 120000 index 0000000..0738548 --- /dev/null +++ b/docker/nginx/certs/live/npm-3/privkey.pem @@ -0,0 +1 @@ +../../archive/npm-3/privkey1.pem \ No newline at end of file diff --git a/docker/nginx/certs/renewal/npm-1.conf b/docker/nginx/certs/renewal/npm-1.conf new file mode 100644 index 0000000..28a6632 Binary files /dev/null and b/docker/nginx/certs/renewal/npm-1.conf differ diff --git a/docker/nginx/certs/renewal/npm-2.conf b/docker/nginx/certs/renewal/npm-2.conf new file mode 100644 index 0000000..1a51013 Binary files /dev/null and b/docker/nginx/certs/renewal/npm-2.conf differ diff --git a/docker/nginx/certs/renewal/npm-3.conf b/docker/nginx/certs/renewal/npm-3.conf new file mode 100644 index 0000000..81e44a7 Binary files /dev/null and b/docker/nginx/certs/renewal/npm-3.conf differ diff --git a/docker/nginx/compose.yml b/docker/nginx/compose.yml new file mode 100644 index 0000000..8448a69 --- /dev/null +++ b/docker/nginx/compose.yml @@ -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 diff --git a/docker/nginx/data/database.sqlite b/docker/nginx/data/database.sqlite new file mode 100644 index 0000000..4f81acb Binary files /dev/null and b/docker/nginx/data/database.sqlite differ diff --git a/docker/nginx/data/keys.json b/docker/nginx/data/keys.json new file mode 100644 index 0000000..a0d5c42 Binary files /dev/null and b/docker/nginx/data/keys.json differ diff --git a/docker/nginx/data/nginx/default_host/site.conf b/docker/nginx/data/nginx/default_host/site.conf new file mode 100644 index 0000000..6fdc1db --- /dev/null +++ b/docker/nginx/data/nginx/default_host/site.conf @@ -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; + } + +} + diff --git a/docker/nginx/data/nginx/proxy_host/1.conf b/docker/nginx/data/nginx/proxy_host/1.conf new file mode 100644 index 0000000..69499f3 --- /dev/null +++ b/docker/nginx/data/nginx/proxy_host/1.conf @@ -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; +} + diff --git a/docker/nginx/data/nginx/proxy_host/2.conf b/docker/nginx/data/nginx/proxy_host/2.conf new file mode 100644 index 0000000..9f72c31 --- /dev/null +++ b/docker/nginx/data/nginx/proxy_host/2.conf @@ -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; +} + diff --git a/docker/timothykim.net/compose.yml b/docker/timothykim.net/compose.yml new file mode 100644 index 0000000..af3ad15 --- /dev/null +++ b/docker/timothykim.net/compose.yml @@ -0,0 +1,10 @@ +services: + static: + image: git.timothykim.net/timothykim/static:latest + restart: unless-stopped + networks: + - shared + +networks: + shared: + external: true diff --git a/scripts/deploy-dispatch.sh b/scripts/deploy-dispatch.sh new file mode 100644 index 0000000..a7eea7b --- /dev/null +++ b/scripts/deploy-dispatch.sh @@ -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 diff --git a/scripts/deploy-nginx.sh b/scripts/deploy-nginx.sh new file mode 100644 index 0000000..287cca8 --- /dev/null +++ b/scripts/deploy-nginx.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cd /opt && git pull && cd docker/nginx && docker compose pull && docker compose up -d diff --git a/scripts/deploy-timothykim.sh b/scripts/deploy-timothykim.sh new file mode 100755 index 0000000..3466b7b --- /dev/null +++ b/scripts/deploy-timothykim.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cd /opt && git pull && cd docker/timothykim.net && docker compose pull && docker compose up -d diff --git a/scripts/new-app.sh b/scripts/new-app.sh new file mode 100755 index 0000000..a35a768 --- /dev/null +++ b/scripts/new-app.sh @@ -0,0 +1,79 @@ +#!/bin/bash +set -euo pipefail + +if [ -z "${1:-}" ]; then + echo "Usage: ./scripts/new-app.sh " + 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" < "$REPO_ROOT/scripts/deploy-$APP.sh" < "$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" diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..dda8dc5 --- /dev/null +++ b/setup.sh @@ -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 /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 /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 /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 ' + 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://:81" +echo " 4. Restore NPM data/certs backup if migrating" +