feat: update after deploy

This commit is contained in:
Oleg Proskurin 2025-12-24 00:53:20 +07:00
parent b9c998f33d
commit 970a0f75c6
8 changed files with 645 additions and 1 deletions

View File

@ -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)

215
docs/deployment.md Normal file
View File

@ -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 <repo> ~/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:<password>@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=<generated>
MINIO_ROOT_USER=banatie_admin
MINIO_ROOT_PASSWORD=<generated>
MINIO_ACCESS_KEY=banatie_service
MINIO_SECRET_KEY=<generated>
GEMINI_API_KEY=<your-key>
JWT_SECRET=<generated>
SESSION_SECRET=<generated>
```
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`

View File

@ -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:<url-encoded-password>@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

64
infrastructure/README.md Normal file
View File

@ -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`

View File

@ -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

View File

@ -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 $$;

View File

@ -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=<generate-strong-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=<generate-strong-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=<generate-strong-password>
# ----------------------------------------
# API Secrets
# ----------------------------------------
# Google Gemini API key for image generation
# Get from: https://aistudio.google.com/app/apikey
GEMINI_API_KEY=<your-gemini-api-key>
# JWT secret for token signing
# Generate: openssl rand -base64 64 | tr -d '\n\r '
JWT_SECRET=<generate-strong-secret>
# Session secret for Express sessions
# Generate: openssl rand -base64 32 | tr -d '\n\r '
SESSION_SECRET=<generate-strong-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=<add-manually>"

View File

@ -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",