--- title: "optical-dev Server — Apache Deployment Pattern" description: "Single-vhost Apache pattern on optical-dev.oliver.solutions GCP server — port allocation, Include fragments, SPA routing, deploy script best practices" tags: [architecture, apache, deployment, docker, ubuntu, gcp] created: 2026-04-17 updated: 2026-04-17 --- # optical-dev Server — Apache Deployment Pattern ## Server Profile | Field | Value | |-------|-------| | Hostname | optical-dev.oliver.solutions | | SSH alias | `optical-dev` (see `~/.ssh/config`) | | OS | Ubuntu 24.04 LTS | | Cloud | GCP europe-west2-b | | Web server | Apache 2.4.58 | | Docker | 29.3.0 | | Node.js | v22.22.2 | | npm | 10.9.7 | | Python | 3.12.3 | --- ## Apache Single-Vhost Pattern **One vhost file handles ALL projects:** ``` /etc/apache2/sites-available/optical-dev.oliver.solutions.conf ``` Each project is included as a fragment: ```apache ServerName optical-dev.oliver.solutions ... Include /opt/project-a/deploy/apache-project-a.conf Include /opt/project-b/deploy/apache-project-b.conf Include /opt/barclays-banner-builder/deploy/apache-barclays.conf ``` Deploy scripts inject the Include line automatically via sed (idempotent): ```bash INCLUDE_LINE=" Include /opt/barclays-banner-builder/deploy/apache-barclays.conf" if ! sudo grep -qF "" ""; then sudo sed -i "s|| |" "" sudo apache2ctl configtest && sudo systemctl reload apache2 fi ``` --- ## Apache Fragment Pattern ### SPA (React/Vue) with API backend ```apache # API proxy — strip project prefix so FastAPI sees /api/... ProxyPass /my-project/api/ http://127.0.0.1:PORT/api/ ProxyPassReverse /my-project/api/ http://127.0.0.1:PORT/api/ # Swagger docs ProxyPass /my-project/docs http://127.0.0.1:PORT/docs ProxyPassReverse /my-project/docs http://127.0.0.1:PORT/docs # SPA static files Alias /my-project /var/www/html/my-project Options -Indexes +FollowSymLinks AllowOverride None Require all granted RewriteEngine On RewriteBase /my-project/ # Pass real files/dirs through RewriteCond %{REQUEST_FILENAME} -f [OR] RewriteCond %{REQUEST_FILENAME} -d RewriteRule ^ - [L] # Everything else → index.html (React Router handles client-side nav) RewriteRule ^ index.html [L] ``` ### Key rules - MUST come BEFORE — Apache processes top to bottom - must include trailing slash - FollowSymLinks is required for symlinked static assets (e.g., illustrations folder) - Apache serves SPA directly from `/var/www/html/` — no Nginx container needed in prod --- ## Port Allocation Table ### Occupied (do NOT use) | Port | Service | |------|--------| | 3000 | Node app | | 3001 | Node app | | 3050 | Node app | | 3456 | Node app | | 5137 | Vite dev | | 5491 | Python app | | 5492 | Python app | | 8000 | OliVAS FastAPI | | 8001 | FastAPI | | 8002 | FastAPI | | 8040 | FastAPI | | 8800 | App | | 9000 | App | | 20201 | App | | 20202 | App | | 27017 | MongoDB | | 6389 | Redis (custom port) | | 6379 | Redis (standard — likely used) | ### Allocated | Port | Project | |------|--------| | 8010 | Barclays Banner Builder API | ### Available range When adding new projects, choose ports in ranges: **8011–8039**, **8041–8799**, **8801–8999** --- ## File System Layout | Path | Purpose | |------|--------| | `/opt//` | Git repo root for all projects | | `/var/www/html//` | Apache-served static files (React/Vue dist) | | `/etc/apache2/sites-available/optical-dev.oliver.solutions.conf` | Single vhost config | | `/opt//deploy/apache-.conf` | Per-project Apache Include fragment | | `/opt//.deploy_state/` | Build cache hashes (dockerfile_hash, npm_hash) | | `/opt//.env` | Production secrets (not in git) | --- ## Deploy Script Patterns ### 1. Hash-based build cache (avoid redundant rebuilds) ```bash # Skip Docker image rebuild if Dockerfile + pyproject unchanged DOCKERFILE_HASH=$(md5sum backend/Dockerfile pyproject.toml 2>/dev/null | md5sum | cut -d' ' -f1) LAST_HASH_FILE=".deploy_state/dockerfile_hash" if [[ -f "$LAST_HASH_FILE" ]] && [[ "$(cat "$LAST_HASH_FILE")" == "$DOCKERFILE_HASH" ]]; then echo "Dockerfile unchanged — skipping rebuild" else docker compose -f docker-compose.prod.yml build --parallel api worker echo "$DOCKERFILE_HASH" > "$LAST_HASH_FILE" fi # Same pattern for npm packages PKG_HASH=$(md5sum package.json package-lock.json 2>/dev/null | md5sum | cut -d' ' -f1) ``` ### 2. First-run detection via SQL COUNT ```bash # Idempotent — only seeds if table is empty USER_COUNT=$(docker compose exec -T api python -c " import asyncio from app.database import AsyncSessionLocal from sqlalchemy import text async def count(): async with AsyncSessionLocal() as db: r = await db.execute(text('SELECT COUNT(*) FROM users')) print(r.scalar()) asyncio.run(count()) " 2>/dev/null || echo "0") if [[ "$USER_COUNT" -eq "0" ]]; then docker compose exec -T api python scripts/seed_admin.py fi ``` ### 3. Postgres readiness check (before migrations) ```bash for i in $(seq 1 30); do if docker compose exec -T postgres pg_isready -U "${POSTGRES_USER}" &>/dev/null; then break fi [[ $i -eq 30 ]] && { echo "Postgres not ready"; exit 1; } sleep 1 done ``` ### 4. Frontend deploy (npm build → rsync to Apache dir) ```bash cd frontend VITE_BASE_PATH="/my-project" npm run build cd .. sudo mkdir -p /var/www/html/my-project sudo find /var/www/html/my-project -mindepth 1 -not -name 'illustrations' -delete sudo cp -r frontend/dist/. /var/www/html/my-project/ sudo chmod -R a+rX /var/www/html/my-project ``` ### 5. Symlink large static assets instead of copying ```bash # Avoids copying hundreds of MB of illustrations on every deploy if [[ ! -L "/var/www/html/my-project/illustrations" ]]; then sudo ln -sfn "/opt/my-project/assets/illustrations" "/var/www/html/my-project/illustrations" fi ``` --- ## Vite Subdirectory Configuration When React app lives at `/project/` (not root `/`): **vite.config.ts** ```ts export default defineConfig({ base: process.env.VITE_BASE_PATH ?? "/", }) ``` **main.tsx** ```tsx const basename = import.meta.env.VITE_BASE_PATH ?? "/"; ``` **api/client.ts** ```ts const API_PREFIX = import.meta.env.VITE_BASE_PATH ?? ""; // All fetch() calls: fetch(`${API_PREFIX}/api/...`) ``` Build command: `VITE_BASE_PATH=/my-project npm run build` --- ## FastAPI Behind Apache Proxy ```bash # uvicorn flags required for correct IP forwarding behind Apache/LB uvicorn app.main:app \ --host 0.0.0.0 \ --port 8000 \ --workers 2 \ --proxy-headers \ --forwarded-allow-ips='*' ``` In docker-compose: bind only to `127.0.0.1::8000` — never expose containers directly. --- ## No WebSocket Rule Apache + corporate LB timeout is ~30–60s. Solution: HTTP job polling. ``` POST /api/jobs/generate → 202 { job_id } ↓ client polls every 2s GET /api/jobs/{id} → { status: pending|running|done, result? } ``` See [[wiki/architecture/gcp-deployment-lb-timeout|gcp-deployment-lb-timeout]] for full pattern.