From 58ac4fd187a91127920a4dcf9473b7869e6543d9 Mon Sep 17 00:00:00 2001 From: almajd3713 Date: Sat, 8 Aug 2026 15:10:03 +0400 Subject: [PATCH 01/11] feat(docker): Containerize and compose app launch --- .dockerignore | 18 +++++++ .env.example | 15 ++++++ Dockerfile | 55 +++++++++++++++++++ compose.dev.yaml | 54 +++++++++++++++++++ compose.yaml | 61 ++++++++++++++++++++++ docker/frontend-env.js | 1 + docker/nginx.conf | 59 +++++++++++++++++++++ frontend/src/shared/config/apiBase.test.ts | 42 +++++++++++++++ frontend/src/shared/config/apiBase.ts | 41 +++++++++++++++ frontend/src/shared/types/index.ts | 6 +-- 10 files changed, 348 insertions(+), 4 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 compose.dev.yaml create mode 100644 compose.yaml create mode 100644 docker/frontend-env.js create mode 100644 docker/nginx.conf create mode 100644 frontend/src/shared/config/apiBase.test.ts create mode 100644 frontend/src/shared/config/apiBase.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..849a504 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.git +.github +.codebase-memory +.env +.env.* +!.env.example +**/node_modules +**/dist +**/build +**/coverage +**/.cache +**/.tmp +**/*.log +.torollo +.torollo-data +AGENTS.md +CLAUDE.md +doc diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8c91245 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# Compose project and host exposure +COMPOSE_PROJECT_NAME=torollo +TOROLLO_BIND_ADDRESS=127.0.0.1 +TOROLLO_FRONTEND_PORT=23232 + +# Used only by compose.dev.yaml, where the browser calls the backend directly. +TOROLLO_BACKEND_PORT=23233 + +# Host-side Docker socket mounted into the backend container. +# Rootless Docker commonly uses /run/user//docker.sock. +TOROLLO_DOCKER_SOCKET=/var/run/docker.sock + +# Comma-separated exact browser origins. Leave blank for localhost-only use. +# LAN example: http://192.168.1.50:23232 +TOROLLO_ALLOWED_ORIGINS= diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a203ca9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# syntax=docker/dockerfile:1.7 + +ARG NODE_IMAGE=node:20.20.2-bookworm-slim +ARG NGINX_IMAGE=nginx:1.28.3-alpine + +FROM ${NODE_IMAGE} AS backend-deps +WORKDIR /app/backend +COPY backend/package.json backend/package-lock.json ./ +RUN --mount=type=cache,target=/root/.npm npm ci + +FROM backend-deps AS backend-build +COPY backend/tsconfig.json ./ +COPY backend/src ./src +RUN npm run build + +FROM backend-deps AS backend-dev +COPY backend/tsconfig.json ./ +COPY backend/src ./src +EXPOSE 23233 +CMD ["npm", "run", "dev"] + +FROM ${NODE_IMAGE} AS backend-runtime +ENV NODE_ENV=production \ + PORT=23233 \ + TOROLLO_HOST=0.0.0.0 +WORKDIR /app/backend +COPY backend/package.json backend/package-lock.json ./ +RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev +COPY --from=backend-build /app/backend/dist ./dist +COPY roadmaps /app/roadmaps +EXPOSE 23233 +CMD ["node", "dist/server.js"] + +FROM ${NODE_IMAGE} AS frontend-deps +WORKDIR /app +COPY package.json ./package.json +WORKDIR /app/frontend +COPY frontend/package.json frontend/package-lock.json ./ +RUN --mount=type=cache,target=/root/.npm npm ci + +FROM frontend-deps AS frontend-build +COPY frontend ./ +RUN npm run build + +FROM frontend-deps AS frontend-dev +COPY frontend ./ +EXPOSE 23232 +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "23232"] + +FROM ${NGINX_IMAGE} AS frontend-runtime +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=frontend-build /app/frontend/dist /usr/share/nginx/html +COPY docker/frontend-env.js /usr/share/nginx/html/env.js +EXPOSE 23232 +CMD ["nginx", "-g", "daemon off;"] diff --git a/compose.dev.yaml b/compose.dev.yaml new file mode 100644 index 0000000..a4f3caa --- /dev/null +++ b/compose.dev.yaml @@ -0,0 +1,54 @@ +services: + backend: + image: torollo/backend-dev:local + build: + target: backend-dev + command: npm run dev + environment: + NODE_ENV: development + ports: + - name: api + target: 23233 + published: ${TOROLLO_BACKEND_PORT:-23233} + host_ip: ${TOROLLO_BIND_ADDRESS:-127.0.0.1} + protocol: tcp + volumes: + - type: bind + source: ./backend/src + target: /app/backend/src + - type: bind + source: ./backend/tsconfig.json + target: /app/backend/tsconfig.json + read_only: true + - type: bind + source: ./roadmaps + target: /app/roadmaps + read_only: true + + frontend: + image: torollo/frontend-dev:local + build: + target: frontend-dev + command: npm run dev -- --host 0.0.0.0 --port 23232 + environment: + VITE_TOROLLO_BACKEND_PORT: ${TOROLLO_BACKEND_PORT:-23233} + depends_on: + backend: + condition: service_started + volumes: + - type: bind + source: ./frontend/src + target: /app/frontend/src + - type: bind + source: ./frontend/public + target: /app/frontend/public + - type: bind + source: ./frontend/index.html + target: /app/frontend/index.html + read_only: true + - type: bind + source: ./frontend/vite.config.ts + target: /app/frontend/vite.config.ts + read_only: true + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:23232').then(response => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"] diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..b280f3c --- /dev/null +++ b/compose.yaml @@ -0,0 +1,61 @@ +name: ${COMPOSE_PROJECT_NAME:-torollo} + +services: + backend: + image: torollo/backend:local + build: + context: . + target: backend-runtime + environment: + NODE_ENV: production + PORT: "23233" + TOROLLO_HOST: 0.0.0.0 + TOROLLO_ALLOWED_ORIGINS: ${TOROLLO_ALLOWED_ORIGINS:-} + DOCKER_SOCKET: /var/run/docker.sock + expose: + - "23233" + volumes: + - type: bind + source: ${TOROLLO_DOCKER_SOCKET:-/var/run/docker.sock} + target: /var/run/docker.sock + - type: volume + source: torollo-data + target: /root/.torollo + healthcheck: + test: + - CMD + - node + - -e + - "fetch('http://127.0.0.1:23233/health').then(response => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))" + interval: 10s + timeout: 5s + retries: 6 + start_period: 10s + init: true + restart: unless-stopped + + frontend: + image: torollo/frontend:local + build: + context: . + target: frontend-runtime + depends_on: + backend: + condition: service_started + ports: + - name: web + target: 23232 + published: ${TOROLLO_FRONTEND_PORT:-23232} + host_ip: ${TOROLLO_BIND_ADDRESS:-127.0.0.1} + protocol: tcp + healthcheck: + test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:23232/healthz"] + interval: 10s + timeout: 5s + retries: 6 + start_period: 5s + init: true + restart: unless-stopped + +volumes: + torollo-data: diff --git a/docker/frontend-env.js b/docker/frontend-env.js new file mode 100644 index 0000000..4c1a594 --- /dev/null +++ b/docker/frontend-env.js @@ -0,0 +1 @@ +window.TOROLLO_BACKEND_URL = window.location.origin; diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..49c6070 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,59 @@ +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 23232; + server_name _; + server_tokens off; + + root /usr/share/nginx/html; + index index.html; + + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + + location = /healthz { + access_log off; + add_header Content-Type text/plain; + return 200 'ok'; + } + + location = /env.js { + add_header Cache-Control 'no-store, no-cache, must-revalidate' always; + try_files $uri =404; + } + + location /api/ { + proxy_pass http://backend:23233; + proxy_http_version 1.1; + 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; + } + + location = /health { + proxy_pass http://backend:23233; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /socket.io/ { + proxy_pass http://backend:23233; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + 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; + proxy_read_timeout 1h; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/frontend/src/shared/config/apiBase.test.ts b/frontend/src/shared/config/apiBase.test.ts new file mode 100644 index 0000000..53d00b0 --- /dev/null +++ b/frontend/src/shared/config/apiBase.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest' +import { resolveApiBase } from './apiBase' + +describe('resolveApiBase', () => { + it('uses the runtime URL for a same-origin Compose deployment', () => { + expect(resolveApiBase({ + isDevelopment: false, + runtimeUrl: 'https://torollo.example.test/', + hostname: 'torollo.example.test', + })).toBe('https://torollo.example.test') + }) + + it('uses the fixed local backend during Vite development', () => { + expect(resolveApiBase({ + isDevelopment: true, + hostname: 'localhost', + })).toBe('http://localhost:23233') + }) + + it('uses a configured host port during Compose development', () => { + expect(resolveApiBase({ + isDevelopment: true, + runtimePort: 24001, + hostname: '192.168.1.50', + })).toBe('http://192.168.1.50:24001') + }) + + it('preserves the CLI runtime port fallback in production', () => { + expect(resolveApiBase({ + isDevelopment: false, + runtimePort: 24001, + hostname: '127.0.0.1', + })).toBe('http://127.0.0.1:24001') + }) + + it('uses the legacy default port when no runtime configuration exists', () => { + expect(resolveApiBase({ + isDevelopment: false, + hostname: 'localhost', + })).toBe('http://localhost:23233') + }) +}) diff --git a/frontend/src/shared/config/apiBase.ts b/frontend/src/shared/config/apiBase.ts new file mode 100644 index 0000000..7d6042b --- /dev/null +++ b/frontend/src/shared/config/apiBase.ts @@ -0,0 +1,41 @@ +export interface ApiBaseOptions { + isDevelopment: boolean + runtimeUrl?: string + runtimePort?: number + hostname: string +} + +declare global { + interface Window { + TOROLLO_BACKEND_URL?: string + TOROLLO_BACKEND_PORT?: number + } +} + +export function resolveApiBase({ + isDevelopment, + runtimeUrl, + runtimePort, + hostname, +}: ApiBaseOptions): string { + if (runtimeUrl !== undefined) { + return runtimeUrl.replace(/\/$/, '') + } + + if (isDevelopment) { + return `http://${hostname}:${runtimePort || 23233}` + } + + return `http://${hostname}:${runtimePort || 23233}` +} + +const viteBackendPort = Number(import.meta.env.VITE_TOROLLO_BACKEND_PORT) + +export const API_BASE = resolveApiBase({ + isDevelopment: import.meta.env.DEV, + runtimeUrl: window.TOROLLO_BACKEND_URL, + runtimePort: import.meta.env.DEV && Number.isInteger(viteBackendPort) && viteBackendPort > 0 + ? viteBackendPort + : window.TOROLLO_BACKEND_PORT, + hostname: window.location.hostname, +}) diff --git a/frontend/src/shared/types/index.ts b/frontend/src/shared/types/index.ts index 58ab258..39a8393 100644 --- a/frontend/src/shared/types/index.ts +++ b/frontend/src/shared/types/index.ts @@ -39,7 +39,5 @@ export interface LearningIntent { roadmap?: { id: string; language: string }; } -/** API base URL for the backend */ -export const API_BASE = import.meta.env.DEV - ? 'http://localhost:23233' - : `http://${window.location.hostname}:${(window as any).TOROLLO_BACKEND_PORT || 23233}`; +/** API base URL for the backend. Kept here as the stable import surface. */ +export { API_BASE } from '../config/apiBase'; From be0d87b78c5953f9c2de91563d071a11a8b9a775 Mon Sep 17 00:00:00 2001 From: almajd3713 Date: Sat, 8 Aug 2026 15:24:23 +0400 Subject: [PATCH 02/11] add CI job for compose verification --- .github/workflows/ci.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e42f441..bc53b35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,3 +64,25 @@ jobs: - name: Run Tests run: npm test + + compose-build: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Validate production Compose configuration + run: docker compose config --quiet + + - name: Validate development Compose configuration + run: docker compose -f compose.yaml -f compose.dev.yaml config --quiet + + - name: Build production images + run: docker compose build + + - name: Build development images + run: docker compose -f compose.yaml -f compose.dev.yaml build From 1d8d7dd31a0a56573ef8d2f74124718e9f4029ff Mon Sep 17 00:00:00 2001 From: almajd3713 Date: Sat, 8 Aug 2026 15:24:54 +0400 Subject: [PATCH 03/11] update docs --- CONTRIBUTING.md | 8 ++++++++ README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0968d3b..8354c0b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,6 +49,14 @@ npm run dev You can now access the interface at `http://localhost:23232`. +Alternatively, run both services in containers with hot reload: + +```bash +docker compose -f compose.yaml -f compose.dev.yaml up --build +``` + +This requires Docker Compose v2. The backend is available on port `23233`, the frontend on `23232`, and Torollo state is kept in the Compose-managed `torollo-data` volume. Rebuild the images after changing either package's dependencies. + ### 4. Testing the Production Build (CLI Mode) If you want to test how the application runs when bundled via the CLI tool: ```bash diff --git a/README.md b/README.md index de80bc5..a604253 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,52 @@ npx torollo start Open the app, create a project, and hit **Learning** in the topbar — the best first contact with Torollo is a guided roadmap, not an empty canvas. Start with **Deploy a resilient three-tier app**: it walks you from a single web server to a load-balanced, firewalled, database-backed architecture in ten validated steps. +### Run with Docker Compose + +The Compose setup builds the frontend and backend, serves them through one local URL, mounts the host Docker socket so Torollo can create lab resources, and persists projects and learning progress in a named volume. + +Prerequisites: + +- Docker Engine or Docker Desktop with the Docker daemon running. +- Docker Compose v2 (`docker compose version`). + +Start the production stack: + +```bash +cp .env.example .env # optional: defaults work for localhost +docker compose up --build -d +docker compose logs -f +``` + +Open `http://localhost:23232`. The first boot can take several minutes while Torollo prepares its node images; follow the backend logs to see progress. Stop the application without deleting its state with: + +```bash +docker compose down +``` + +For development with Vite and nodemon hot reload: + +```bash +docker compose -f compose.yaml -f compose.dev.yaml up --build +``` + +The development override also publishes the backend at `http://localhost:23233`. Source and roadmap changes are bind-mounted; rebuild after changing dependencies. + +Compose reads these optional settings from the root `.env` file: + +| Variable | Default | Purpose | +|---|---|---| +| `COMPOSE_PROJECT_NAME` | `torollo` | Prefix for containers, networks, and volumes | +| `TOROLLO_BIND_ADDRESS` | `127.0.0.1` | Host address used for published ports | +| `TOROLLO_FRONTEND_PORT` | `23232` | Browser-facing frontend/proxy port | +| `TOROLLO_BACKEND_PORT` | `23233` | Backend port published only by the development override | +| `TOROLLO_DOCKER_SOCKET` | `/var/run/docker.sock` | Host Docker socket to mount into the backend | +| `TOROLLO_ALLOWED_ORIGINS` | empty | Comma-separated extra browser origins | + +Project and roadmap-progress data live in the `torollo-data` named volume. `docker compose down` preserves it; `docker compose down -v` permanently deletes it. + +> ⚠️ **Docker access:** The socket mount gives the backend control of the host Docker daemon, which is Torollo's core function. Do not run untrusted Torollo images or expose the application to an untrusted network. Rootless Docker users can set `TOROLLO_DOCKER_SOCKET=/run/user//docker.sock`. + --- ## Guided, auto-graded roadmaps @@ -85,6 +131,13 @@ To access Torollo from another machine (e.g. a home lab server), opt in explicit TOROLLO_HOST=0.0.0.0 TOROLLO_ALLOWED_ORIGINS=http://:23232 npx torollo start ``` +For Compose, set the equivalent root `.env` values and restart the stack: + +```dotenv +TOROLLO_BIND_ADDRESS=0.0.0.0 +TOROLLO_ALLOWED_ORIGINS=http://:23232 +``` + - `TOROLLO_HOST` — address the API binds to (default `127.0.0.1`; set to `0.0.0.0` to listen on all interfaces). - `TOROLLO_ALLOWED_ORIGINS` — comma-separated list of exact extra origins allowed to call the API from a browser (the address you type into the browser, e.g. `http://192.168.1.5:23232`). From 9c937fa7266cc9ce6b00bdeee657a1df29fe9b53 Mon Sep 17 00:00:00 2001 From: OthmaneZ05 Date: Tue, 11 Aug 2026 10:02:38 -0400 Subject: [PATCH 04/11] fix(docker): resolve the backend upstream per request in nginx --- docker/nginx.conf | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index 49c6070..d32f58c 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -11,6 +11,13 @@ server { root /usr/share/nginx/html; index index.html; + # A literal upstream name is resolved once at startup and cached for the + # lifetime of the container, so nginx would keep proxying to a stale address + # after `docker compose up` recreates the backend. Going through a variable + # defers the lookup to request time, against Docker's embedded DNS. + resolver 127.0.0.11 ipv6=off valid=10s; + set $torollo_backend backend; + add_header X-Content-Type-Options nosniff always; add_header Referrer-Policy strict-origin-when-cross-origin always; @@ -26,7 +33,7 @@ server { } location /api/ { - proxy_pass http://backend:23233; + proxy_pass http://$torollo_backend:23233; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -35,14 +42,14 @@ server { } location = /health { - proxy_pass http://backend:23233; + proxy_pass http://$torollo_backend:23233; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; } location /socket.io/ { - proxy_pass http://backend:23233; + proxy_pass http://$torollo_backend:23233; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; From e05a113110b7c06e54f5e992188c724d18aaf292 Mon Sep 17 00:00:00 2001 From: OthmaneZ05 Date: Tue, 11 Aug 2026 10:02:48 -0400 Subject: [PATCH 05/11] fix(docker): allow slow container creation through the API proxy --- docker/nginx.conf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker/nginx.conf b/docker/nginx.conf index d32f58c..f05fd5c 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -39,6 +39,11 @@ server { 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; + + # Creating a node blocks on pulling its image, which on a cold host runs + # well past the default 60s. Timing out here would report a failure for a + # container the backend goes on to create. + proxy_read_timeout 15m; } location = /health { From c7142a7b368dc8e8cc9cd35f26cc9f9153fb2050 Mon Sep 17 00:00:00 2001 From: OthmaneZ05 Date: Tue, 11 Aug 2026 10:03:09 -0400 Subject: [PATCH 06/11] fix(docker): keep the security headers on the env.js and healthz responses --- docker/nginx.conf | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docker/nginx.conf b/docker/nginx.conf index f05fd5c..9706baf 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -18,16 +18,20 @@ server { resolver 127.0.0.11 ipv6=off valid=10s; set $torollo_backend backend; + # add_header replaces rather than merges across levels: any location + # declaring its own header loses the two below and has to repeat them. add_header X-Content-Type-Options nosniff always; add_header Referrer-Policy strict-origin-when-cross-origin always; location = /healthz { access_log off; - add_header Content-Type text/plain; + default_type text/plain; return 200 'ok'; } location = /env.js { + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy strict-origin-when-cross-origin always; add_header Cache-Control 'no-store, no-cache, must-revalidate' always; try_files $uri =404; } From 971c9f70f952e58f6b6225b0365aed6a689be0f7 Mon Sep 17 00:00:00 2001 From: OthmaneZ05 Date: Tue, 11 Aug 2026 10:03:19 -0400 Subject: [PATCH 07/11] fix(compose): drop the ports name field unsupported by older Compose v2 --- compose.dev.yaml | 3 +-- compose.yaml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/compose.dev.yaml b/compose.dev.yaml index a4f3caa..db3b023 100644 --- a/compose.dev.yaml +++ b/compose.dev.yaml @@ -7,8 +7,7 @@ services: environment: NODE_ENV: development ports: - - name: api - target: 23233 + - target: 23233 published: ${TOROLLO_BACKEND_PORT:-23233} host_ip: ${TOROLLO_BIND_ADDRESS:-127.0.0.1} protocol: tcp diff --git a/compose.yaml b/compose.yaml index b280f3c..218df60 100644 --- a/compose.yaml +++ b/compose.yaml @@ -43,8 +43,7 @@ services: backend: condition: service_started ports: - - name: web - target: 23232 + - target: 23232 published: ${TOROLLO_FRONTEND_PORT:-23232} host_ip: ${TOROLLO_BIND_ADDRESS:-127.0.0.1} protocol: tcp From ca8de09a8ccfdc8b4f2743a8d03b61e2424d7595 Mon Sep 17 00:00:00 2001 From: OthmaneZ05 Date: Tue, 11 Aug 2026 10:03:37 -0400 Subject: [PATCH 08/11] fix(compose): start the frontend only once the backend is healthy --- compose.dev.yaml | 3 --- compose.yaml | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/compose.dev.yaml b/compose.dev.yaml index db3b023..c15f647 100644 --- a/compose.dev.yaml +++ b/compose.dev.yaml @@ -31,9 +31,6 @@ services: command: npm run dev -- --host 0.0.0.0 --port 23232 environment: VITE_TOROLLO_BACKEND_PORT: ${TOROLLO_BACKEND_PORT:-23233} - depends_on: - backend: - condition: service_started volumes: - type: bind source: ./frontend/src diff --git a/compose.yaml b/compose.yaml index 218df60..0542991 100644 --- a/compose.yaml +++ b/compose.yaml @@ -41,7 +41,7 @@ services: target: frontend-runtime depends_on: backend: - condition: service_started + condition: service_healthy ports: - target: 23232 published: ${TOROLLO_FRONTEND_PORT:-23232} From f0806e9a995226bc6164cc8adbce341a03082368 Mon Sep 17 00:00:00 2001 From: OthmaneZ05 Date: Tue, 11 Aug 2026 10:03:52 -0400 Subject: [PATCH 09/11] chore(compose): remove the DOCKER_SOCKET variable the backend never reads --- compose.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/compose.yaml b/compose.yaml index 0542991..31a654f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -11,7 +11,6 @@ services: PORT: "23233" TOROLLO_HOST: 0.0.0.0 TOROLLO_ALLOWED_ORIGINS: ${TOROLLO_ALLOWED_ORIGINS:-} - DOCKER_SOCKET: /var/run/docker.sock expose: - "23233" volumes: From 642c0238ddceede99394e3f6db5db1ec5780a6fb Mon Sep 17 00:00:00 2001 From: OthmaneZ05 Date: Tue, 11 Aug 2026 10:04:38 -0400 Subject: [PATCH 10/11] refactor(frontend): drop the inert development branch in resolveApiBase --- frontend/src/shared/config/apiBase.test.ts | 22 ++++++---------------- frontend/src/shared/config/apiBase.ts | 12 +++++------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/frontend/src/shared/config/apiBase.test.ts b/frontend/src/shared/config/apiBase.test.ts index 53d00b0..e84ab84 100644 --- a/frontend/src/shared/config/apiBase.test.ts +++ b/frontend/src/shared/config/apiBase.test.ts @@ -4,38 +4,28 @@ import { resolveApiBase } from './apiBase' describe('resolveApiBase', () => { it('uses the runtime URL for a same-origin Compose deployment', () => { expect(resolveApiBase({ - isDevelopment: false, runtimeUrl: 'https://torollo.example.test/', hostname: 'torollo.example.test', })).toBe('https://torollo.example.test') }) - it('uses the fixed local backend during Vite development', () => { + it('prefers the runtime URL over a configured port', () => { expect(resolveApiBase({ - isDevelopment: true, - hostname: 'localhost', - })).toBe('http://localhost:23233') + runtimeUrl: 'http://torollo.example.test:8080', + runtimePort: 24001, + hostname: 'torollo.example.test', + })).toBe('http://torollo.example.test:8080') }) - it('uses a configured host port during Compose development', () => { + it('falls back to the page host on the configured port', () => { expect(resolveApiBase({ - isDevelopment: true, runtimePort: 24001, hostname: '192.168.1.50', })).toBe('http://192.168.1.50:24001') }) - it('preserves the CLI runtime port fallback in production', () => { - expect(resolveApiBase({ - isDevelopment: false, - runtimePort: 24001, - hostname: '127.0.0.1', - })).toBe('http://127.0.0.1:24001') - }) - it('uses the legacy default port when no runtime configuration exists', () => { expect(resolveApiBase({ - isDevelopment: false, hostname: 'localhost', })).toBe('http://localhost:23233') }) diff --git a/frontend/src/shared/config/apiBase.ts b/frontend/src/shared/config/apiBase.ts index 7d6042b..5545cff 100644 --- a/frontend/src/shared/config/apiBase.ts +++ b/frontend/src/shared/config/apiBase.ts @@ -1,5 +1,4 @@ export interface ApiBaseOptions { - isDevelopment: boolean runtimeUrl?: string runtimePort?: number hostname: string @@ -12,8 +11,12 @@ declare global { } } +/** + * Same-origin deployments (Compose, behind the nginx proxy) inject an explicit + * runtime URL through `env.js`. Everywhere else — Vite dev and the CLI — the + * API answers on its own port, on the host the page was served from. + */ export function resolveApiBase({ - isDevelopment, runtimeUrl, runtimePort, hostname, @@ -22,17 +25,12 @@ export function resolveApiBase({ return runtimeUrl.replace(/\/$/, '') } - if (isDevelopment) { - return `http://${hostname}:${runtimePort || 23233}` - } - return `http://${hostname}:${runtimePort || 23233}` } const viteBackendPort = Number(import.meta.env.VITE_TOROLLO_BACKEND_PORT) export const API_BASE = resolveApiBase({ - isDevelopment: import.meta.env.DEV, runtimeUrl: window.TOROLLO_BACKEND_URL, runtimePort: import.meta.env.DEV && Number.isInteger(viteBackendPort) && viteBackendPort > 0 ? viteBackendPort From 0a89b73b2043b91741505ba4ba68d4abd51080ee Mon Sep 17 00:00:00 2001 From: OthmaneZ05 Date: Tue, 11 Aug 2026 10:05:02 -0400 Subject: [PATCH 11/11] docs: state that binding beyond loopback exposes host Docker access --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a604253..0ec1bdc 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Compose reads these optional settings from the root `.env` file: Project and roadmap-progress data live in the `torollo-data` named volume. `docker compose down` preserves it; `docker compose down -v` permanently deletes it. -> ⚠️ **Docker access:** The socket mount gives the backend control of the host Docker daemon, which is Torollo's core function. Do not run untrusted Torollo images or expose the application to an untrusted network. Rootless Docker users can set `TOROLLO_DOCKER_SOCKET=/run/user//docker.sock`. +> ⚠️ **Docker access:** The socket mount gives the backend control of the host Docker daemon, which is Torollo's core function. Torollo has no authentication, so anyone who can reach the published port inherits that control — the default `TOROLLO_BIND_ADDRESS=127.0.0.1` is what keeps the stack private, not `TOROLLO_ALLOWED_ORIGINS`, which only covers browsers. Read [Self-Hosting & Network Exposure](#self-hosting--network-exposure) before changing it, and do not run untrusted Torollo images. Rootless Docker users can set `TOROLLO_DOCKER_SOCKET=/run/user//docker.sock`. ---