deploy: landing page redesign - static HTML with anime.js
- Simplified Dockerfile to serve static files directly (no Next.js build) - Copied redesign files (index.html, style.css, main.js) to out/ - Added brand/favicon assets - Backed up previous Next.js build to out.backup.* - Container rebuilt and verified healthy on port 8080 Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
79e74b4810
commit
dcc26b766b
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,3 +26,4 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
out.backup.*
|
||||
|
||||
201
DEPLOYMENT.md
Normal file
201
DEPLOYMENT.md
Normal file
@ -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 <commit-hash>
|
||||
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
|
||||
19
Dockerfile
19
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
|
||||
|
||||
28
Dockerfile.bak
Normal file
28
Dockerfile.bak
Normal file
@ -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
|
||||
@ -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:
|
||||
|
||||
106
gitea-webhook.py
Normal file
106
gitea-webhook.py
Normal file
@ -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()
|
||||
Loading…
Reference in New Issue
Block a user