Skip to content

Commit a338919

Browse files
Merge pull request #23 from offendingcommit/feat/docker-web
feat(docker): containerized web build + CORS guidance
2 parents 7a478eb + cfe07f1 commit a338919

4 files changed

Lines changed: 169 additions & 0 deletions

File tree

.dockerignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
**/node_modules
2+
**/dist
3+
**/target
4+
**/.turbo
5+
**/.vite
6+
.git
7+
.github
8+
packages/desktop/src-tauri/target
9+
e2e
10+
playwright-report
11+
test-results
12+
*.log

Dockerfile

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# OpenConcho web SPA — self-hosted Honcho dashboard.
2+
#
3+
# Multi-stage build:
4+
# 1. node:22-alpine + pnpm builds the @openconcho/web SPA to packages/web/dist
5+
# 2. nginx-unprivileged serves the static bundle as non-root (UID 101) on
6+
# port 8080 — runs cleanly under read-only filesystem + cap_drop ALL.
7+
8+
# ---------- Builder stage ----------
9+
FROM node:22-alpine AS builder
10+
11+
RUN corepack enable \
12+
&& corepack prepare pnpm@10.33.2 --activate
13+
14+
WORKDIR /app
15+
16+
# Copy workspace/lockfile/manifests first for layer-cache efficiency.
17+
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json .npmrc .pnpmfile.cjs ./
18+
COPY packages/web/package.json packages/web/
19+
COPY packages/desktop/package.json packages/desktop/
20+
21+
# Install only the web filter's transitive deps (skips the Tauri Rust toolchain).
22+
RUN pnpm install --frozen-lockfile --filter @openconcho/web...
23+
24+
# Copy remaining sources + build.
25+
COPY . .
26+
RUN pnpm --filter @openconcho/web build
27+
28+
# ---------- Runtime stage ----------
29+
# Unprivileged variant runs as UID 101 with no root setup steps, so it works
30+
# under a read-only filesystem with cap_drop ALL.
31+
FROM nginxinc/nginx-unprivileged:alpine
32+
33+
COPY --chown=101:101 --from=builder /app/packages/web/dist /usr/share/nginx/html
34+
COPY --chown=101:101 docker/nginx.conf /etc/nginx/conf.d/default.conf
35+
36+
EXPOSE 8080
37+
38+
# Base image CMD runs nginx in the foreground as UID 101.

docker/nginx.conf

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# OpenConcho — nginx site config for the runtime container.
2+
# Serves the React SPA from /usr/share/nginx/html with client-side routing.
3+
4+
server {
5+
listen 8080;
6+
listen [::]:8080;
7+
server_name _;
8+
root /usr/share/nginx/html;
9+
index index.html;
10+
11+
# Don't leak the nginx version.
12+
server_tokens off;
13+
14+
# Long-cache static assets — Vite hashes filenames so they're safely immutable.
15+
location ~* \.(?:js|mjs|css|woff2?|ttf|otf|eot|svg|png|jpg|jpeg|gif|ico|webp|avif|wasm)$ {
16+
expires 1y;
17+
add_header Cache-Control "public, immutable";
18+
access_log off;
19+
try_files $uri =404;
20+
}
21+
22+
# Healthcheck (no logging spam).
23+
location = /healthz {
24+
access_log off;
25+
default_type text/plain;
26+
return 200 "ok\n";
27+
}
28+
29+
# --- Optional: same-origin Honcho reverse proxy (eliminates browser CORS) ---
30+
# The SPA's fetches are subject to browser CORS only on the web build (the
31+
# desktop app routes through Rust and bypasses CORS). To avoid configuring
32+
# CORS on Honcho itself, proxy the API under this origin and point the UI's
33+
# base URL at it. See docs/docker.md for the full tradeoff — note the UI
34+
# currently requires an absolute base URL, so this block is opt-in.
35+
#
36+
# location /honcho/ {
37+
# proxy_pass http://your-honcho-host:8000/;
38+
# proxy_set_header Host $host;
39+
# proxy_set_header X-Real-IP $remote_addr;
40+
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
41+
# proxy_set_header X-Forwarded-Proto $scheme;
42+
# }
43+
44+
# SPA fallback: any unknown path returns index.html so the router resolves it.
45+
location / {
46+
try_files $uri $uri/ /index.html;
47+
add_header Cache-Control "no-cache, no-store, must-revalidate";
48+
}
49+
50+
# gzip text responses (Vite pre-compresses CSS/JS, but HTML still benefits).
51+
gzip on;
52+
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
53+
gzip_min_length 1024;
54+
}

docs/docker.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Running OpenConcho in Docker
2+
3+
The `@openconcho/web` SPA can be served from a container. The image is a
4+
two-stage build: Node + pnpm builds the static bundle, then
5+
`nginx-unprivileged` serves it on port `8080` as a non-root user.
6+
7+
## Build and run
8+
9+
```bash
10+
docker build -t openconcho-web .
11+
docker run --rm -p 8080:8080 openconcho-web
12+
# → http://localhost:8080
13+
```
14+
15+
Hardened run (read-only filesystem, no added capabilities):
16+
17+
```bash
18+
docker run --rm -p 8080:8080 \
19+
--read-only \
20+
--tmpfs /tmp \
21+
--tmpfs /var/cache/nginx \
22+
--cap-drop ALL \
23+
--security-opt no-new-privileges \
24+
openconcho-web
25+
```
26+
27+
`GET /healthz` returns `200 ok` for container health checks.
28+
29+
## CORS
30+
31+
The desktop app routes HTTP through Rust (`reqwest`), so it is **not** subject
32+
to browser CORS. The **web build is**: it uses the browser's `fetch`, so every
33+
request to your Honcho API is cross-origin. Honcho calls are `POST` +
34+
`application/json` + `Authorization: Bearer`, which the browser always
35+
**preflights** (`OPTIONS`). You must handle this one of two ways.
36+
37+
### Option 1 — configure Honcho's CORS (recommended)
38+
39+
Honcho is a FastAPI service. Allow the UI's origin via its
40+
`CORSMiddleware` so preflight and actual requests succeed:
41+
42+
```python
43+
app.add_middleware(
44+
CORSMiddleware,
45+
allow_origins=["http://localhost:8080"], # the OpenConcho origin
46+
allow_methods=["*"],
47+
allow_headers=["*"],
48+
)
49+
```
50+
51+
This fits OpenConcho's model directly — the UI keeps using the absolute Honcho
52+
URL you enter in Settings (stored in `localStorage`). Since you self-host
53+
Honcho, you control this.
54+
55+
### Option 2 — same-origin reverse proxy (advanced)
56+
57+
Proxy the Honcho API under the same origin that serves the SPA, so the browser
58+
sees same-origin requests and CORS never applies (the token also never crosses
59+
origins). Uncomment the `location /honcho/` block in
60+
[`docker/nginx.conf`](../docker/nginx.conf) and set `proxy_pass` to your Honcho
61+
host.
62+
63+
Caveat: the Settings form currently validates the base URL as an **absolute**
64+
URL (`z.string().url()`), so pointing the UI at a relative same-origin path
65+
(`/honcho`) isn't wired yet. Until that lands, Option 1 is the supported path.

0 commit comments

Comments
 (0)