5c38b0631a
- 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
190 lines
5.0 KiB
Bash
Executable File
190 lines
5.0 KiB
Bash
Executable File
#!/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 <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)."
|
|
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
|
|
|
|
# --- 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
|
|
services:
|
|
app:
|
|
image: git.timothykim.net/hantim/$APP:latest
|
|
restart: unless-stopped
|
|
container_name: $CONTAINER_NAME
|
|
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 "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. 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"
|