7.1 KiB
7.1 KiB
| 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 |
|
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: 8011–8039, 8041–8799, 8801–8999
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
5. Symlink large static assets instead of copying
# 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 ~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 for full pattern.