obsidian/wiki/architecture/optical-dev-server-deploy.md
2026-04-17 12:01:45 +01:00

7.1 KiB
Raw Blame History

title description tags created updated
optical-dev Server — Apache Deployment Pattern Single-vhost Apache pattern on optical-dev.oliver.solutions GCP server — port allocation, Include fragments, SPA routing, deploy script best practices
architecture
apache
deployment
docker
ubuntu
gcp
2026-04-17 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:

<VirtualHost *:80>
    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
</VirtualHost>

Deploy scripts inject the Include line automatically via sed (idempotent):

INCLUDE_LINE="    Include /opt/barclays-banner-builder/deploy/apache-barclays.conf"
if ! sudo grep -qF "" ""; then
  sudo sed -i "s|</VirtualHost>|
</VirtualHost>|" ""
  sudo apache2ctl configtest && sudo systemctl reload apache2
fi

Apache Fragment Pattern

SPA (React/Vue) with API backend

# 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
<Directory /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]
</Directory>

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: 80118039, 80418799, 88018999


File System Layout

Path Purpose
/opt/<project>/ Git repo root for all projects
/var/www/html/<project>/ Apache-served static files (React/Vue dist)
/etc/apache2/sites-available/optical-dev.oliver.solutions.conf Single vhost config
/opt/<project>/deploy/apache-<project>.conf Per-project Apache Include fragment
/opt/<project>/.deploy_state/ Build cache hashes (dockerfile_hash, npm_hash)
/opt/<project>/.env Production secrets (not in git)

Deploy Script Patterns

1. Hash-based build cache (avoid redundant rebuilds)

# 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

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

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)

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

export default defineConfig({
  base: process.env.VITE_BASE_PATH ?? "/",
})

main.tsx

const basename = import.meta.env.VITE_BASE_PATH ?? "/";
<BrowserRouter basename={basename}>

api/client.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

# 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:<host-port>:8000 — never expose containers directly.


No WebSocket Rule

Apache + corporate LB timeout is ~3060s. 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 for full pattern.