Add production deployment config for nomadweb.nomadro.com
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
63778ba761
commit
dcd2b60885
6
.env.production.example
Normal file
6
.env.production.example
Normal 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
|
||||||
37
deploy/nginx/nomadweb.conf
Normal file
37
deploy/nginx/nomadweb.conf
Normal 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
39
docker-compose.prod.yml
Normal 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
|
||||||
@ -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
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
71
scripts/deploy-production.sh
Normal file
71
scripts/deploy-production.sh
Normal 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"
|
||||||
Loading…
Reference in New Issue
Block a user