Add production deployment config for nomadweb.nomadro.com

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric 2026-08-28 19:56:34 -05:00
parent 63778ba761
commit dcd2b60885
6 changed files with 162 additions and 7 deletions

6
.env.production.example Normal file
View File

@ -0,0 +1,6 @@
# Production environment (copy to server as /opt/nomadweb/.env)
DOMAIN=nomadweb.nomadro.com
NEXT_PUBLIC_API_URL=https://nomadweb.nomadro.com/api/v1
CORS_ORIGINS=https://nomadweb.nomadro.com,http://nomadweb.nomadro.com
POCKETBASE_ADMIN_EMAIL=admin@nomadflow.io
POCKETBASE_ADMIN_PASSWORD=change-me-in-production

View File

@ -0,0 +1,37 @@
server {
listen 80;
server_name nomadweb.nomadro.com;
client_max_body_size 20M;
location /api/ {
proxy_pass http://127.0.0.1:8000/api/;
proxy_http_version 1.1;
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;
}
location /docs {
proxy_pass http://127.0.0.1:8000/docs;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /openapi.json {
proxy_pass http://127.0.0.1:8000/openapi.json;
proxy_set_header Host $host;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

39
docker-compose.prod.yml Normal file
View File

@ -0,0 +1,39 @@
services:
pocketbase:
image: ghcr.io/muchobien/pocketbase:latest
container_name: nomadflow-pb
volumes:
- ./pocketbase/pb_data:/pb_data
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8090/api/health"]
interval: 10s
timeout: 5s
retries: 5
backend:
build: ./backend
container_name: nomadflow-api
ports:
- "127.0.0.1:8000:8000"
environment:
- POCKETBASE_URL=http://pocketbase:8090
- POCKETBASE_ADMIN_EMAIL=${POCKETBASE_ADMIN_EMAIL:-admin@nomadflow.io}
- POCKETBASE_ADMIN_PASSWORD=${POCKETBASE_ADMIN_PASSWORD:-admin123456}
- CORS_ORIGINS=${CORS_ORIGINS:-https://nomadweb.nomadro.com,http://nomadweb.nomadro.com}
depends_on:
pocketbase:
condition: service_healthy
restart: unless-stopped
frontend:
build:
context: ./frontend
args:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-https://nomadweb.nomadro.com/api/v1}
container_name: nomadflow-web
ports:
- "127.0.0.1:3000:3000"
depends_on:
- backend
restart: unless-stopped

View File

@ -3,7 +3,7 @@ FROM node:20-alpine AS base
FROM base AS deps FROM base AS deps
WORKDIR /app WORKDIR /app
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci RUN npm ci --legacy-peer-deps || npm install --legacy-peer-deps
FROM base AS builder FROM base AS builder
WORKDIR /app WORKDIR /app

View File

@ -12,23 +12,25 @@ interface Props {
const COLORS = ["#FF6B6B", "#FFE66D", "#4ECDC4", "#A78BFA", "#F472B6", "#60A5FA"]; const COLORS = ["#FF6B6B", "#FFE66D", "#4ECDC4", "#A78BFA", "#F472B6", "#60A5FA"];
function loadChartJS(): Promise<typeof import("chart.js")> { function loadChartJS(): Promise<{ Chart: ChartConstructor }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (typeof window !== "undefined" && (window as unknown as { Chart?: unknown }).Chart) { if (typeof window !== "undefined" && (window as unknown as { Chart?: ChartConstructor }).Chart) {
resolve({ Chart: (window as unknown as { Chart: typeof import("chart.js").Chart }).Chart } as typeof import("chart.js")); resolve({ Chart: (window as unknown as { Chart: ChartConstructor }).Chart });
return; return;
} }
const script = document.createElement("script"); const script = document.createElement("script");
script.src = "https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"; script.src = "https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js";
script.onload = () => { script.onload = () => {
const Chart = (window as unknown as { Chart: typeof import("chart.js").Chart }).Chart; resolve({ Chart: (window as unknown as { Chart: ChartConstructor }).Chart });
resolve({ Chart } as typeof import("chart.js"));
}; };
script.onerror = reject; script.onerror = reject;
document.head.appendChild(script); document.head.appendChild(script);
}); });
} }
type ChartInstance = { destroy: () => void };
type ChartConstructor = new (ctx: HTMLCanvasElement, config: unknown) => ChartInstance;
export default function Charts({ cost, speed, growth, radar }: Props) { export default function Charts({ cost, speed, growth, radar }: Props) {
const costRef = useRef<HTMLCanvasElement>(null); const costRef = useRef<HTMLCanvasElement>(null);
const speedRef = useRef<HTMLCanvasElement>(null); const speedRef = useRef<HTMLCanvasElement>(null);
@ -36,7 +38,7 @@ export default function Charts({ cost, speed, growth, radar }: Props) {
const radarRef = useRef<HTMLCanvasElement>(null); const radarRef = useRef<HTMLCanvasElement>(null);
useEffect(() => { useEffect(() => {
let charts: InstanceType<typeof import("chart.js").Chart>[] = []; let charts: ChartInstance[] = [];
loadChartJS().then(({ Chart }) => { loadChartJS().then(({ Chart }) => {
const isLight = document.documentElement.getAttribute("data-theme") === "light"; const isLight = document.documentElement.getAttribute("data-theme") === "light";

View File

@ -0,0 +1,71 @@
#!/bin/bash
set -euo pipefail
APP_DIR="${APP_DIR:-/opt/nomadweb}"
DOMAIN="${DOMAIN:-nomadweb.nomadro.com}"
GIT_REPO="${GIT_REPO:-https://gitea.dsx2020.com/eric/nomadweb.git}"
BRANCH="${BRANCH:-main}"
echo "==> NomadFlow production deploy"
echo " Dir: $APP_DIR"
echo " Domain: $DOMAIN"
export DEBIAN_FRONTEND=noninteractive
if ! command -v docker &>/dev/null; then
echo "==> Installing Docker..."
curl -fsSL https://get.docker.com | sh
systemctl enable docker
systemctl start docker
fi
if ! command -v nginx &>/dev/null; then
echo "==> Installing Nginx..."
apt-get update -qq
apt-get install -y -qq nginx git curl
systemctl enable nginx
fi
if [ ! -d "$APP_DIR/.git" ]; then
echo "==> Cloning repository..."
mkdir -p "$(dirname "$APP_DIR")"
git clone "$GIT_REPO" "$APP_DIR"
else
echo "==> Pulling latest..."
cd "$APP_DIR"
git fetch origin
git reset --hard "origin/$BRANCH"
fi
cd "$APP_DIR"
export NEXT_PUBLIC_API_URL="https://${DOMAIN}/api/v1"
export CORS_ORIGINS="https://${DOMAIN},http://${DOMAIN}"
echo "==> Building and starting containers..."
docker compose -f docker-compose.prod.yml down 2>/dev/null || true
docker compose -f docker-compose.prod.yml build --no-cache
docker compose -f docker-compose.prod.yml up -d
echo "==> Configuring Nginx..."
cp deploy/nginx/nomadweb.conf "/etc/nginx/sites-available/${DOMAIN}"
ln -sf "/etc/nginx/sites-available/${DOMAIN}" "/etc/nginx/sites-enabled/${DOMAIN}"
rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true
nginx -t
systemctl reload nginx
if command -v certbot &>/dev/null; then
echo "==> Renewing SSL certificate..."
certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos --register-unsafely-without-email --redirect 2>/dev/null || \
certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos -m "admin@${DOMAIN}" --redirect || true
fi
echo "==> Health check..."
sleep 5
curl -sf "http://127.0.0.1:8000/api/v1/health" && echo " API OK"
curl -sf -o /dev/null "http://127.0.0.1:3000" && echo " Frontend OK"
echo ""
echo "Deploy complete!"
echo " Site: https://${DOMAIN}"
echo " API: https://${DOMAIN}/api/v1/health"