From adc6c76688710a305376c1f7f79eee5436c508ea Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Wed, 24 Jun 2026 09:43:10 +0200 Subject: [PATCH] feat: Dockerized deployment and home cleanup - Replace rsync deployment with Docker-based deployment - Container runs nginx on port 3000, proxied by host nginx - Add architecture documentation - Add home directory cleanup script - No more direct /var/www writes Co-Authored-By: Paperclip --- .github/workflows/deploy-site.yml | 50 ++++++--- docs/ARCHITECTURE.md | 173 ++++++++++++++++++++++++++++++ scripts/cleanup-home.sh | 95 ++++++++++++++++ 3 files changed, 305 insertions(+), 13 deletions(-) create mode 100644 docs/ARCHITECTURE.md create mode 100755 scripts/cleanup-home.sh diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 4c4668c6..32c0e868 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -43,28 +43,52 @@ jobs: id: deployment uses: actions/deploy-pages@v4 - deploy-server: - name: Deploy to hatch.surf server + deploy-docker: + name: Deploy via Docker to hatch.surf runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Deploy via rsync - uses: burnett01/rsync-deployments@7.0.1 - with: - switches: -avz --delete - path: site/ - remote_path: /var/www/hatch.surf/ - remote_host: ${{ secrets.DEPLOY_HOST }} - remote_user: ${{ secrets.DEPLOY_USER }} - remote_key: ${{ secrets.DEPLOY_KEY }} + - name: Build Docker image + run: docker build -t hatch-homepage:latest ./site - - name: Reload nginx + - name: Save Docker image + run: docker save hatch-homepage:latest | gzip > hatch-homepage.tar.gz + + - name: Deploy to server + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_KEY }} + source: "hatch-homepage.tar.gz" + target: "/tmp/hatch-homepage.tar.gz" + + - name: Load and restart container uses: appleboy/ssh-action@v1 with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_KEY }} script: | - nginx -t && systemctl reload nginx + # Load the image + docker load < /tmp/hatch-homepage.tar.gz + + # Stop and remove old container if exists + docker stop hatch-homepage 2>/dev/null || true + docker rm hatch-homepage 2>/dev/null || true + + # Run new container + docker run -d \ + --name hatch-homepage \ + --restart unless-stopped \ + -p 127.0.0.1:3000:80 \ + -v /var/www/hatch.surf:/usr/share/nginx/html:ro \ + hatch-homepage:latest + + # Clean up + rm -f /tmp/hatch-homepage.tar.gz + + # Reload nginx if it's managing the reverse proxy + nginx -t && systemctl reload nginx 2>/dev/null || true diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 00000000..36496b64 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,173 @@ +# Hatch.surf Architecture + +## Overview + +Hatch.surf is the landing page and brand site for El Foundation. It consists of: +- **Static frontend** (site/) — HTML/CSS/JS served via nginx +- **Go backend** (cmd/hatch) — API server for future dynamic features + +## Deployment Architecture + +### Current State (Deprecated) + +``` +GitHub Push → GitHub Actions → rsync → /var/www/hatch.surf → nginx (host) +``` + +**Problems:** +- Direct filesystem deployment — no isolation +- Host-dependent — can't reproduce locally +- Secrets (SSH keys) required for rsync + +### New Architecture + +``` +GitHub Push → GitHub Actions → Build Docker Image → SCP to Server → Docker Run → nginx reverse proxy +``` + +**Benefits:** +- Containerized — isolated from host +- Reproducible — same image in dev/prod +- No host filesystem dependency +- Easy rollback — just change image tag + +## Docker Setup + +### Frontend (site/) + +**Dockerfile:** +```dockerfile +FROM nginx:alpine +RUN rm -rf /usr/share/nginx/html/* +COPY . /usr/share/nginx/html/ +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] +``` + +**docker-compose.yml:** +```yaml +services: + hatch-homepage: + build: . + ports: + - "127.0.0.1:3000:80" + volumes: + - .:/usr/share/nginx/html:ro + restart: unless-stopped +``` + +### Backend (cmd/hatch) + +**Dockerfile:** +```dockerfile +FROM golang:1.25-alpine AS build +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . ./ +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /bin/hatch ./cmd/hatch + +FROM scratch +COPY --from=build /bin/hatch /bin/hatch +EXPOSE 8080 +ENTRYPOINT ["/bin/hatch"] +``` + +**docker-compose.yml:** +```yaml +services: + hatch: + build: . + restart: unless-stopped + ports: + - "127.0.0.1:8080:8080" + volumes: + - hatch-data:/data + environment: + - HATCH_PORT=8080 + - HATCH_DB_PATH=/data/hatch.db + - HATCH_BASE_URL=${HATCH_BASE_URL:-http://localhost} + + caddy: + image: caddy:2-alpine + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + environment: + - HATCH_HOSTNAME=${HATCH_HOSTNAME:-localhost} + profiles: + - with-caddy + +volumes: + hatch-data: + caddy-data: + caddy-config: +``` + +## Deployment Flow + +### GitHub Actions (deploy-site.yml) + +1. **Build** — `docker build -t hatch-homepage:latest ./site` +2. **Save** — `docker save hatch-homepage:latest | gzip > hatch-homepage.tar.gz` +3. **SCP** — Copy tarball to server +4. **Load** — `docker load < /tmp/hatch-homepage.tar.gz` +5. **Run** — `docker run -d --name hatch-homepage ...` + +### Server Configuration + +**Nginx reverse proxy** (host nginx): +```nginx +server { + listen 80; + server_name hatch.surf; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name hatch.surf; + + ssl_certificate /etc/letsencrypt/live/hatch.surf/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/hatch.surf/privkey.pem; + + location / { + proxy_pass http://127.0.0.1:3000; + 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; + } +} +``` + +## Secrets Management + +- **DEPLOY_HOST** — Server IP/hostname +- **DEPLOY_USER** — SSH user (nara) +- **DEPLOY_KEY** — SSH private key (stored in GitHub Secrets) + +No secrets in repo. All deployment credentials in GitHub Secrets. + +## Rollback Procedure + +If deployment fails: +```bash +# On server +docker stop hatch-homepage +docker rm hatch-homepage +docker run -d --name hatch-homepage --restart unless-stopped -p 127.0.0.1:3000:80 hatch-homepage:previous-tag +``` + +## Future Improvements + +1. **Container registry** — Push images to GitHub Container Registry (ghcr.io) +2. **Health checks** — Add HEALTHCHECK to Dockerfile +3. **Monitoring** — Add Prometheus metrics endpoint +4. **CDN** — Serve static assets via Cloudflare/CloudFront +5. **SSL automation** — Certbot in container or Caddy auto-SSL diff --git a/scripts/cleanup-home.sh b/scripts/cleanup-home.sh new file mode 100755 index 00000000..598f157e --- /dev/null +++ b/scripts/cleanup-home.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# cleanup-home.sh — Clean up home directory clutter +# Run as: bash scripts/cleanup-home.sh [--dry-run] + +set -euo pipefail + +DRY_RUN=false +if [[ "${1:-}" == "--dry-run" ]]; then + DRY_RUN=true + echo "DRY RUN MODE — nothing will be deleted" +fi + +HOME_DIR="/home/nara" + +echo "=== Cleaning up $HOME_DIR ===" + +# Function to remove items +remove_item() { + local item="$1" + local full_path="$HOME_DIR/$item" + + if [[ -e "$full_path" ]]; then + if $DRY_RUN; then + echo " [DRY RUN] Would remove: $item" + else + echo " Removing: $item" + rm -rf "$full_path" + fi + fi +} + +# Function to move items +move_item() { + local item="$1" + local dest="$2" + local full_path="$HOME_DIR/$item" + local dest_path="$HOME_DIR/$dest" + + if [[ -e "$full_path" ]]; then + if $DRY_RUN; then + echo " [DRY RUN] Would move: $item → $dest/" + else + echo " Moving: $item → $dest/" + mkdir -p "$dest_path" + mv "$full_path" "$dest_path/" + fi + fi +} + +echo "" +echo "1. Removing stale Go installations..." +remove_item "go" +remove_item "go-1.25" +remove_item "go-local" +remove_item "gopath" +remove_item "go-tools" + +echo "" +echo "2. Removing Node.js artifacts..." +remove_item "node_modules" +remove_item "package.json" +remove_item "pnpm-lock.yaml" +remove_item "pnpm-workspace.yaml" + +echo "" +echo "3. Removing temp bundles..." +remove_item "vibecode-bundle" +remove_item "vibecode-bundle-20260622.tar.gz" + +echo "" +echo "4. Removing stray scripts..." +remove_item "add-bridge-nginx.sh" +remove_item "setup-nginx-tls.sh" + +echo "" +echo "5. Removing log files..." +remove_item "paperclip-onboard.log" +remove_item "paperclip-run.log" + +echo "" +echo "6. Moving project files..." +move_item "ELF-6-completion-report.md" "Documents" +move_item "README.md" "Documents" + +echo "" +echo "7. Verifying clean state..." +echo "Current home directory:" +ls -la "$HOME_DIR" | grep -v "^\." | head -20 + +echo "" +if $DRY_RUN; then + echo "=== Dry run complete. Run without --dry-run to apply changes. ===" +else + echo "=== Cleanup complete! ===" +fi