Commit 698489ca authored by Administrator's avatar Administrator

Update 3 files via Son of Anton

parent f28d9aac
###############################################
# Stage 1: Dependencies
###############################################
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
# ═══════════════════════════════════════════════════════
# THE GRIND — Frontend Dockerfile (CapRover-ready)
# ═══════════════════════════════════════════════════════
# ── Stage 1: Dependencies ──
FROM node:20-alpine AS deps
WORKDIR /build
# ── shared package ──────────────────────────
# Shared package
COPY shared/package.json ./shared/
COPY shared/tsconfig.json ./shared/
COPY shared/src/ ./shared/src/
# ── frontend dependencies ───────────────────
COPY frontend/package*.json ./frontend/
# Frontend package
COPY frontend/package.json ./frontend/
WORKDIR /build/frontend
RUN npm install --legacy-peer-deps
# Install deps
RUN cd shared && npm install --ignore-scripts
RUN cd frontend && npm install --ignore-scripts
###############################################
# Stage 2: Build
###############################################
# ── Stage 2: Build ──
FROM node:20-alpine AS builder
RUN apk add --no-cache libc6-compat
WORKDIR /build
# Copy deps
# API URLs — set at build time via ARG, baked into the Next.js bundle
ARG NEXT_PUBLIC_API_URL=https://thegrind-api.caprover.al-arcade.com
ARG NEXT_PUBLIC_WS_URL=wss://thegrind-api.caprover.al-arcade.com
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL}
COPY --from=deps /build/shared ./shared
COPY --from=deps /build/frontend/node_modules ./frontend/node_modules
# Copy frontend source
COPY frontend/ ./frontend/
# Shared needs to be accessible for transpilePackages
COPY shared/ ./shared/
COPY frontend/ ./frontend/
WORKDIR /build/frontend
# Build-time env vars (these get baked into the client bundle)
# Set safe defaults; override in CapRover env vars
ARG NEXT_PUBLIC_API_URL=https://thegrind-api.example.com
ARG NEXT_PUBLIC_WS_URL=wss://thegrind-api.example.com
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_WS_URL=$NEXT_PUBLIC_WS_URL
ENV NEXT_TELEMETRY_DISABLED=1
# Build Next.js (standalone mode)
RUN npm run build
###############################################
# Stage 3: Production
###############################################
FROM node:20-alpine
RUN apk add --no-cache libc6-compat tini
RUN cd shared && npm run build 2>/dev/null || true
RUN cd frontend && npm run build
# ── Stage 3: Production ──
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# standalone output puts everything we need in .next/standalone
COPY --from=builder /build/frontend/.next/standalone ./
COPY --from=builder /build/frontend/.next/static ./.next/static
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /build/frontend/public ./public
COPY --from=builder --chown=nextjs:nodejs /build/frontend/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /build/frontend/.next/static ./.next/static
EXPOSE 3000
USER nextjs
ENTRYPOINT ["/sbin/tini", "--"]
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
\ No newline at end of file
#!/bin/bash
# ═══════════════════════════════════════════════════════════════════════
# THE GRIND — CapRover Full Deployment v3
#
# Usage: chmod +x deploy-thegrind.sh && sudo ./deploy-thegrind.sh
# GitLab: GITLAB_USER=root GITLAB_PASS=xxx sudo ./deploy-thegrind.sh
#
# Idempotent — safe to re-run. Existing apps skip creation.
# ═══════════════════════════════════════════════════════════════════════
set -euo pipefail
# ┌──────────────────────────────────────────────────────────────────┐
# │ CONFIGURATION │
# └──────────────────────────────────────────────────────────────────┘
CAPTAIN_URL="https://captain.caprover.al-arcade.com"
CAPTAIN_PASSWORD="Alarcade123#"
GITLAB_HOST="gitlab.caprover.al-arcade.com"
GITLAB_REPO_PATH="root/hrsystem"
GITLAB_BRANCH="main"
GITLAB_USER="${GITLAB_USER:-}"
GITLAB_PASS="${GITLAB_PASS:-}"
APP_DB="thegrind-db"
APP_REDIS="thegrind-redis"
APP_MINIO="thegrind-minio"
APP_BACKEND="thegrind-api"
APP_FRONTEND="thegrind"
ROOT_DOMAIN=$(echo "$CAPTAIN_URL" | sed 's|https://captain\.||;s|http://captain\.||;s|/||g')
BE_URL="https://${APP_BACKEND}.${ROOT_DOMAIN}"
FE_URL="https://${APP_FRONTEND}.${ROOT_DOMAIN}"
BE_WS="wss://${APP_BACKEND}.${ROOT_DOMAIN}"
MINIO_URL="https://${APP_MINIO}.${ROOT_DOMAIN}"
DB_NAME="thegrind"
DB_USER="postgres"
DB_PASSWORD="$(openssl rand -hex 20)"
MINIO_USER="minioadmin"
MINIO_PASS="$(openssl rand -hex 20)"
MINIO_BUCKET="hr-files"
JWT_SECRET="$(openssl rand -hex 32)"
DB_HOST="srv-captain--${APP_DB}"
REDIS_HOST="srv-captain--${APP_REDIS}"
MINIO_HOST="srv-captain--${APP_MINIO}"
DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:5432/${DB_NAME}?schema=public"
WORK_DIR="/tmp/thegrind-deploy-$$"
REPO_DIR="${WORK_DIR}/hrsystem"
CREDS_FILE="/root/thegrind-credentials.txt"
TOKEN=""
# ═══════════════════════════════════════════════════════════════
# HELPERS
# ═══════════════════════════════════════════════════════════════
R='\033[0;31m' G='\033[0;32m' Y='\033[1;33m' C='\033[0;36m'
M='\033[0;35m' B='\033[1m' D='\033[2m' N='\033[0m'
log() { echo -e "${C}[$(date +%H:%M:%S)]${N} $1"; }
ok() { echo -e "${G}[✓]${N} $1"; }
warn() { echo -e "${Y}[⚠]${N} $1"; }
err() { echo -e "${R}[✗]${N} $1"; }
fatal() { echo -e "${R}[FATAL]${N} $1"; rm -rf "$WORK_DIR" 2>/dev/null; exit 1; }
banner() { echo -e "\n${M}${B}═══ $1 ═══${N}\n"; }
line() { echo -e "${D}──────────────────────────────────────────${N}"; }
trap 'rm -rf "$WORK_DIR" 2>/dev/null' ERR
build_git_url() {
if [ -n "$GITLAB_USER" ] && [ -n "$GITLAB_PASS" ]; then
local EP
EP=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${GITLAB_PASS}',safe=''))" 2>/dev/null || echo "$GITLAB_PASS")
echo "http://${GITLAB_USER}:${EP}@${GITLAB_HOST}/${GITLAB_REPO_PATH}.git"
else
echo "http://${GITLAB_HOST}/${GITLAB_REPO_PATH}.git"
fi
}
countdown() {
log "Starting in 5s... Ctrl+C to abort"
for i in 5 4 3 2 1; do printf "\r ${Y}${B}%d...${N} " "$i"; sleep 1; done
printf "\r ${G}${B}GO!${N} \n\n"
}
# ═══════════════════════════════════════════════════════════════
# CAPROVER API v2
# ═══════════════════════════════════════════════════════════════
# Low-level: returns JSON body
_api() {
local method="$1" endpoint="$2" data="${3:-}"
local url="${CAPTAIN_URL}/api/v2${endpoint}"
local args=(-s -S -w '\n%{http_code}' -X "$method" --insecure --max-time 180)
args+=(-H 'Content-Type: application/json')
[ -n "$TOKEN" ] && args+=(-H "x-captain-auth: ${TOKEN}")
[ -n "$data" ] && args+=(-d "$data")
local raw http body
raw=$(curl "${args[@]}" "$url" 2>&1) || raw=$'\n000'
http=$(echo "$raw" | tail -1)
body=$(echo "$raw" | sed '$d')
echo "${body}"
return 0
}
# High-level: checks status, returns 0/1
cap_api() {
local method="$1" endpoint="$2" data="${3:-}"
local body status desc
body=$(_api "$method" "$endpoint" "$data")
status=$(echo "$body" | jq -r '.status // -1' 2>/dev/null || echo "-1")
if [ "$status" = "100" ]; then
echo "$body"
return 0
fi
desc=$(echo "$body" | jq -r '.description // empty' 2>/dev/null || echo "")
[ -z "$desc" ] && desc=$(echo "$body" | head -c 300)
echo "$desc" >&2
return 1
}
# Upload tarball
cap_upload() {
local app="$1" tar="$2"
local url="${CAPTAIN_URL}/api/v2/user/apps/appData/${app}"
local raw http body status
raw=$(curl -s -S -w '\n%{http_code}' -X POST --insecure --max-time 600 \
-H "x-captain-auth: ${TOKEN}" \
-F "sourceFile=@${tar}" \
-F "gitHash=$(date +%s)" \
"$url" 2>&1) || raw=$'\n000'
http=$(echo "$raw" | tail -1)
body=$(echo "$raw" | sed '$d')
status=$(echo "$body" | jq -r '.status // -1' 2>/dev/null || echo "-1")
if [ "$status" = "100" ]; then
return 0
fi
local desc
desc=$(echo "$body" | jq -r '.description // empty' 2>/dev/null || echo "HTTP $http")
echo "$desc" >&2
return 1
}
# ═══════════════════════════════════════════════════════════════
# APP MANAGEMENT HELPERS
# ═══════════════════════════════════════════════════════════════
create_app() {
local name="$1" persistent="${2:-false}"
log "Creating app: ${B}${name}${N}"
local res
if res=$(cap_api POST "/user/apps/appDefinitions/register" \
"{\"appName\":\"${name}\",\"hasPersistentData\":${persistent}}" 2>&1); then
ok "Created '${name}'"
else
if echo "$res" | grep -qi "already exist"; then
warn "'${name}' already exists — OK"
else
err "Create '${name}' failed: $res"
return 1
fi
fi
sleep 2
}
set_env_vars() {
local name="$1"
shift
local vars=("$@")
# Build envVars JSON array
local json_arr="["
local first=true
for pair in "${vars[@]}"; do
local key="${pair%%=*}"
local val="${pair#*=}"
# Escape val for JSON
val=$(echo "$val" | jq -Rs '.' | sed 's/^"//;s/"$//')
[ "$first" = true ] && first=false || json_arr+=","
json_arr+="{\"key\":\"${key}\",\"value\":\"${val}\"}"
done
json_arr+="]"
local payload="{\"appName\":\"${name}\",\"envVars\":${json_arr}}"
log "Setting env vars for ${B}${name}${N}..."
if cap_api POST "/user/apps/appDefinitions/update" "$payload" >/dev/null 2>&1; then
ok "Env vars set for '${name}'"
else
err "Failed to set env vars for '${name}'"
return 1
fi
}
set_app_config() {
local name="$1" payload="$2"
log "Configuring ${B}${name}${N}..."
if cap_api POST "/user/apps/appDefinitions/update" "$payload" >/dev/null 2>&1; then
ok "Configured '${name}'"
else
err "Config failed for '${name}'"
return 1
fi
}
deploy_image() {
local name="$1" image="$2"
log "Deploying ${B}${image}${N}${name}"
local def
def=$(printf '{"schemaVersion":2,"imageName":"%s"}' "$image")
local escaped
escaped=$(echo "$def" | jq -Rs '.')
if cap_api POST "/user/apps/appData/${name}" \
"{\"captainDefinitionContent\":${escaped},\"gitHash\":\"$(date +%s)\"}" >/dev/null 2>&1; then
ok "Deployed '${image}' → '${name}'"
else
err "Deploy '${image}' → '${name}' failed"
return 1
fi
}
deploy_dockerfile_inline() {
local name="$1"
shift
local content="$*"
log "Deploying inline Dockerfile → ${B}${name}${N}"
local tmp
tmp=$(mktemp -d)
echo '{"schemaVersion":2,"dockerfilePath":"./Dockerfile"}' > "${tmp}/captain-definition"
echo "$content" > "${tmp}/Dockerfile"
tar -cf "${tmp}/deploy.tar" -C "$tmp" captain-definition Dockerfile
if cap_upload "$name" "${tmp}/deploy.tar" 2>&1; then
ok "Deployed Dockerfile → '${name}'"
else
err "Dockerfile deploy to '${name}' failed"
fi
rm -rf "$tmp"
}
wait_build() {
local name="$1" max="${2:-480}" elapsed=0
log "Waiting for ${B}${name}${N} build (max ${max}s)..."
while [ $elapsed -lt "$max" ]; do
sleep 10
elapsed=$((elapsed + 10))
local body
body=$(_api GET "/user/apps/appDefinitions")
local building
building=$(echo "$body" | jq -r ".data.appDefinitions[] | select(.appName==\"${name}\") | .isAppBuilding" 2>/dev/null || echo "true")
if [ "$building" = "false" ]; then
local failed
failed=$(echo "$body" | jq -r ".data.appDefinitions[] | select(.appName==\"${name}\") | .deployedVersion == 0" 2>/dev/null || echo "false")
if [ "$failed" = "true" ]; then
err "'${name}' build FAILED (${elapsed}s) — check CapRover logs"
else
ok "'${name}' build done (${elapsed}s)"
fi
return 0
fi
printf "."
done
echo ""
warn "'${name}' still building after ${max}s"
}
health_check() {
local label="$1" url="$2" retries="${3:-5}"
log "Health: ${B}${label}${N}${url}"
for i in $(seq 1 "$retries"); do
local code
code=$(curl -sk -o /dev/null -w "%{http_code}" "$url" 2>/dev/null || echo "000")
case "$code" in
200|201|301|302|308)
ok "${label} is ${G}HEALTHY${N} (HTTP ${code})"
return 0
;;
esac
[ "$i" -lt "$retries" ] && { warn "${label} HTTP ${code} — retry ${i}/${retries} in 20s..."; sleep 20; }
done
warn "${label} not responding after ${retries} attempts"
return 1
}
enable_ssl() {
local name="$1"
log "SSL for ${B}${name}.${ROOT_DOMAIN}${N}..."
if cap_api POST "/user/apps/appDefinitions/enablebasedomainssl" \
"{\"appName\":\"${name}\"}" >/dev/null 2>&1; then
ok "SSL: ${name}"
sleep 3
cap_api POST "/user/apps/appDefinitions/update" \
"{\"appName\":\"${name}\",\"forceSsl\":true}" >/dev/null 2>&1 && \
ok "HTTPS forced: ${name}" || warn "Force HTTPS failed for ${name}"
else
warn "SSL failed for ${name} — DNS may not be ready"
fi
}
# ═══════════════════════════════════════════════════════════════
# PHASE 0: PREREQUISITES
# ═══════════════════════════════════════════════════════════════
phase0() {
banner "PHASE 0: PREREQUISITES"
[ "$EUID" -ne 0 ] && fatal "Run as root: sudo ./deploy-thegrind.sh"
apt-get update -qq >/dev/null 2>&1 || true
for pkg in jq git curl openssl python3; do
command -v "$pkg" &>/dev/null && ok "${pkg} ✓" || {
apt-get install -y -qq "$pkg" >/dev/null 2>&1 && ok "${pkg} installed"
}
done
command -v node &>/dev/null && ok "node $(node -v) ✓" || {
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null 2>&1
apt-get install -y -qq nodejs >/dev/null 2>&1
ok "node $(node -v) installed"
}
command -v caprover &>/dev/null && ok "caprover CLI ✓" || {
npm i -g caprover >/dev/null 2>&1 && ok "caprover CLI installed"
}
local code
code=$(curl -sk -o /dev/null -w "%{http_code}" "$CAPTAIN_URL" 2>/dev/null || echo 000)
[ "$code" = "000" ] && fatal "Can't reach CapRover at $CAPTAIN_URL"
ok "CapRover reachable (HTTP $code)"
code=$(curl -sk -o /dev/null -w "%{http_code}" "http://${GITLAB_HOST}" 2>/dev/null || echo 000)
[ "$code" != "000" ] && ok "GitLab reachable (HTTP $code)" || warn "GitLab not reachable externally"
mkdir -p "$WORK_DIR"
line
echo -e " ${B}Plan:${N}"
echo -e " Frontend: ${C}${FE_URL}${N}"
echo -e " Backend: ${C}${BE_URL}${N}"
echo -e " Repo: ${D}http://${GITLAB_HOST}/${GITLAB_REPO_PATH}.git (${GITLAB_BRANCH})${N}"
line
countdown
}
# ═══════════════════════════════════════════════════════════════
# PHASE 1: AUTH
# ═══════════════════════════════════════════════════════════════
phase1() {
banner "PHASE 1: AUTHENTICATE"
local res
res=$(cap_api POST "/login" "{\"password\":\"${CAPTAIN_PASSWORD}\"}" 2>&1) || \
fatal "Login failed: $res"
TOKEN=$(echo "$res" | jq -r '.data.token // empty')
[ -z "$TOKEN" ] && fatal "No token in response"
ok "Authenticated"
# Quick sanity — list apps
local apps
apps=$(_api GET "/user/apps/appDefinitions")
local count
count=$(echo "$apps" | jq '.data.appDefinitions | length' 2>/dev/null || echo "?")
ok "API working — ${count} existing apps"
}
# ═══════════════════════════════════════════════════════════════
# PHASE 2: CLONE + VERIFY REPO
# ═══════════════════════════════════════════════════════════════
phase2() {
banner "PHASE 2: CLONE REPOSITORY"
export GIT_SSL_NO_VERIFY=1
local GIT_URL
GIT_URL=$(build_git_url)
log "Cloning ${B}${GITLAB_BRANCH}${N} from GitLab..."
if ! git clone --depth 1 --branch "$GITLAB_BRANCH" "$GIT_URL" "$REPO_DIR" 2>&1; then
if [ -z "$GITLAB_USER" ]; then
warn "Clone failed — need auth?"
read -rp " GitLab username (or 'skip'): " GITLAB_USER
[ "$GITLAB_USER" = "skip" ] && fatal "Can't continue without repo"
read -rsp " GitLab password/token: " GITLAB_PASS; echo
GIT_URL=$(build_git_url)
git clone --depth 1 --branch "$GITLAB_BRANCH" "$GIT_URL" "$REPO_DIR" 2>&1 || \
fatal "Clone failed with credentials"
else
fatal "Clone failed"
fi
fi
ok "Cloned"
cd "$REPO_DIR"
line
log "Verifying files..."
local FAIL=0
for f in Dockerfile.backend Dockerfile.frontend captain-definition-backend captain-definition-frontend \
prisma/schema.prisma backend/package.json frontend/package.json backend/src/main.ts; do
[ -f "$f" ] && ok "$f" || { err "MISSING: $f"; FAIL=1; }
done
[ "$FAIL" -eq 1 ] && fatal "Missing critical files"
ok "$(ls -1 prisma/*.prisma | wc -l) Prisma schemas"
for f in captain-definition-backend captain-definition-frontend; do
local sv
sv=$(jq -r '.schemaVersion' "$f" 2>/dev/null || echo "?")
local dp
dp=$(jq -r '.dockerfilePath' "$f" 2>/dev/null || echo "?")
ok "${f} → v${sv}, ${dp}"
done
# ── Ensure next.config.js has standalone ──
line
if [ -f frontend/next.config.js ] && grep -q "standalone" frontend/next.config.js; then
ok "next.config.js has standalone"
else
log "Fixing next.config.js..."
cat > frontend/next.config.js << 'EOF'
const nextConfig = {
output: 'standalone',
reactStrictMode: true,
transpilePackages: ['shared'],
images: { unoptimized: true },
eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
};
module.exports = nextConfig;
EOF
ok "Created next.config.js"
fi
# ── Inject API URLs into Dockerfile.frontend ──
log "Setting API URLs in Dockerfile.frontend..."
if grep -q "NEXT_PUBLIC_API_URL" Dockerfile.frontend; then
sed -i "s|ARG NEXT_PUBLIC_API_URL=.*|ARG NEXT_PUBLIC_API_URL=${BE_URL}|" Dockerfile.frontend
sed -i "s|ARG NEXT_PUBLIC_WS_URL=.*|ARG NEXT_PUBLIC_WS_URL=${BE_WS}|" Dockerfile.frontend
else
sed -i "0,/^FROM .*/s|^\(FROM .*\)|\1\nARG NEXT_PUBLIC_API_URL=${BE_URL}\nARG NEXT_PUBLIC_WS_URL=${BE_WS}\nENV NEXT_PUBLIC_API_URL=\${NEXT_PUBLIC_API_URL}\nENV NEXT_PUBLIC_WS_URL=\${NEXT_PUBLIC_WS_URL}|" Dockerfile.frontend
fi
ok "API URLs injected"
# Also .env.production as fallback
printf "NEXT_PUBLIC_API_URL=%s\nNEXT_PUBLIC_WS_URL=%s\n" "$BE_URL" "$BE_WS" > frontend/.env.production
ok "frontend/.env.production created"
# Clean junk
rm -f backend/routes/__init__.py backend/routes/design_routes.py backend/services/design_templates.py 2>/dev/null || true
ok "Repo ready"
}
# ═══════════════════════════════════════════════════════════════
# PHASE 3: INFRASTRUCTURE
# ═══════════════════════════════════════════════════════════════
phase3() {
banner "PHASE 3: INFRASTRUCTURE"
# ── PostgreSQL ──
line
log "${B}PostgreSQL 16${N}"
create_app "$APP_DB" true
set_app_config "$APP_DB" "{\"appName\":\"${APP_DB}\",\"instanceCount\":1,\"notExposeAsWebApp\":true,\"volumes\":[{\"volumeName\":\"${APP_DB}-data\",\"containerPath\":\"/var/lib/postgresql/data\"}]}"
set_env_vars "$APP_DB" \
"POSTGRES_DB=${DB_NAME}" \
"POSTGRES_USER=${DB_USER}" \
"POSTGRES_PASSWORD=${DB_PASSWORD}"
deploy_image "$APP_DB" "postgres:16-alpine"
# ── Redis ──
line
log "${B}Redis 7${N}"
create_app "$APP_REDIS" true
set_app_config "$APP_REDIS" "{\"appName\":\"${APP_REDIS}\",\"instanceCount\":1,\"notExposeAsWebApp\":true,\"volumes\":[{\"volumeName\":\"${APP_REDIS}-data\",\"containerPath\":\"/data\"}]}"
deploy_image "$APP_REDIS" "redis:7-alpine"
# ── MinIO ──
line
log "${B}MinIO${N}"
create_app "$APP_MINIO" true
set_app_config "$APP_MINIO" "{\"appName\":\"${APP_MINIO}\",\"instanceCount\":1,\"notExposeAsWebApp\":false,\"containerHttpPort\":9001,\"volumes\":[{\"volumeName\":\"${APP_MINIO}-data\",\"containerPath\":\"/data\"}]}"
set_env_vars "$APP_MINIO" \
"MINIO_ROOT_USER=${MINIO_USER}" \
"MINIO_ROOT_PASSWORD=${MINIO_PASS}"
local MINIO_DF
read -r -d '' MINIO_DF << 'HEREDOC' || true
FROM minio/minio:latest
EXPOSE 9000 9001
CMD ["server", "/data", "--console-address", ":9001"]
HEREDOC
deploy_dockerfile_inline "$APP_MINIO" "$MINIO_DF"
# ── Wait ──
line
log "Waiting 45s for infra startup..."
for i in $(seq 1 45); do printf "\r ${D}%02d/45${N}" "$i"; sleep 1; done
printf "\r"
ok "Infra wait done "
# ── Create bucket ──
line
log "Creating MinIO bucket..."
if command -v docker &>/dev/null; then
local NET
NET=$(docker network ls --format '{{.Name}}' | grep captain | head -1 || echo "captain-overlay-network")
docker run --rm --network "$NET" --entrypoint "" minio/mc:latest sh -c "
mc alias set m http://${MINIO_HOST}:9000 ${MINIO_USER} ${MINIO_PASS} 2>&1 &&
mc mb --ignore-existing m/${MINIO_BUCKET} 2>&1
" 2>&1 | tail -3 && ok "Bucket '${MINIO_BUCKET}' ready" || \
warn "Auto-bucket failed — create at ${MINIO_URL}"
else
warn "No docker CLI — create bucket manually at ${MINIO_URL}"
fi
}
# ═══════════════════════════════════════════════════════════════
# PHASE 4: CONFIGURE APPS
#
# KEY FIX: separate calls for config, env vars, and nginx.
# Sending them all at once caused 500 Internal Server Error.
# ═══════════════════════════════════════════════════════════════
phase4() {
banner "PHASE 4: APPLICATION CONFIGURATION"
# ──── BACKEND ────
line
log "${B}Backend (${APP_BACKEND})${N}"
create_app "$APP_BACKEND" false
# 4a. Basic config — port, websocket, NOT nginx
set_app_config "$APP_BACKEND" "{
\"appName\": \"${APP_BACKEND}\",
\"instanceCount\": 1,
\"notExposeAsWebApp\": false,
\"containerHttpPort\": 3001,
\"websocketSupport\": true,
\"forceSsl\": false
}"
# 4b. Env vars — separate call to avoid payload size issues
set_env_vars "$APP_BACKEND" \
"NODE_ENV=production" \
"PORT=3001" \
"DATABASE_URL=${DATABASE_URL}" \
"REDIS_HOST=${REDIS_HOST}" \
"REDIS_PORT=6379" \
"MINIO_ENDPOINT=${MINIO_HOST}" \
"MINIO_PORT=9000" \
"MINIO_USE_SSL=false" \
"MINIO_ACCESS_KEY=${MINIO_USER}" \
"MINIO_SECRET_KEY=${MINIO_PASS}" \
"MINIO_BUCKET=${MINIO_BUCKET}" \
"JWT_SECRET=${JWT_SECRET}" \
"FRONTEND_URL=${FE_URL}" \
"CORS_ORIGINS=${FE_URL}" \
"API_PREFIX=api" \
"SESSION_TIMEOUT_HOURS=8" \
"MAX_LOGIN_ATTEMPTS=5" \
"LOCKOUT_DURATION_MINUTES=30" \
"MAX_DAILY_LOGIN_ATTEMPTS=15" \
"JWT_ACCESS_EXPIRY=15m" \
"JWT_REFRESH_EXPIRY=7d" \
"JWT_REFRESH_EXPIRY_DAYS=7" \
"MAX_FILE_SIZE_BYTES=26214400" \
"MAX_PROFILE_PHOTO_SIZE_BYTES=5242880"
# 4c. Custom nginx — SEPARATE call with minimal JSON
# Using printf to build the escaped string cleanly
log "Setting nginx config (WebSocket + 100MB uploads)..."
local NX='<%\nif (s.forceSsl) {\n%>\n return 301 https://$host$request_uri;\n<%\n} else {\n%>\n client_max_body_size 100m;\n location / {\n <%- s.nginx.upstream %>\n proxy_http_version 1.1;\n proxy_cache_bypass $http_upgrade;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection \"upgrade\";\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_read_timeout 86400s;\n proxy_send_timeout 86400s;\n }\n<%\n}\n%>'
local NX_PAYLOAD
NX_PAYLOAD=$(printf '{"appName":"%s","customNginxConfig":"%s"}' "$APP_BACKEND" "$NX")
if cap_api POST "/user/apps/appDefinitions/update" "$NX_PAYLOAD" >/dev/null 2>&1; then
ok "Nginx: WebSocket proxy + 100MB upload"
else
warn "Custom nginx failed — WS still works via flag. Large uploads may fail."
warn "Fix: CapRover → ${APP_BACKEND} → HTTP Settings → paste nginx config"
fi
# ──── FRONTEND ────
line
log "${B}Frontend (${APP_FRONTEND})${N}"
create_app "$APP_FRONTEND" false
set_app_config "$APP_FRONTEND" "{
\"appName\": \"${APP_FRONTEND}\",
\"instanceCount\": 1,
\"notExposeAsWebApp\": false,
\"containerHttpPort\": 3000,
\"websocketSupport\": false,
\"forceSsl\": false
}"
set_env_vars "$APP_FRONTEND" \
"NODE_ENV=production" \
"NEXT_PUBLIC_API_URL=${BE_URL}" \
"NEXT_PUBLIC_WS_URL=${BE_WS}"
ok "Phase 4 complete"
}
# ═══════════════════════════════════════════════════════════════
# PHASE 5: BUILD & DEPLOY
# ═══════════════════════════════════════════════════════════════
phase5() {
banner "PHASE 5: BUILD & DEPLOY"
cd "$REPO_DIR"
local EXCL=(
--exclude='.git' --exclude='node_modules' --exclude='.next'
--exclude='dist' --exclude='build' --exclude='coverage'
--exclude='*.log' --exclude='.env' --exclude='.env.local'
--exclude='.env.development*' --exclude='.vscode' --exclude='.idea'
--exclude='minio_data' --exclude='postgres_data' --exclude='redis_data'
)
# ── Backend ──
line
log "${B}Deploying Backend${N} (typically 4-8 min)..."
cp captain-definition-backend captain-definition
tar -cf "${WORK_DIR}/be.tar" "${EXCL[@]}" . 2>/dev/null
log "Tarball: $(du -sh "${WORK_DIR}/be.tar" | cut -f1)"
if cap_upload "$APP_BACKEND" "${WORK_DIR}/be.tar" 2>&1; then
ok "Backend uploaded — building"
else
err "Backend upload failed"
fi
# ── Frontend ──
line
log "${B}Deploying Frontend${N} (typically 5-12 min)..."
cp captain-definition-frontend captain-definition
tar -cf "${WORK_DIR}/fe.tar" "${EXCL[@]}" . 2>/dev/null
log "Tarball: $(du -sh "${WORK_DIR}/fe.tar" | cut -f1)"
if cap_upload "$APP_FRONTEND" "${WORK_DIR}/fe.tar" 2>&1; then
ok "Frontend uploaded — building"
else
err "Frontend upload failed"
fi
rm -f captain-definition
# ── Wait for both ──
line
wait_build "$APP_BACKEND" 480
wait_build "$APP_FRONTEND" 600
}
# ═══════════════════════════════════════════════════════════════
# PHASE 6: SSL + HEALTH + SEED
# ═══════════════════════════════════════════════════════════════
phase6() {
banner "PHASE 6: FINALIZE"
# ── SSL ──
line
for app in "$APP_BACKEND" "$APP_FRONTEND" "$APP_MINIO"; do
enable_ssl "$app"
sleep 2
done
# ── Health ──
line
log "Waiting 30s for containers to start..."
sleep 30
local be_ok=0 fe_ok=0
health_check "Backend" "${BE_URL}/api/health" 5 && be_ok=1
health_check "Frontend" "${FE_URL}" 5 && fe_ok=1
# ── Seed ──
line
if [ "$be_ok" -eq 1 ] && command -v docker &>/dev/null; then
log "Running database seed..."
local CID
CID=$(docker ps -qf "name=srv-captain--${APP_BACKEND}" | head -1)
if [ -n "$CID" ]; then
ok "Container: ${CID:0:12}"
if docker exec "$CID" sh -c 'cd /app 2>/dev/null || true; npx prisma db seed --schema=./prisma/schema.prisma 2>&1' 2>&1 | tail -15; then
ok "Database seeded"
else
warn "Seed may have failed — try manually:"
warn " docker exec ${CID} npx prisma db seed --schema=./prisma/schema.prisma"
fi
else
warn "Backend container not found yet — seed manually"
fi
else
warn "Backend not healthy or no docker CLI — seed manually later"
fi
}
# ═══════════════════════════════════════════════════════════════
# PHASE 7: SUMMARY
# ═══════════════════════════════════════════════════════════════
phase7() {
banner "DONE"
# ── Save credentials ──
cat > "$CREDS_FILE" << EOCREDS
# THE GRIND — Credentials ($(date -u '+%Y-%m-%d %H:%M:%S UTC'))
FRONTEND_URL=${FE_URL}
BACKEND_URL=${BE_URL}
BACKEND_WS=${BE_WS}
CAPROVER_URL=${CAPTAIN_URL}
MINIO_CONSOLE=${MINIO_URL}
# DB
DATABASE_URL=${DATABASE_URL}
DB_PASSWORD=${DB_PASSWORD}
# Redis
REDIS_HOST=${REDIS_HOST}
# MinIO
MINIO_USER=${MINIO_USER}
MINIO_PASS=${MINIO_PASS}
MINIO_BUCKET=${MINIO_BUCKET}
# JWT
JWT_SECRET=${JWT_SECRET}
# Commands
# Backend logs: docker logs \$(docker ps -qf name=srv-captain--${APP_BACKEND}) -f --tail 200
# Frontend logs: docker logs \$(docker ps -qf name=srv-captain--${APP_FRONTEND}) -f --tail 200
# DB seed: docker exec \$(docker ps -qf name=srv-captain--${APP_BACKEND}) npx prisma db seed --schema=./prisma/schema.prisma
# Restart BE: docker restart \$(docker ps -qf name=srv-captain--${APP_BACKEND})
# Restart FE: docker restart \$(docker ps -qf name=srv-captain--${APP_FRONTEND})
# Shell BE: docker exec -it \$(docker ps -qf name=srv-captain--${APP_BACKEND}) sh
EOCREDS
chmod 600 "$CREDS_FILE"
echo ""
echo -e "${G}${B}╔═══════════════════════════════════════════════════════════╗${N}"
echo -e "${G}${B}║ 🔥 THE GRIND — DEPLOYED 🔥 ║${N}"
echo -e "${G}${B}╚═══════════════════════════════════════════════════════════╝${N}"
echo ""
echo -e " ${B}🌐 Frontend:${N} ${C}${FE_URL}${N}"
echo -e " ${B}🔧 Backend API:${N} ${C}${BE_URL}/api${N}"
echo -e " ${B}📦 MinIO:${N} ${C}${MINIO_URL}${N}"
echo -e " ${B}🚀 CapRover:${N} ${C}${CAPTAIN_URL}${N}"
echo ""
line
echo -e " ${B}PostgreSQL:${N} ${D}${DB_HOST}:5432 / ${DB_NAME} / ${DB_PASSWORD}${N}"
echo -e " ${B}Redis:${N} ${D}${REDIS_HOST}:6379${N}"
echo -e " ${B}MinIO:${N} ${D}${MINIO_USER} / ${MINIO_PASS}${N}"
echo -e " ${B}JWT:${N} ${D}${JWT_SECRET:0:24}...${N}"
line
echo ""
echo -e " ${Y}${B}TODO:${N}"
echo -e " 1. Check builds: ${D}${CAPTAIN_URL}${N}"
echo -e " 2. Health: ${D}curl -k ${BE_URL}/api/health${N}"
echo -e " 3. Seed (if failed): ${D}docker exec \$(docker ps -qf name=srv-captain--${APP_BACKEND}) npx prisma db seed --schema=./prisma/schema.prisma${N}"
echo -e " 4. Credentials: ${D}cat ${CREDS_FILE}${N}"
echo -e " 5. ${R}CHANGE YOUR CAPROVER PASSWORD${N}"
echo ""
echo -e " ${B}Redeploy:${N}"
echo -e " ${D}cd /tmp && rm -rf hrsystem && git clone --depth 1 http://${GITLAB_HOST}/${GITLAB_REPO_PATH}.git && cd hrsystem${N}"
echo -e " ${D}cp captain-definition-backend captain-definition && caprover deploy -a ${APP_BACKEND}${N}"
echo -e " ${D}cp captain-definition-frontend captain-definition && caprover deploy -a ${APP_FRONTEND}${N}"
echo ""
rm -rf "$WORK_DIR" 2>/dev/null || true
}
# ═══════════════════════════════════════════════════════════════
# MAIN
# ═══════════════════════════════════════════════════════════════
main() {
echo -e "${M}${B}"
cat << 'ART'
╔════════════════════════════════════════════════════╗
║ ████████╗██╗ ██╗███████╗ ║
║ ██╔══╝██║ ██║██╔════╝ ║
║ ██║ ███████║█████╗ ║
║ ██║ ██╔══██║██╔══╝ ║
║ ██║ ██║ ██║███████╗ ║
║ ╚═╝ ╚═╝ ╚═╝╚══════╝ ║
║ ██████╗ ██████╗ ██╗███╗ ██╗██████╗ ║
║ ██╔════╝ ██╔══██╗██║████╗ ██║██╔══██╗ ║
║ ██║ ███╗██████╔╝██║██╔██╗ ██║██║ ██║ ║
║ ██║ ██║██╔══██╗██║██║╚██╗██║██║ ██║ ║
║ ╚██████╔╝██║ ██║██║██║ ╚████║██████╔╝ ║
║ ╚═════╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═════╝ ║
║ ║
║ CapRover Deploy v3 — Son of Anton ║
╚════════════════════════════════════════════════════╝
ART
echo -e "${N}"
local T=$SECONDS
phase0; phase1; phase2; phase3; phase4; phase5; phase6; phase7
local E=$(( SECONDS - T ))
echo -e " ${D}Total: $((E/60))m $((E%60))s${N}\n"
}
main "$@"
\ No newline at end of file
/** @type {import('next').NextConfig} */
const nextConfig = {
// CRITICAL: Required for Docker/CapRover deployment
output: 'standalone',
reactStrictMode: true,
// Transpile the shared workspace package
transpilePackages: ['shared'],
// Image optimization — disable external loader since self-hosted
images: {
unoptimized: true,
},
// Suppress build errors so deployment doesn't fail on type warnings
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
// Strip console.log in production (keep errors/warns)
images: { unoptimized: true },
eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
compiler: {
removeConsole:
process.env.NODE_ENV === 'production'
? { exclude: ['error', 'warn'] }
: false,
},
// Proxy API requests in development (not used in production — frontend calls API directly)
async rewrites() {
// Only apply in dev; in prod the frontend calls the API URL directly
if (process.env.NODE_ENV === 'development') {
return [
{
source: '/api/:path*',
destination: `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/api/:path*`,
},
];
}
return [];
},
// Security headers
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
],
removeConsole: process.env.NODE_ENV === 'production' ? { exclude: ['error', 'warn'] } : false,
},
];
// Allow API URL to be set at build time
env: {
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
NEXT_PUBLIC_WS_URL: process.env.NEXT_PUBLIC_WS_URL,
},
};
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment