From 970a0f75c695babe5597439cd041368ed0a13708 Mon Sep 17 00:00:00 2001 From: Oleg Proskurin Date: Wed, 24 Dec 2025 00:53:20 +0700 Subject: [PATCH] feat: update after deploy --- CLAUDE.md | 46 ++++ docs/deployment.md | 215 +++++++++++++++++++ infrastructure/.env.example | 57 +++++ infrastructure/README.md | 64 ++++++ infrastructure/docker-compose.production.yml | 181 ++++++++++++++++ infrastructure/init-db.sql | 20 ++ infrastructure/secrets.env.example | 60 ++++++ package.json | 3 +- 8 files changed, 645 insertions(+), 1 deletion(-) create mode 100644 docs/deployment.md create mode 100644 infrastructure/.env.example create mode 100644 infrastructure/README.md create mode 100644 infrastructure/docker-compose.production.yml create mode 100644 infrastructure/init-db.sql create mode 100644 infrastructure/secrets.env.example diff --git a/CLAUDE.md b/CLAUDE.md index 817f81f..6ec593f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -300,6 +300,52 @@ curl -X POST http://localhost:3000/api/upload \ - **Rate Limits**: 100 requests per hour per key - **Revocation**: Soft delete via `is_active` flag +## Production Deployment + +### VPS Infrastructure + +Banatie is deployed as an isolated ecosystem on VPS at `/opt/banatie/`: + +| Service | URL | Container | +|---------|-----|-----------| +| Landing | https://banatie.app | banatie-landing | +| API | https://api.banatie.app | banatie-api | +| MinIO Console | https://storage.banatie.app | banatie-minio | +| MinIO CDN | https://cdn.banatie.app | banatie-minio | + +### Deploy Scripts + +```bash +# From project root +./scripts/deploy-landing.sh # Deploy landing +./scripts/deploy-landing.sh --no-cache # Force rebuild (when deps change) +./scripts/deploy-api.sh # Deploy API +./scripts/deploy-api.sh --no-cache # Force rebuild +``` + +### Production Configuration Files + +``` +infrastructure/ +├── docker-compose.production.yml # VPS docker-compose +├── .env.example # Environment variables template +└── secrets.env.example # Secrets template +``` + +### Key Production Learnings + +1. **NEXT_PUBLIC_* variables** - Must be set at build time AND runtime for Next.js client-side code +2. **pnpm workspaces in Docker** - Symlinks break between stages; use single-stage install with `pnpm --filter` +3. **Docker User NS Remapping** - VPS uses UID offset 165536; container UID 1001 → host UID 166537 +4. **DATABASE_URL encoding** - Special characters like `=` must be URL-encoded (`%3D`) + +### Known Production Issues (Non-Critical) + +1. **Healthcheck showing "unhealthy"** - Alpine images lack curl; services work correctly +2. **Next.js cache permission** - `.next/cache` may show EACCES; non-critical for functionality + +See [docs/deployment.md](docs/deployment.md) for full deployment guide. + ## Development Notes - Uses pnpm workspaces for monorepo management (required >= 8.0.0) diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..0ecf3b3 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,215 @@ +# Banatie Production Deployment Guide + +> Last Updated: December 23, 2025 + +This guide covers deploying Banatie to a VPS with Docker. For local development, see [environment.md](./environment.md). + +## Overview + +Banatie is deployed as an isolated ecosystem with: + +- **Landing** (Next.js 15.5.9) → banatie.app +- **API** (Express.js) → api.banatie.app +- **PostgreSQL** (15-alpine) → Database +- **MinIO** (SNMD mode) → Object storage + +## Prerequisites + +- VPS with Docker and Docker Compose +- Caddy reverse proxy (or similar) with SSL +- DNS records configured +- GEMINI_API_KEY from Google AI Studio + +## Quick Start + +```bash +# 1. Create directory structure +sudo mkdir -p /opt/banatie/{data,logs,scripts} +sudo mkdir -p /opt/banatie/data/{postgres,minio,waitlist-logs,api-results,api-uploads} +sudo mkdir -p /opt/banatie/data/minio/{drive1,drive2,drive3,drive4} +sudo chown -R $USER:$USER /opt/banatie + +# 2. Clone repository +git clone ~/workspace/projects/banatie-service + +# 3. Copy production configs +cp ~/workspace/projects/banatie-service/infrastructure/docker-compose.production.yml /opt/banatie/docker-compose.yml +cp ~/workspace/projects/banatie-service/infrastructure/.env.example /opt/banatie/.env +cp ~/workspace/projects/banatie-service/infrastructure/secrets.env.example /opt/banatie/secrets.env +cp ~/workspace/projects/banatie-service/infrastructure/init-db.sql /opt/banatie/scripts/ + +# 4. Configure environment +nano /opt/banatie/.env # Edit public variables +nano /opt/banatie/secrets.env # Generate and add secrets +chmod 600 /opt/banatie/secrets.env + +# 5. Build and start +cd /opt/banatie +docker compose --env-file .env --env-file secrets.env build +docker compose --env-file .env --env-file secrets.env up -d + +# 6. Initialize database schema +cd ~/workspace/projects/banatie-service +pnpm install +pnpm --filter @banatie/database db:push + +# 7. Create master API key +curl -X POST https://api.banatie.app/api/bootstrap/initial-key +# Or use UI: https://banatie.app/admin/master/ +``` + +## Deploy Scripts + +Located in `scripts/` directory: + +```bash +# Deploy landing page +./scripts/deploy-landing.sh # Normal deploy +./scripts/deploy-landing.sh --no-cache # Fresh build (when deps change) + +# Deploy API +./scripts/deploy-api.sh # Normal deploy +./scripts/deploy-api.sh --no-cache # Fresh build +``` + +## Configuration Files + +### Environment Variables (.env) + +```bash +NODE_ENV=production +PORT=3000 +POSTGRES_DB=banatie_db +POSTGRES_USER=banatie_user +DATABASE_URL=postgresql://banatie_user:@banatie-postgres:5432/banatie_db +MINIO_ENDPOINT=banatie-minio:9000 +MINIO_PUBLIC_URL=https://cdn.banatie.app +API_PUBLIC_URL=https://api.banatie.app +NEXT_PUBLIC_API_URL=https://api.banatie.app +CORS_ORIGIN=https://banatie.app,https://api.banatie.app +``` + +### Secrets (secrets.env) + +```bash +POSTGRES_PASSWORD= +MINIO_ROOT_USER=banatie_admin +MINIO_ROOT_PASSWORD= +MINIO_ACCESS_KEY=banatie_service +MINIO_SECRET_KEY= +GEMINI_API_KEY= +JWT_SECRET= +SESSION_SECRET= +``` + +Generate secrets with: +```bash +openssl rand -base64 32 | tr -d '\n\r ' +``` + +## DNS Configuration + +| Type | Name | Value | +|------|------|-------| +| A | @ | VPS_IP | +| CNAME | www | banatie.app | +| CNAME | api | banatie.app | +| CNAME | storage | banatie.app | +| CNAME | cdn | banatie.app | + +## Caddy Configuration + +Add to your Caddyfile: + +```caddy +www.banatie.app { + redir https://banatie.app{uri} permanent +} + +banatie.app { + reverse_proxy banatie-landing:3000 +} + +api.banatie.app { + reverse_proxy banatie-api:3000 +} + +storage.banatie.app { + reverse_proxy banatie-minio:9001 +} + +cdn.banatie.app { + reverse_proxy banatie-minio:9000 + header Access-Control-Allow-Origin "*" +} +``` + +## Troubleshooting + +### Permission Denied on Volumes + +Docker User Namespace Remapping offsets UIDs by 165536: + +```bash +# Fix permissions for Next.js (uid 1001 → 166537) +sudo chown -R 166537:166537 /opt/banatie/data/waitlist-logs + +# Fix permissions for API (uid 1001 → 166537) +sudo chown -R 166537:166537 /opt/banatie/data/api-results +sudo chown -R 166537:166537 /opt/banatie/data/api-uploads +``` + +### Environment Variables Not Applied + +Use `docker compose up -d` instead of `docker restart`: + +```bash +docker compose --env-file .env --env-file secrets.env up -d banatie-api +``` + +### NEXT_PUBLIC_* Variables + +Must be set at both build time AND runtime. Ensure `NEXT_PUBLIC_API_URL` is in .env before building. + +### pnpm Workspace Symlinks in Docker + +The Dockerfiles use simplified single-stage install to avoid symlink issues: + +```dockerfile +COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ +COPY apps/landing ./apps/landing +COPY packages/database ./packages/database +RUN pnpm install --frozen-lockfile +RUN pnpm --filter @banatie/landing build +``` + +### Database Connection Refused + +URL-encode special characters in DATABASE_URL: +- `=` → `%3D` +- `@` → `%40` +- `#` → `%23` + +## Known Issues + +### Healthcheck Showing "Unhealthy" + +Alpine images don't have `curl` by default. The healthcheck uses `wget` but may still show unhealthy in some cases. Services work correctly despite this status. + +### Next.js Cache Permission Warning + +``` +EACCES: permission denied, mkdir '/app/apps/landing/.next/cache' +``` + +This is non-critical - images still work, just not cached. To fix: +```bash +sudo chown -R 166537:166537 /opt/banatie/data/landing-cache +# And add volume mount for .next/cache +``` + +## Full VPS Documentation + +For complete VPS setup and infrastructure details, see: +- VPS Repository: `VPS/docs/banatie-deployment.md` +- Deployment Manual: `VPS/manuals/banatie-service-deployment.md` diff --git a/infrastructure/.env.example b/infrastructure/.env.example new file mode 100644 index 0000000..7173962 --- /dev/null +++ b/infrastructure/.env.example @@ -0,0 +1,57 @@ +# Banatie Production Environment Variables +# ========================================== +# Copy this file to .env and fill in the values +# +# Location on VPS: /opt/banatie/.env +# Last Updated: December 23, 2025 + +# ---------------------------------------- +# Node.js Configuration +# ---------------------------------------- +NODE_ENV=production +PORT=3000 + +# ---------------------------------------- +# PostgreSQL Database +# ---------------------------------------- +POSTGRES_DB=banatie_db +POSTGRES_USER=banatie_user +# Note: POSTGRES_PASSWORD is in secrets.env + +# DATABASE_URL for application use +# IMPORTANT: URL-encode special characters (e.g., = → %3D, @ → %40) +# Example: DATABASE_URL=postgresql://banatie_user:MyP%3Dssword@banatie-postgres:5432/banatie_db +DATABASE_URL=postgresql://banatie_user:@banatie-postgres:5432/banatie_db + +# ---------------------------------------- +# MinIO Object Storage +# ---------------------------------------- +MINIO_ENDPOINT=banatie-minio:9000 +MINIO_BUCKET_NAME=banatie +MINIO_USE_SSL=false +STORAGE_TYPE=minio + +# Public URL for CDN access (used in API responses) +MINIO_PUBLIC_URL=https://cdn.banatie.app + +# ---------------------------------------- +# API Configuration +# ---------------------------------------- +API_BASE_URL=https://api.banatie.app +API_PUBLIC_URL=https://api.banatie.app + +# IMPORTANT: This must be set for Next.js client-side code +NEXT_PUBLIC_API_URL=https://api.banatie.app + +# ---------------------------------------- +# CORS Configuration +# ---------------------------------------- +# Comma-separated list of allowed origins +CORS_ORIGIN=https://banatie.app,https://api.banatie.app + +# ---------------------------------------- +# Multi-tenancy Defaults +# ---------------------------------------- +DEFAULT_ORG_ID=default +DEFAULT_PROJECT_ID=main +DEFAULT_USER_ID=system diff --git a/infrastructure/README.md b/infrastructure/README.md new file mode 100644 index 0000000..3af4e9d --- /dev/null +++ b/infrastructure/README.md @@ -0,0 +1,64 @@ +# Banatie Infrastructure + +Production deployment configuration files for VPS. + +## Files + +| File | Purpose | +|------|---------| +| `docker-compose.production.yml` | Docker Compose for VPS deployment | +| `.env.example` | Environment variables template | +| `secrets.env.example` | Secrets template (passwords, API keys) | +| `init-db.sql` | PostgreSQL initialization (grants permissions) | + +## Usage + +```bash +# Copy to VPS +scp -r infrastructure/* usul-vps:/opt/banatie/ + +# On VPS +cd /opt/banatie +cp docker-compose.production.yml docker-compose.yml +cp .env.example .env +cp secrets.env.example secrets.env +cp init-db.sql scripts/ + +# Edit configuration +nano .env +nano secrets.env +chmod 600 secrets.env + +# Deploy +docker compose --env-file .env --env-file secrets.env up -d +``` + +## VPS Directory Structure + +``` +/opt/banatie/ +├── docker-compose.yml # Copy from docker-compose.production.yml +├── .env # Copy from .env.example and configure +├── secrets.env # Copy from secrets.env.example and configure +├── scripts/ +│ └── init-db.sql # Copy from init-db.sql +├── data/ +│ ├── postgres/ # PostgreSQL data (DO NOT CREATE MANUALLY) +│ ├── minio/ # MinIO drives (DO NOT CREATE MANUALLY) +│ ├── waitlist-logs/ +│ ├── api-results/ +│ └── api-uploads/ +└── logs/ + └── api/ +``` + +## Important Notes + +1. **Docker User NS Remapping**: VPS uses UID offset 165536. Let Docker create data directories. +2. **Secrets**: Never commit secrets.env to git. Use `chmod 600`. +3. **Database**: Tables are created by Drizzle ORM, not init-db.sql. + +## Documentation + +- Full guide: [docs/deployment.md](../docs/deployment.md) +- VPS docs: VPS repo `docs/banatie-deployment.md` diff --git a/infrastructure/docker-compose.production.yml b/infrastructure/docker-compose.production.yml new file mode 100644 index 0000000..b998974 --- /dev/null +++ b/infrastructure/docker-compose.production.yml @@ -0,0 +1,181 @@ +# Banatie Production - VPS Isolated Deployment +# ============================================ +# This is the production docker-compose file used on VPS at /opt/banatie/ +# Last Updated: December 23, 2025 +# +# Usage: +# docker compose --env-file .env --env-file secrets.env up -d +# docker compose --env-file .env --env-file secrets.env build --no-cache +# +# Key differences from dev: +# - Uses external proxy-network for Caddy integration +# - All services isolated in banatie-internal network +# - MinIO with 4 drives for full S3 compatibility +# - Secrets stored in separate secrets.env file + +services: + # ---------------------------------------- + # API Service - Express.js REST API + # ---------------------------------------- + banatie-api: + build: + context: /home/usul/workspace/projects/banatie-service + dockerfile: apps/api-service/Dockerfile + target: production + container_name: banatie-api + restart: unless-stopped + networks: + - banatie-internal + - proxy-network + depends_on: + banatie-postgres: + condition: service_healthy + banatie-minio: + condition: service_healthy + env_file: + - .env + - secrets.env + environment: + - IS_DOCKER=true + - NODE_ENV=production + volumes: + - ./logs/api:/app/apps/api-service/logs + - ./data/api-results:/app/results + - ./data/api-uploads:/app/uploads + healthcheck: + # Note: Alpine images don't have curl by default + # Using wget instead, but may still show "unhealthy" - service works correctly + test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + # ---------------------------------------- + # Landing Page - Next.js 15.5.9 + # ---------------------------------------- + banatie-landing: + build: + context: /home/usul/workspace/projects/banatie-service + dockerfile: apps/landing/Dockerfile + container_name: banatie-landing + restart: unless-stopped + networks: + - banatie-internal + - proxy-network + depends_on: + - banatie-postgres + env_file: + - .env + - secrets.env + environment: + - IS_DOCKER=true + - NODE_ENV=production + - HOSTNAME=0.0.0.0 + - WAITLIST_LOGS_PATH=/app/waitlist-logs + volumes: + - ./data/waitlist-logs:/app/waitlist-logs + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + # ---------------------------------------- + # PostgreSQL Database + # ---------------------------------------- + banatie-postgres: + image: postgres:15-alpine + container_name: banatie-postgres + restart: unless-stopped + networks: + - banatie-internal + volumes: + - ./data/postgres:/var/lib/postgresql/data + - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/01-init.sql:ro + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # ---------------------------------------- + # MinIO Object Storage (S3-compatible) + # ---------------------------------------- + banatie-minio: + image: quay.io/minio/minio:latest + container_name: banatie-minio + restart: unless-stopped + networks: + - banatie-internal + - proxy-network + volumes: + # 4 drives for SNMD mode (full S3 compatibility) + - ./data/minio/drive1:/data1 + - ./data/minio/drive2:/data2 + - ./data/minio/drive3:/data3 + - ./data/minio/drive4:/data4 + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + MINIO_BROWSER_REDIRECT_URL: https://storage.banatie.app + MINIO_SERVER_URL: https://cdn.banatie.app + command: server /data{1...4} --console-address ":9001" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # ---------------------------------------- + # Storage Initialization (runs once) + # ---------------------------------------- + banatie-storage-init: + image: minio/mc:latest + container_name: banatie-storage-init + networks: + - banatie-internal + depends_on: + banatie-minio: + condition: service_healthy + env_file: + - secrets.env + entrypoint: + - /bin/sh + - -c + - | + echo '=== MinIO Storage Initialization ===' + mc alias set storage http://banatie-minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD} + mc mb --ignore-existing storage/banatie + mc admin user add storage $${MINIO_ACCESS_KEY} $${MINIO_SECRET_KEY} || echo 'User may already exist' + mc admin policy attach storage readwrite --user=$${MINIO_ACCESS_KEY} || echo 'Policy may already be attached' + cat > /tmp/lifecycle.json <<'LCEOF' + {"Rules":[{"ID":"temp-cleanup","Status":"Enabled","Filter":{"Prefix":"temp/"},"Expiration":{"Days":7}}]} + LCEOF + mc ilm import storage/banatie < /tmp/lifecycle.json || echo 'Lifecycle policy may already exist' + echo '=== Storage Initialization Completed ===' + exit 0 + restart: "no" + +# ---------------------------------------- +# Networks +# ---------------------------------------- +networks: + # Internal network for service communication + # internal: true means no outbound access + banatie-internal: + driver: bridge + internal: true + + # External network shared with Caddy reverse proxy + # Must be created by Caddy's docker-compose first + proxy-network: + name: services_proxy-network + external: true diff --git a/infrastructure/init-db.sql b/infrastructure/init-db.sql new file mode 100644 index 0000000..d597c86 --- /dev/null +++ b/infrastructure/init-db.sql @@ -0,0 +1,20 @@ +-- Banatie PostgreSQL 15 Permission Fix +-- ===================================== +-- This script runs only on first PostgreSQL startup. +-- It grants necessary permissions for the Drizzle ORM to create tables. +-- +-- Note: Actual tables are created by Drizzle ORM during deployment: +-- pnpm --filter @banatie/database db:push +-- +-- PostgreSQL 15+ removed default CREATE privileges on public schema for security. +-- This script restores those privileges for the service user. + +-- Grant CREATE permission on public schema +GRANT CREATE ON SCHEMA public TO banatie_user; +GRANT ALL ON SCHEMA public TO banatie_user; + +-- Log completion +DO $$ +BEGIN + RAISE NOTICE 'Banatie database initialized. Run db:push to create tables.'; +END $$; diff --git a/infrastructure/secrets.env.example b/infrastructure/secrets.env.example new file mode 100644 index 0000000..e88c8fd --- /dev/null +++ b/infrastructure/secrets.env.example @@ -0,0 +1,60 @@ +# Banatie Production Secrets +# ========================== +# NEVER COMMIT THIS FILE TO GIT! +# +# Copy this file to secrets.env and generate real values +# Location on VPS: /opt/banatie/secrets.env +# Permissions: chmod 600 secrets.env +# +# Last Updated: December 23, 2025 + +# ---------------------------------------- +# PostgreSQL Secrets +# ---------------------------------------- +# Generate: openssl rand -base64 32 | tr -d '\n\r ' +POSTGRES_PASSWORD= + +# ---------------------------------------- +# MinIO Root Credentials +# ---------------------------------------- +# Root user for MinIO admin console +MINIO_ROOT_USER=banatie_admin +# Generate: openssl rand -base64 32 | tr -d '\n\r ' +MINIO_ROOT_PASSWORD= + +# ---------------------------------------- +# MinIO Service Account +# ---------------------------------------- +# Service account for API access to MinIO +MINIO_ACCESS_KEY=banatie_service +# Generate: openssl rand -base64 32 | tr -d '\n\r ' +MINIO_SECRET_KEY= + +# ---------------------------------------- +# API Secrets +# ---------------------------------------- +# Google Gemini API key for image generation +# Get from: https://aistudio.google.com/app/apikey +GEMINI_API_KEY= + +# JWT secret for token signing +# Generate: openssl rand -base64 64 | tr -d '\n\r ' +JWT_SECRET= + +# Session secret for Express sessions +# Generate: openssl rand -base64 32 | tr -d '\n\r ' +SESSION_SECRET= + +# ---------------------------------------- +# Quick Generation Script +# ---------------------------------------- +# Run this to generate all secrets: +# +# echo "POSTGRES_PASSWORD=$(openssl rand -base64 32 | tr -d '\n\r ')" +# echo "MINIO_ROOT_USER=banatie_admin" +# echo "MINIO_ROOT_PASSWORD=$(openssl rand -base64 32 | tr -d '\n\r ')" +# echo "MINIO_ACCESS_KEY=banatie_service" +# echo "MINIO_SECRET_KEY=$(openssl rand -base64 32 | tr -d '\n\r ')" +# echo "JWT_SECRET=$(openssl rand -base64 64 | tr -d '\n\r ')" +# echo "SESSION_SECRET=$(openssl rand -base64 32 | tr -d '\n\r ')" +# echo "GEMINI_API_KEY=" diff --git a/package.json b/package.json index 57fbdf9..7c85e27 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "clean": "pnpm -r clean && rm -rf node_modules", "deploy:landing": "./scripts/deploy-landing.sh", "deploy:landing:no-cache": "./scripts/deploy-landing.sh --no-cache", - "deploy:api": "./scripts/deploy-landing.sh" + "deploy:api": "./scripts/deploy-api.sh", + "deploy:api:no-cache": "./scripts/deploy-api.sh --no-cache" }, "keywords": [ "monorepo",