diff --git a/.gitignore b/.gitignore index 393d3883..3a9ae994 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +out.backup.* diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 00000000..3a86f7f5 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,201 @@ +# Hatch Landing Page — Deployment Guide + +## Overview + +The Hatch landing page (`hatch.surf`) is a **Next.js** app deployed via **Docker** on the production server. + +**Key principle**: All changes MUST go through the git repository and automated deployment. Never edit files directly on the server or use `/var/www`. + +--- + +## Repository + +**Location**: `/home/nara/apps/web/` + +**Structure**: +``` +/app/page.tsx # Main page component +/app/layout.tsx # Root layout +/app/globals.css # Global styles +/components/ # React components (Hero, Features, Why, CTA, Footer, Header) +/public/ # Static assets +Dockerfile # Docker build configuration +docker-compose.yml # Container orchestration +deploy.sh # Deployment script +``` + +--- + +## Deployment Flow + +### Automatic (Recommended) + +1. Push changes to `main` branch +2. Gitea webhook triggers `gitea-webhook.py` +3. Script runs `deploy.sh` automatically +4. Docker image rebuilds and container restarts + +### Manual + +```bash +cd /home/nara/apps/web +./deploy.sh +``` + +**What `deploy.sh` does**: +1. `git pull origin main` — fetches latest changes +2. `docker build -t hatch-web:latest .` — rebuilds image +3. Stops and removes old container +4. `docker compose up -d` — starts new container +5. Verifies deployment with health check + +--- + +## Docker Setup + +**Container**: `hatch-web` +**Port**: `8080` (mapped to host) +**Internal**: nginx serving static Next.js export on port 80 +**Network**: `hatch-network` + +**Check status**: +```bash +docker ps | grep hatch-web +docker logs hatch-web +``` + +**Restart without rebuild**: +```bash +docker compose -f /home/nara/apps/web/docker-compose.yml restart +``` + +--- + +## Making Changes + +### 1. Edit the source code + +```bash +cd /home/nara/apps/web + +# Edit components +vim components/Hero.tsx +vim components/Features.tsx +# etc. + +# Edit styles +vim app/globals.css +``` + +### 2. Test locally (optional) + +```bash +npm run dev +# Visit http://localhost:3000 +``` + +### 3. Commit and push + +```bash +git add . +git commit -m "Description of changes" +git push origin main +``` + +### 4. Deploy + +Either wait for webhook or run manually: +```bash +./deploy.sh +``` + +### 5. Verify + +```bash +curl -s http://localhost:8080 | head -20 +# Or visit https://hatch.surf +``` + +--- + +## Common Mistakes to Avoid + +❌ **DON'T**: Edit files in `/var/www/html/` +❌ **DON'T**: Edit files directly inside the Docker container +❌ **DON'T**: Work on static HTML/CSS files in a workspace +❌ **DON'T**: Copy files to the server without using git + +✅ **DO**: Edit files in `/home/nara/apps/web/` +✅ **DO**: Commit changes to git +✅ **DO**: Use `deploy.sh` for deployment +✅ **DO**: Work on the Next.js components (`app/`, `components/`) + +--- + +## Rollback + +If a deployment causes issues: + +```bash +cd /home/nara/apps/web + +# View recent commits +git log --oneline -10 + +# Revert to previous version +git revert HEAD +git push origin main + +# Or reset to specific commit +git reset --hard +git push origin main --force + +# Redeploy +./deploy.sh +``` + +--- + +## Cache Headers + +**HTML pages**: `no-cache, must-revalidate` — browsers always revalidate, ensuring fresh content after deploys. + +**Static assets** (CSS/JS/images): `public, immutable` with 30-day expiry. Safe because Next.js uses content hashes in filenames (e.g., `2oioldxlp5hov.css`), so new deployments get new URLs. + +**Nginx config**: `/etc/nginx/sites-available/hatch.surf.conf` + +If users report seeing old content after deployment: +1. Verify the container is running: `docker ps | grep hatch-web` +2. Check the HTML has new classes: `curl -s http://localhost:8080 | grep bg-gradient-to-r` +3. Ask user to hard refresh: Ctrl+Shift+R (Chrome/Firefox) or Cmd+Shift+R (Mac) +4. If still cached, check nginx config has `Cache-Control: no-cache, must-revalidate` + +--- + +## Troubleshooting + +**Container not starting**: +```bash +docker logs hatch-web +docker compose -f /home/nara/apps/web/docker-compose.yml up -d +``` + +**Changes not showing**: +1. Verify commit pushed: `git log --oneline -1` +2. Check container was rebuilt: `docker images | grep hatch-web` +3. Force rebuild: `cd /home/nara/apps/web && docker build -t hatch-web:latest . && docker compose up -d` + +**Port conflict**: +```bash +lsof -i :8080 +# Kill conflicting process or change port in docker-compose.yml +``` + +--- + +## Contact + +- **DevOps**: Jing Yang (Sam Lee) +- **CTO**: Jordan Patel +- **Repository**: `/home/nara/apps/web/` +- **Live site**: https://hatch.surf diff --git a/Dockerfile b/Dockerfile index 7484c2ad..f3d7575d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,11 @@ -# Stage 1: Build -FROM node:20-alpine AS builder - -WORKDIR /app - -# Install dependencies -COPY package.json package-lock.json ./ -RUN npm ci - -# Build the application -COPY . . -RUN npm run build - -# Stage 2: Production +# Production - Static landing page FROM nginx:alpine AS production # Copy custom nginx config COPY nginx.conf /etc/nginx/conf.d/default.conf -# Copy static export (everything is in /app/out) -COPY --from=builder /app/out /usr/share/nginx/html +# Copy static files from build context +COPY out/ /usr/share/nginx/html # Expose port (internal only, no SSL) EXPOSE 80 diff --git a/Dockerfile.bak b/Dockerfile.bak new file mode 100644 index 00000000..7484c2ad --- /dev/null +++ b/Dockerfile.bak @@ -0,0 +1,28 @@ +# Stage 1: Build +FROM node:20-alpine AS builder + +WORKDIR /app + +# Install dependencies +COPY package.json package-lock.json ./ +RUN npm ci + +# Build the application +COPY . . +RUN npm run build + +# Stage 2: Production +FROM nginx:alpine AS production + +# Copy custom nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy static export (everything is in /app/out) +COPY --from=builder /app/out /usr/share/nginx/html + +# Expose port (internal only, no SSL) +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s \ + CMD wget -qO- http://localhost:80/ || exit 1 diff --git a/docker-compose.yml b/docker-compose.yml index 4324b0c7..7732c986 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,10 +10,11 @@ services: networks: - hatch-network healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:80/"] + test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"] interval: 30s - timeout: 3s - retries: 3 + timeout: 10s + retries: 5 + start_period: 30s networks: hatch-network: diff --git a/gitea-webhook.py b/gitea-webhook.py new file mode 100644 index 00000000..e2cee628 --- /dev/null +++ b/gitea-webhook.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +""" +Gitea webhook server for hatch.surf deployment +Listens on port 9000 for POST requests from Gitea +""" + +import http.server +import json +import subprocess +import logging +from pathlib import Path + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('/home/nara/logs/gitea-webhook.log'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +DEPLOY_SCRIPT = Path(__file__).parent / "deploy.sh" + +class WebhookHandler(http.server.BaseHTTPRequestHandler): + def do_POST(self): + """Handle POST request from Gitea webhook""" + content_length = int(self.headers.get('Content-Length', 0)) + body = self.rfile.read(content_length) if content_length > 0 else b'' + + try: + payload = json.loads(body) if body else {} + event = self.headers.get('X-Gitea-Event', 'unknown') + + logger.info(f"Received webhook: {event}") + logger.info(f"Payload: {json.dumps(payload, indent=2)}") + + # Only deploy on push to main branch + if event == 'push': + ref = payload.get('ref', '') + if ref == 'refs/heads/main': + logger.info("Push to main branch - triggering deployment...") + self._deploy() + else: + logger.info(f"Ignoring push to {ref}") + else: + logger.info(f"Ignoring event: {event}") + + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({"status": "ok"}).encode()) + + except Exception as e: + logger.error(f"Error processing webhook: {e}") + self.send_response(500) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({"status": "error", "message": str(e)}).encode()) + + def _deploy(self): + """Run the deployment script""" + try: + logger.info(f"Running deploy script: {DEPLOY_SCRIPT}") + result = subprocess.run( + [str(DEPLOY_SCRIPT)], + capture_output=True, + text=True, + timeout=300 # 5 minute timeout + ) + + if result.returncode == 0: + logger.info("Deployment successful!") + logger.info(f"Output: {result.stdout}") + else: + logger.error(f"Deployment failed with code {result.returncode}") + logger.error(f"Stdout: {result.stdout}") + logger.error(f"Stderr: {result.stderr}") + + except subprocess.TimeoutExpired: + logger.error("Deployment timed out after 5 minutes") + except Exception as e: + logger.error(f"Error running deployment: {e}") + + def do_GET(self): + """Health check endpoint""" + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({"status": "healthy"}).encode()) + + def log_message(self, format, *args): + """Override to use our logger""" + logger.info(format % args) + +def main(): + server = http.server.HTTPServer(('0.0.0.0', 9000), WebhookHandler) + logger.info("Starting webhook server on port 9000...") + try: + server.serve_forever() + except KeyboardInterrupt: + logger.info("Shutting down webhook server...") + server.shutdown() + +if __name__ == '__main__': + main()