Compare commits
No commits in common. "0f7492f6b42abe3d437864717c4c748e0f2a7c0f" and "79e74b4810d04d149aeb9765170900efa50cb6ed" have entirely different histories.
0f7492f6b4
...
79e74b4810
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,4 +26,3 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
out.backup.*
|
|
||||||
|
|||||||
200
DEPLOYMENT.md
200
DEPLOYMENT.md
@ -1,200 +0,0 @@
|
|||||||
# Hatch Landing Page — Deployment Guide
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Hatch landing page (`hatch.surf`) is a **static HTML** site 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**:
|
|
||||||
```
|
|
||||||
out/ # Static files served by nginx
|
|
||||||
index.html # Main landing page
|
|
||||||
style.css # Styles
|
|
||||||
main.js # Anime.js animations
|
|
||||||
brand/favicon/ # Favicon assets
|
|
||||||
Dockerfile # Docker build configuration (static files only)
|
|
||||||
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 files 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 static files
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/nara/apps/web
|
|
||||||
|
|
||||||
# Edit landing page
|
|
||||||
vim out/index.html
|
|
||||||
|
|
||||||
# Edit styles
|
|
||||||
vim out/style.css
|
|
||||||
|
|
||||||
# Edit animations
|
|
||||||
vim out/main.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Test locally (optional)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Serve files locally
|
|
||||||
cd out && python3 -m http.server 3000
|
|
||||||
# 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 without committing
|
|
||||||
❌ **DON'T**: Copy files to the server without using git
|
|
||||||
|
|
||||||
✅ **DO**: Edit files in `/home/nara/apps/web/out/`
|
|
||||||
✅ **DO**: Commit changes to git
|
|
||||||
✅ **DO**: Use `deploy.sh` for deployment
|
|
||||||
✅ **DO**: Test locally before pushing
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
# Redeploy
|
|
||||||
./deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Previous builds are backed up in `out.backup.*` directories.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cache Headers
|
|
||||||
|
|
||||||
**HTML pages**: `no-cache, must-revalidate` — browsers always revalidate, ensuring fresh content after deploys.
|
|
||||||
|
|
||||||
**Static assets** (CSS/JS/images): `public, immutable` with 1-year expiry.
|
|
||||||
|
|
||||||
**Nginx config**: Built into the Dockerfile.
|
|
||||||
|
|
||||||
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 content: `curl -s http://localhost:8080 | head -5`
|
|
||||||
3. Ask user to hard refresh: Ctrl+Shift+R (Chrome/Firefox) or Cmd+Shift+R (Mac)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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
|
|
||||||
19
Dockerfile
19
Dockerfile
@ -1,11 +1,24 @@
|
|||||||
# Production - Static landing page
|
# 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
|
FROM nginx:alpine AS production
|
||||||
|
|
||||||
# Copy custom nginx config
|
# Copy custom nginx config
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
# Copy static files from build context
|
# Copy static export (everything is in /app/out)
|
||||||
COPY out/ /usr/share/nginx/html
|
COPY --from=builder /app/out /usr/share/nginx/html
|
||||||
|
|
||||||
# Expose port (internal only, no SSL)
|
# Expose port (internal only, no SSL)
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -10,11 +10,10 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- hatch-network
|
- hatch-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"]
|
test: ["CMD", "wget", "-qO-", "http://localhost:80/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 3s
|
||||||
retries: 5
|
retries: 3
|
||||||
start_period: 30s
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
hatch-network:
|
hatch-network:
|
||||||
|
|||||||
106
gitea-webhook.py
106
gitea-webhook.py
@ -1,106 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
Loading…
Reference in New Issue
Block a user