From e9c18567aba1fecc0a78dc97fefc0d2cfa5fa441 Mon Sep 17 00:00:00 2001 From: Rajkumar Date: Sat, 12 Sep 2026 16:44:19 +0530 Subject: [PATCH] (Deployment) deployment environment setup done to vps --- .github/workflows/cd.yml | 2 +- .github/workflows/ci.yml | 5 --- .gitignore | 2 +- ARCHITECTURE.md | 79 ++++++++++++++++++++++------------------ README.md | 4 ++ docker-compose.prod.yml | 71 +++++++++--------------------------- 6 files changed, 67 insertions(+), 96 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 386e413..8f55f51 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -129,7 +129,7 @@ jobs: docker compose -f docker-compose.prod.yml ps # Fail the deploy if any expected service is not running. - for svc in frontend backend ws-server genai db redis; do + for svc in frontend backend ws-server genai; do state=$(docker compose -f docker-compose.prod.yml ps \ --format '{{.State}}' "$svc" || true) echo " $svc: ${state:-missing}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1e26d4..a70acb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -196,11 +196,6 @@ jobs: docker compose -f docker-compose.yml config --quiet - name: Validate prod compose - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: rexial - REDIS_PASSWORD: placeholder run: | # .env.prod lives on the deploy server and is not in the repo, # so stand in an empty one just to check the file's structure. diff --git a/.gitignore b/.gitignore index 73a66ca..c2859cc 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ node_modules .env.development.local .env.test.local .env.production.local - +VPS-DEPLOYMENT.md # Testing coverage diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1b0fc60..d2d8176 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -10,18 +10,18 @@ running, then come back for the *why*. ## Contents --[The short version](#the-short-version) --[System overview](#system-overview) --[Why a monorepo](#why-a-monorepo) --[The services](#the-services) --[Data model](#data-model) --[Flow 1: Creating a quiz](#flow-1-creating-a-quiz) --[Flow 2: AI question generation](#flow-2-ai-question-generation) --[Flow 3: Running a live quiz](#flow-3-running-a-live-quiz) --[Caching and scale](#caching-and-scale) --[Deployment topology](#deployment-topology) --[Design decisions](#design-decisions) --[Known limitations](#known-limitations) +- [The short version](#the-short-version) +- [System overview](#system-overview) +- [Why a monorepo](#why-a-monorepo) +- [The services](#the-services) +- [Data model](#data-model) +- [Flow 1: Creating a quiz](#flow-1-creating-a-quiz) +- [Flow 2: AI question generation](#flow-2-ai-question-generation) +- [Flow 3: Running a live quiz](#flow-3-running-a-live-quiz) +- [Caching and scale](#caching-and-scale) +- [Deployment topology](#deployment-topology) +- [Design decisions](#design-decisions) +- [Known limitations](#known-limitations) --- @@ -453,33 +453,40 @@ instance, but nothing in the code assumes that. ## Deployment topology ``` - Internet - │ - ▼ - ┌────────────────────────┐ - │ nginx-proxy-manager │ :80 :443 (TLS, routing) - │ │ :81 (admin UI) - └──┬─────┬─────┬─────┬───┘ - │ │ │ │ - ┌────────▼┐ ┌──▼───┐ ┌▼─────┐ ┌▼──────┐ - │frontend │ │backend│ │ ws │ │ genai │ - │ (nginx) │ │ :4000 │ │:8080 │ │ :8000 │ - └─────────┘ └───┬───┘ └──┬───┘ └───────┘ - │ │ - ┌─────▼────┐ ┌─▼──────┐ - │ postgres │ │ redis │ - │ (volume) │ │(volume)│ - └──────────┘ └────────┘ - - all on the private `rexial-network` bridge + Internet + │ + ▼ :80 :443 + ┌───────────────────── VPS ──────────────────────────┐ + │ ┌────────────────────────┐ │ + │ │ nginx-proxy-manager │ TLS, routing │ + │ │ (:81 admin, loopback) │ │ + │ └──┬─────┬─────┬─────┬───┘ │ + │ │ │ │ │ │ + │ ┌───────▼─┐ ┌─▼─────┐ ┌───▼──┐ ┌▼──────┐ │ + │ │frontend │ │backend│ │ ws │ │ genai │ Docker │ + │ │ (nginx) │ │ :4000 │ │:8080 │ │ :8000 │ bridge │ + │ └─────────┘ └───┬───┘ └──┬───┘ └───────┘ │ + │ │ │ │ + │ host.docker.internal -> 172.17.0.1 │ + │ │ │ │ + │ ┌───────────────▼────────▼────────────────────┐ │ + │ │ host services (systemd, NOT containers) │ │ + │ │ postgresql :5432 redis :6379 │ │ + │ └─────────────────────────────────────────────┘ │ + └────────────────────────────────────────────────────┘ + + containers on the private `rexial-network` bridge ``` -Only nginx is exposed. Everything else is reachable only inside the Docker -network. +**PostgreSQL and Redis run on the host**, installed with apt, not as +containers. The containers that need them declare +`extra_hosts: host.docker.internal:host-gateway`, which resolves to the Docker +bridge gateway. The host services must bind that interface, and the firewall +must restrict those ports to the bridge subnet — see +[VPS-DEPLOYMENT.md](VPS-DEPLOYMENT.md). -> **Warning:** Port **81** is the nginx-proxy-manager admin UI. It is published on the ->host, so it should be firewalled to trusted IPs — anyone who reaches it can ->re-route your traffic. +Only ports 80 and 443 are public. The nginx admin UI is bound to `127.0.0.1:81` +and reached through an SSH tunnel. Images are built by GitHub Actions and tagged `:latest` **and** `:`; the deploy pins the SHA so rollback is exact. See diff --git a/README.md b/README.md index 1e1bd6d..aa2eaee 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,10 @@ The platform evolves in multiple versions: boundaries, data model, request flows, caching, and the design decisions behind them. +**[VPS deployment guide →](VPS-DEPLOYMENT.md)** — step-by-step setup on a +single server, with PostgreSQL and Redis installed on the host rather than in +containers. + --- ## Getting Started diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index e1f80af..e08fe93 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,3 +1,12 @@ +## Production stack. +## +## PostgreSQL and Redis run on the HOST (installed via apt), not as containers. +## Containers reach them through `host.docker.internal`, which `extra_hosts` +## maps to the Docker bridge gateway (172.17.0.1). +## +## The host services must listen on that bridge and be firewalled to it. +## See VPS-DEPLOYMENT.md for the full setup. + services: nginx: @@ -7,7 +16,9 @@ services: ports: - "80:80" - "443:443" - - "81:81" + # Admin UI on localhost only - tunnel in: + # ssh -L 8181:localhost:81 user@your-vps + - "127.0.0.1:81:81" volumes: - nginx_data:/data @@ -50,9 +61,9 @@ services: networks: - rexial-network - depends_on: - db: - condition: service_healthy + # Reach PostgreSQL and Redis running on the host. + extra_hosts: + - "host.docker.internal:host-gateway" # now run the migrations command: > @@ -73,9 +84,9 @@ services: - .env.prod networks: - rexial-network - depends_on: - db: - condition: service_healthy + # Reach PostgreSQL and Redis running on the host. + extra_hosts: + - "host.docker.internal:host-gateway" command: > sh -c "pnpm --filter @repo/db run db:generate:prod && pnpm run start" @@ -102,57 +113,11 @@ services: retries: 5 start_period: 60s - db: - image: postgres:15-alpine - container_name: rexial_db_prod - restart: always - env_file: - - .env.prod - volumes: - - Rexial_data:/var/lib/postgresql/data - networks: - - rexial-network - healthcheck: - test: [ "CMD-SHELL", "pg_isready -U postgres -d Rexial" ] - interval: 10s - timeout: 5s - retries: 10 - start_period: 30s - - redis: - image: redis:7-alpine - container_name: rexial_redis_prod - restart: always - env_file: - - .env.prod - command: - - redis-server - - --requirepass - - ${REDIS_PASSWORD} - - --maxmemory - - 256mb - - --maxmemory-policy - - allkeys-lru - - --appendonly - - yes - volumes: - - redis_data:/data - networks: - - rexial-network - healthcheck: - test: [ "CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping" ] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - networks: rexial-network: driver: bridge volumes: - Rexial_data: genai_uploads: nginx_data: letsencrypt: - redis_data: