Merge pull request #23 from offendingcommit/feat/docker-web
feat(docker): containerized web build + CORS guidance
This commit is contained in:
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
||||
**/node_modules
|
||||
**/dist
|
||||
**/target
|
||||
**/.turbo
|
||||
**/.vite
|
||||
.git
|
||||
.github
|
||||
packages/desktop/src-tauri/target
|
||||
e2e
|
||||
playwright-report
|
||||
test-results
|
||||
*.log
|
||||
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
# OpenConcho web SPA — self-hosted Honcho dashboard.
|
||||
#
|
||||
# Multi-stage build:
|
||||
# 1. node:22-alpine + pnpm builds the @openconcho/web SPA to packages/web/dist
|
||||
# 2. nginx-unprivileged serves the static bundle as non-root (UID 101) on
|
||||
# port 8080 — runs cleanly under read-only filesystem + cap_drop ALL.
|
||||
|
||||
# ---------- Builder stage ----------
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
RUN corepack enable \
|
||||
&& corepack prepare pnpm@10.33.2 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy workspace/lockfile/manifests first for layer-cache efficiency.
|
||||
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json .npmrc .pnpmfile.cjs ./
|
||||
COPY packages/web/package.json packages/web/
|
||||
COPY packages/desktop/package.json packages/desktop/
|
||||
|
||||
# Install only the web filter's transitive deps (skips the Tauri Rust toolchain).
|
||||
RUN pnpm install --frozen-lockfile --filter @openconcho/web...
|
||||
|
||||
# Copy remaining sources + build.
|
||||
COPY . .
|
||||
RUN pnpm --filter @openconcho/web build
|
||||
|
||||
# ---------- Runtime stage ----------
|
||||
# Unprivileged variant runs as UID 101 with no root setup steps, so it works
|
||||
# under a read-only filesystem with cap_drop ALL.
|
||||
FROM nginxinc/nginx-unprivileged:alpine
|
||||
|
||||
COPY --chown=101:101 --from=builder /app/packages/web/dist /usr/share/nginx/html
|
||||
COPY --chown=101:101 docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
# Base image CMD runs nginx in the foreground as UID 101.
|
||||
54
docker/nginx.conf
Normal file
54
docker/nginx.conf
Normal file
@@ -0,0 +1,54 @@
|
||||
# OpenConcho — nginx site config for the runtime container.
|
||||
# Serves the React SPA from /usr/share/nginx/html with client-side routing.
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
listen [::]:8080;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Don't leak the nginx version.
|
||||
server_tokens off;
|
||||
|
||||
# Long-cache static assets — Vite hashes filenames so they're safely immutable.
|
||||
location ~* \.(?:js|mjs|css|woff2?|ttf|otf|eot|svg|png|jpg|jpeg|gif|ico|webp|avif|wasm)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Healthcheck (no logging spam).
|
||||
location = /healthz {
|
||||
access_log off;
|
||||
default_type text/plain;
|
||||
return 200 "ok\n";
|
||||
}
|
||||
|
||||
# --- Optional: same-origin Honcho reverse proxy (eliminates browser CORS) ---
|
||||
# The SPA's fetches are subject to browser CORS only on the web build (the
|
||||
# desktop app routes through Rust and bypasses CORS). To avoid configuring
|
||||
# CORS on Honcho itself, proxy the API under this origin and point the UI's
|
||||
# base URL at it. See docs/docker.md for the full tradeoff — note the UI
|
||||
# currently requires an absolute base URL, so this block is opt-in.
|
||||
#
|
||||
# location /honcho/ {
|
||||
# proxy_pass http://your-honcho-host:8000/;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# }
|
||||
|
||||
# SPA fallback: any unknown path returns index.html so the router resolves it.
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
|
||||
# gzip text responses (Vite pre-compresses CSS/JS, but HTML still benefits).
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||
gzip_min_length 1024;
|
||||
}
|
||||
65
docs/docker.md
Normal file
65
docs/docker.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Running OpenConcho in Docker
|
||||
|
||||
The `@openconcho/web` SPA can be served from a container. The image is a
|
||||
two-stage build: Node + pnpm builds the static bundle, then
|
||||
`nginx-unprivileged` serves it on port `8080` as a non-root user.
|
||||
|
||||
## Build and run
|
||||
|
||||
```bash
|
||||
docker build -t openconcho-web .
|
||||
docker run --rm -p 8080:8080 openconcho-web
|
||||
# → http://localhost:8080
|
||||
```
|
||||
|
||||
Hardened run (read-only filesystem, no added capabilities):
|
||||
|
||||
```bash
|
||||
docker run --rm -p 8080:8080 \
|
||||
--read-only \
|
||||
--tmpfs /tmp \
|
||||
--tmpfs /var/cache/nginx \
|
||||
--cap-drop ALL \
|
||||
--security-opt no-new-privileges \
|
||||
openconcho-web
|
||||
```
|
||||
|
||||
`GET /healthz` returns `200 ok` for container health checks.
|
||||
|
||||
## CORS
|
||||
|
||||
The desktop app routes HTTP through Rust (`reqwest`), so it is **not** subject
|
||||
to browser CORS. The **web build is**: it uses the browser's `fetch`, so every
|
||||
request to your Honcho API is cross-origin. Honcho calls are `POST` +
|
||||
`application/json` + `Authorization: Bearer`, which the browser always
|
||||
**preflights** (`OPTIONS`). You must handle this one of two ways.
|
||||
|
||||
### Option 1 — configure Honcho's CORS (recommended)
|
||||
|
||||
Honcho is a FastAPI service. Allow the UI's origin via its
|
||||
`CORSMiddleware` so preflight and actual requests succeed:
|
||||
|
||||
```python
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:8080"], # the OpenConcho origin
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
```
|
||||
|
||||
This fits OpenConcho's model directly — the UI keeps using the absolute Honcho
|
||||
URL you enter in Settings (stored in `localStorage`). Since you self-host
|
||||
Honcho, you control this.
|
||||
|
||||
### Option 2 — same-origin reverse proxy (advanced)
|
||||
|
||||
Proxy the Honcho API under the same origin that serves the SPA, so the browser
|
||||
sees same-origin requests and CORS never applies (the token also never crosses
|
||||
origins). Uncomment the `location /honcho/` block in
|
||||
[`docker/nginx.conf`](../docker/nginx.conf) and set `proxy_pass` to your Honcho
|
||||
host.
|
||||
|
||||
Caveat: the Settings form currently validates the base URL as an **absolute**
|
||||
URL (`z.string().url()`), so pointing the UI at a relative same-origin path
|
||||
(`/honcho`) isn't wired yet. Until that lands, Option 1 is the supported path.
|
||||
Reference in New Issue
Block a user