mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Add split web Docker Compose PoC (#445)
## Summary Adds a standalone Docker Compose proof that runs the Fabro Rust API, a Caddy static SPA server, and a Caddy edge proxy as separate services. This demonstrates split web asset serving while keeping `/api/*`, `/auth/*`, and `/health` same-origin with the API server. ## Changes - Adds `docker-compose.split-web.yaml` with private `fabro-api` and `fabro-web` services behind an exposed `edge` proxy on port 8080. - Adds Caddy edge routing that sends `/api/*`, `/auth/*`, and `/health` to Rust, while everything else goes to the static web service. - Adds a static Caddy config for `apps/fabro-web/dist` with SPA fallback, source-map blocking, security headers, immutable asset caching, and `X-Fabro-PoC-Upstream` route-proof headers. - Adds PoC server settings and a README with build, run, and validation commands. ## Verification - `cargo dev docker-build --tag fabro-sh/fabro:split-web-poc` - `docker compose -f docker-compose.split-web.yaml up -d` - `docker compose -f docker-compose.split-web.yaml ps` - `curl` checks for `/runs`, `/assets/app.css`, `/assets/app.css.map`, `/api/v1/health`, `/api/v1/auth/config`, `/auth/login/dev-token`, `/api/v1/auth/me`, and `/api/v1/attach` - Browser login flow via `browser-use`: loaded `/login`, submitted the dev token, and landed on the authenticated Runs screen - `docker compose -f docker-compose.split-web.yaml config` - `caddy validate` for both Caddyfiles - `git diff --check` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
This commit is contained in:
parent
9ee576690b
commit
31a990c4dc
5 changed files with 180 additions and 0 deletions
52
docker-compose.split-web.yaml
Normal file
52
docker-compose.split-web.yaml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
services:
|
||||
fabro-api:
|
||||
image: ${FABRO_IMAGE:-fabro-sh/fabro:split-web-poc}
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "32276"
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- >-
|
||||
exec fabro server start --foreground
|
||||
--bind 0.0.0.0:32276
|
||||
--config /config/settings.toml
|
||||
volumes:
|
||||
- fabro-storage:/storage
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./docker/split-web/settings.toml:/config/settings.toml:ro
|
||||
environment:
|
||||
FABRO_WEB_URL: http://localhost:${SPLIT_WEB_PORT:-8080}
|
||||
SESSION_SECRET: ${SESSION_SECRET:-}
|
||||
FABRO_DEV_TOKEN: ${FABRO_DEV_TOKEN:-}
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:32276/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 20s
|
||||
|
||||
fabro-web:
|
||||
image: caddy:2-alpine
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "80"
|
||||
volumes:
|
||||
- ./apps/fabro-web:/srv/web:ro
|
||||
- ./docker/split-web/Caddyfile.static:/etc/caddy/Caddyfile:ro
|
||||
|
||||
edge:
|
||||
image: caddy:2-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${SPLIT_WEB_PORT:-8080}:80"
|
||||
volumes:
|
||||
- ./docker/split-web/Caddyfile.edge:/etc/caddy/Caddyfile:ro
|
||||
depends_on:
|
||||
fabro-api:
|
||||
condition: service_healthy
|
||||
fabro-web:
|
||||
condition: service_started
|
||||
|
||||
volumes:
|
||||
fabro-storage:
|
||||
14
docker/split-web/Caddyfile.edge
Normal file
14
docker/split-web/Caddyfile.edge
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
:80 {
|
||||
encode gzip zstd
|
||||
|
||||
@api path /api/* /auth/* /health
|
||||
handle @api {
|
||||
header X-Fabro-PoC-Upstream fabro-api
|
||||
reverse_proxy fabro-api:32276
|
||||
}
|
||||
|
||||
handle {
|
||||
header X-Fabro-PoC-Upstream fabro-web
|
||||
reverse_proxy fabro-web:80
|
||||
}
|
||||
}
|
||||
39
docker/split-web/Caddyfile.static
Normal file
39
docker/split-web/Caddyfile.static
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
:80 {
|
||||
root * /srv/web/dist
|
||||
encode gzip zstd
|
||||
|
||||
header {
|
||||
X-Content-Type-Options nosniff
|
||||
X-Frame-Options DENY
|
||||
Referrer-Policy strict-origin-when-cross-origin
|
||||
Cross-Origin-Opener-Policy same-origin
|
||||
Cross-Origin-Resource-Policy same-origin
|
||||
Permissions-Policy "accelerometer=(), autoplay=(), camera=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), usb=(), web-share=(), xr-spatial-tracking=()"
|
||||
Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' https://fonts.googleapis.com 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://avatars.githubusercontent.com; connect-src 'self' ws: wss:; worker-src 'self' blob:; manifest-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'"
|
||||
}
|
||||
|
||||
@sourceMaps path *.map
|
||||
handle @sourceMaps {
|
||||
respond 404
|
||||
}
|
||||
|
||||
@cacheAssets {
|
||||
path /assets/*
|
||||
not path *.map
|
||||
}
|
||||
header @cacheAssets Cache-Control "public, max-age=31536000, immutable"
|
||||
|
||||
@htmlNavigation {
|
||||
header Accept *text/html*
|
||||
not path /assets/*
|
||||
}
|
||||
handle @htmlNavigation {
|
||||
try_files {path} /index.html
|
||||
header Cache-Control "no-cache"
|
||||
file_server
|
||||
}
|
||||
|
||||
handle {
|
||||
file_server
|
||||
}
|
||||
}
|
||||
67
docker/split-web/README.md
Normal file
67
docker/split-web/README.md
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Split Web Compose PoC
|
||||
|
||||
This Compose stack proves that Fabro can serve the React SPA from a separate
|
||||
static process while the Rust server remains the API and browser-auth origin.
|
||||
|
||||
Request ownership:
|
||||
|
||||
- `/api/*` -> `fabro-api:32276`
|
||||
- `/auth/*` -> `fabro-api:32276`
|
||||
- `/health` -> `fabro-api:32276`
|
||||
- everything else -> `fabro-web:80`
|
||||
|
||||
The Rust server still contains bundled SPA assets. In this PoC they are simply
|
||||
not reachable through the `edge` service for normal web paths.
|
||||
|
||||
The edge proxy adds `X-Fabro-PoC-Upstream` to responses so manual checks can
|
||||
confirm which service handled a request.
|
||||
|
||||
## Run
|
||||
|
||||
Build the local Fabro image from the current tree:
|
||||
|
||||
```sh
|
||||
cargo dev docker-build --tag fabro-sh/fabro:split-web-poc
|
||||
```
|
||||
|
||||
Set local auth secrets:
|
||||
|
||||
```sh
|
||||
export SESSION_SECRET="$(openssl rand -hex 32)"
|
||||
export FABRO_DEV_TOKEN="fabro_dev_$(openssl rand -hex 32)"
|
||||
```
|
||||
|
||||
Start the split stack:
|
||||
|
||||
```sh
|
||||
docker compose -f docker-compose.split-web.yaml up
|
||||
```
|
||||
|
||||
Open http://localhost:8080.
|
||||
|
||||
Use `SPLIT_WEB_PORT` to expose a different local port, or `FABRO_IMAGE` to use
|
||||
a different API image.
|
||||
|
||||
## Validate
|
||||
|
||||
```sh
|
||||
curl -i http://localhost:8080/health
|
||||
curl -i http://localhost:8080/api/v1/health
|
||||
curl -I http://localhost:8080/runs
|
||||
curl -I http://localhost:8080/assets/app.css
|
||||
|
||||
curl -c /tmp/fabro.cookies \
|
||||
-H "content-type: application/json" \
|
||||
-d "{\"token\":\"$FABRO_DEV_TOKEN\"}" \
|
||||
http://localhost:8080/auth/login/dev-token
|
||||
|
||||
curl -b /tmp/fabro.cookies http://localhost:8080/api/v1/auth/me
|
||||
```
|
||||
|
||||
Expected results:
|
||||
|
||||
- `/runs` and `/assets/*` are served by the static `fabro-web` container.
|
||||
- `/api/*`, `/auth/*`, and `/health` are served by the Rust `fabro-api`
|
||||
container through the same browser origin.
|
||||
- Dev-token login sets a same-origin session cookie, and
|
||||
`/api/v1/auth/me` accepts it.
|
||||
8
docker/split-web/settings.toml
Normal file
8
docker/split-web/settings.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
_version = 1
|
||||
|
||||
[server.web]
|
||||
enabled = true
|
||||
url = "{{ env.FABRO_WEB_URL }}"
|
||||
|
||||
[server.auth]
|
||||
methods = ["dev-token"]
|
||||
Loading…
Add table
Reference in a new issue