Deploy to the Coolify test server: docker-compose.coolify.yml
The stack Coolify builds from the repository, on the test server only for now, modelled on the sibling ActivityPods apps: the app (root Dockerfile), the backend (backend/Dockerfile) and a per-stack Redis; Fuseki is the shared one, reached over the external coolify network, with a pair of datasets per stack. One base URL per environment: the app at https://festipod.dev.reconnexion.coop, the backend's Domains https://<host>/api,https://<host>/.well-known, with Coolify's Strip Prefixes OFF. The Pod provider offered in one click defaults to https://mypod.store, as for the sibling dev apps. The deployment leaf no longer claims that nothing is deployed with this shape and that no Coolify config exists; it records the stack, the base-path layout, both images' build traps and the first-boot ones (set the domains before the first deployment; restart the backend once so /app carries the access-need group).
This commit is contained in:
@@ -3,7 +3,7 @@ type: _overview
|
||||
summary: Stack and tooling — Bun-first (runtime, bundler, native APIs), build pipeline, and the project's commands
|
||||
triggers:
|
||||
keywords: [bun, bunx, build, bundler, vite, webpack, jest, npm, storybook, "bun.serve", hmr, tailwind, package.json]
|
||||
paths: ["build.ts", "package.json", "pnpm-lock.yaml", "Dockerfile", ".env.example", "bunfig.toml", "tsconfig.json", "src/index.ts", "src/index.html", ".storybook/**", "scripts/**"]
|
||||
paths: ["build.ts", "package.json", "pnpm-lock.yaml", "Dockerfile", "backend/Dockerfile", "docker-compose*.yml", "backend/services/base-path.js", ".env.example", "bunfig.toml", "tsconfig.json", "src/index.ts", "src/index.html", ".storybook/**", "scripts/**"]
|
||||
---
|
||||
|
||||
# Tech stack
|
||||
@@ -18,4 +18,4 @@ The project's stack and tooling. Guiding principle: **Bun-first** — Bun replac
|
||||
- [[knowledge_bun-apis]] — native Bun APIs (serve, sqlite, redis, sql, file, shell)
|
||||
- [[knowledge_build-pipeline]] — build.ts, bundler, server, harness built separately, Storybook
|
||||
- [[knowledge_stack-and-commands]] — stack components + the real scripts (plus quirks)
|
||||
- [[knowledge_deployment]] — Dockerfile, prod served from src/, no CI, `portless` in dev
|
||||
- [[knowledge_deployment]] — the Coolify test stack (one base URL, backend under /api), both images, first-boot traps, prod served from src/, `portless` in dev
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Deployment — multi-stage Bun Alpine Dockerfile; install through pnpm (git+node inside the image) but bun at runtime; runs `bun run start` from src/ (not dist/), EXPOSE 3000; the data-layer git dependency must be pinned to a tag/commit and match `contracts.yaml`'s ref; the shared wallet reaches the container through env vars, not a mount, because it isn't a secret; no CI/CD committed; dev goes through the portless wrapper
|
||||
last_checked: 2026-08-17
|
||||
summary: Deployment — the Coolify test stack (app + backend + Redis, shared Fuseki), one base URL with the backend under /api (Strip Prefixes OFF), both images' build traps, first-boot traps, the data-layer git pin, the shared wallet as env config
|
||||
last_checked: 2026-10-19
|
||||
---
|
||||
|
||||
# Deployment & infra
|
||||
|
||||
Nothing has actually been deployed with this shape yet — this leaf states what a deployment needs to line up, verified against the code and manifests, not a procedure that has been run end to end.
|
||||
## The Coolify test stack
|
||||
|
||||
## Dockerfile
|
||||
`docker-compose.coolify.yml` is what Coolify (build pack *Docker Compose*) builds and runs from the git repository, on the test server only for now: `frontend` (the root `Dockerfile`, below), `backend` (`backend/Dockerfile`, Moleculer + `@activitypods/app`) and a per-stack Redis. Fuseki is the one shared with the other ActivityPods apps of the Coolify instance (`shared-infra`): only the backend joins the external `coolify` network to reach it, with a pair of datasets of its own named from `DATASET_PREFIX` and the backend's service name (a second non-preview stack on the same Fuseki needs its own prefix). The variables Coolify must carry are listed at the top of the compose file; `SERVICE_FQDN_*` / `SERVICE_NAME_*` are generated by Coolify.
|
||||
|
||||
**One base URL per environment** (user decision 2026-10-19, so a pull-request preview lives on a single host, like the sibling apps): the app at `https://festipod.dev.reconnexion.coop/`, the backend on the same host under `/api` — its Domains field is `https://<host>/api,https://<host>/.well-known`. Coolify's **Strip Prefixes must be OFF** (it is on the sibling apps): the backend mounts its routes under `/api` itself, read from `SEMAPPS_HOME_URL=https://<host>/api/`, and the front end is pointed at `VITE_BACKEND_URL=https://<host>/api`. With the prefix stripped every backend request is refused (404).
|
||||
|
||||
**The backend is base-path aware** (`backend/services/base-path.js`), because SemApps is: it mounts every route it adds under the base URL's path (LDP resources, the app actor and its inbox, public keys, access needs, webhooks, `/nodeinfo/2.1`) but keeps the `/.well-known` ones (`context.jsonld`, `nodeinfo`, `solid`, `void`) at the origin root. Festipod's own routes (`/app`, `/as/actor/app`, `/catalog`) are mounted under the base path the same way, the public-surface allow-list matches paths relative to it (the two `/.well-known` endpoints against the full path), and the Solid-OIDC `client_id` is the requested URI rebuilt from the base URL as written — so it equals, byte for byte, the actor URI the backend mints and the `${backendUrl}/as/actor/app` the front end derives. At the root (`http://localhost:3003`, local dev) every decision is identical to the pre-base-path code. To verify: `bun test backend/services` (with and without `SEMAPPS_HOME_URL=https://<host>/api/`).
|
||||
|
||||
**First-boot traps.**
|
||||
- The backend registers its app actor in its dataset with the domain it has on the FIRST boot: set the domains in Coolify before the first deployment; changing them later means deleting the two datasets and redeploying.
|
||||
- On a cold first boot, `GET <base>app` answers from an in-memory copy of the actor taken before the access-need group got linked, so it lacks `interop:hasAccessNeedGroup` until the backend restarts (the actor itself, `<base>as/actor/app`, is right). A real sign-in is unaffected — the pod reads the actor — but the rights matrix's programmatic install reads `/app`: restart the backend once after a first deployment.
|
||||
|
||||
**Verified 2026-10-19, locally, not yet on Coolify**: both images built from a clean export of the tree, run against the local pod provider behind a same-origin proxy that routes `/api` and `/.well-known` to the backend unstripped with the Host header kept (as Traefik does) — the login flow, the rights matrix (98/98 hard lines) and the revocation probe all pass; the same images at the origin root pass them too.
|
||||
|
||||
## Backend image
|
||||
|
||||
`node:22-slim`, `pnpm install --frozen-lockfile --prod`, `pnpm start` (tsx over the Moleculer runner). Two things it needs that a local install hides:
|
||||
- **pnpm 10 skips dependency build scripts unless approved**: `backend/package.json` approves `bcrypt`, `sharp` and `sodium-native` (`pnpm.onlyBuiltDependencies`). Without it the image builds and the container exits at start (`sharp-linux-x64.node` missing; bcrypt would fail next). A local `node_modules` may carry the binaries from an earlier manual build, which is how this stayed invisible.
|
||||
- **sharp must not depend on GitHub at build time**: its installer downloads prebuilt libvips and binaries from GitHub, and that download timed out on every in-build attempt (2026-10-19) and failed the build. The image installs `libvips-dev` (so sharp detects a system libvips and compiles itself) and sets `npm_config_nodedir=/usr/local` (node-gyp uses the image's own headers). bcrypt still tries GitHub first and falls back to compiling, slower but working offline. Cost: a larger image (a multi-stage build keeping only `libvips42` at runtime would drop several hundred MB — not done).
|
||||
|
||||
## App image (root Dockerfile)
|
||||
|
||||
A `Dockerfile` exists (multi-stage Bun Alpine). **Installation goes through pnpm, but runtime/build/test stay on bun** (see [[knowledge_stack-and-commands]]):
|
||||
- `FROM oven/bun:1-alpine`, `install` stage: `apk add --no-cache git nodejs npm` then `npm install -g pnpm@10.26.0` (the bun image has neither Node nor pnpm; Alpine's `apk nodejs` does not ship corepack), `COPY package.json pnpm-lock.yaml`, then `pnpm install --frozen-lockfile`. `git` is required because `@ng-eventually/polyfill` is a public **git+https** dependency (Gitea, no auth). `release` stage: copies `node_modules` plus the source.
|
||||
@@ -31,9 +49,7 @@ A `Dockerfile` exists (multi-stage Bun Alpine). **Installation goes through pnpm
|
||||
|
||||
**`pnpm install --frozen-lockfile` (the Dockerfile's install step) never regenerates — it only verifies.** `pnpm-lock.yaml` must already reproduce `package.json` exactly, so any change to the git specifier (ref, path, or package name) needs `pnpm install` run and the regenerated lockfile committed *before* the image can build; skipping that step fails the build outright, not silently. This has bitten once: the lockfile still named the old package and path after the dependency was renamed, so `--frozen-lockfile` refused and the image could not build until it was regenerated.
|
||||
|
||||
## CI/CD
|
||||
|
||||
**No** pipeline is committed (`.github/workflows/` absent, no Coolify config in the repo). A knowingly accepted blind spot. To host the Bun app, the `coolify-hosting` skill applies.
|
||||
## The NextGraph-era deployment
|
||||
|
||||
**A deployed origin IS embeddable in the hosted broker's iframe — VERIFIED 2026-08-17 in production**, on the first deployment carrying the injected wallet and the external data layer: a user signed in and saw their own data, which is only reachable through that iframe. The question had been open because nothing in this repo exercises it; it is settled for this origin, and it is settled by the deployment rather than by a test — **no scenario covers it**, so a change of origin, of proxy, or of the broker's embedding policy would be found by a person, not by the suite. [[caveat_firefox-lna-blocks-broker-iframe]] remains the one recorded failure mode, and it is a local-dev-origin one (`127.0.0.1` blocked by Firefox LNA).
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Docker Compose definition used by Coolify to build and run Festipod straight from the
|
||||
# git repository: one stack for the branch it follows (the test environment). The images
|
||||
# are built on the Coolify server from the two Dockerfiles of this repository (the app at
|
||||
# the root, the backend in /backend); nothing is pulled from a registry.
|
||||
#
|
||||
# Before `docker compose build` and `up`, Coolify writes a .env file next to this file
|
||||
# with the variables defined in its UI plus generated ones. The generated ones used here:
|
||||
# SERVICE_FQDN_BACKEND / SERVICE_FQDN_FRONTEND host of each service, as set in its
|
||||
# Domains field
|
||||
# SERVICE_NAME_BACKEND / SERVICE_NAME_FESTIPOD_REDIS service names (Coolify suffixes
|
||||
# them with "-pr-<n>" in a preview)
|
||||
# A value starting with `${` becomes an editable variable in Coolify (with its default);
|
||||
# the other `${...}` references are only interpolated by docker compose.
|
||||
#
|
||||
# One base URL per environment, as for the other ActivityPods apps on this Coolify (it is
|
||||
# what lets a pull request preview live on a single host): the frontend's domain serves the
|
||||
# app, and the backend is served on the same host under `/api` and `/.well-known` — its
|
||||
# Domains field is `https://<host>/api,https://<host>/.well-known`. The proxy must pass those
|
||||
# paths through unchanged — Coolify's "Strip Prefixes" OFF, as on the other apps: the backend
|
||||
# mounts its routes under `/api` itself, from its base URL, and keeps its `/.well-known` ones
|
||||
# at the origin root (backend/services/base-path.js). With the prefixes stripped, every
|
||||
# backend request would be refused (404).
|
||||
#
|
||||
# The backend registers its app actor in its main dataset on the first boot, with the
|
||||
# domain it had at that time: set the domains in Coolify BEFORE the first deployment. If
|
||||
# they change later, delete the two datasets in Fuseki and redeploy.
|
||||
#
|
||||
# Fuseki is the one shared with the other apps (shared-infra, in the "infra" project): the
|
||||
# backend joins the "coolify" network to reach it, with its own pair of datasets. Redis
|
||||
# runs inside the stack (on the stack's private network only, under a name that cannot be
|
||||
# confused with the shared one).
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
depends_on:
|
||||
- festipod-redis
|
||||
expose:
|
||||
- '3003'
|
||||
# Coolify adds the stack's own network to every service; only the backend also needs
|
||||
# the shared one, where Fuseki lives.
|
||||
networks:
|
||||
- coolify
|
||||
environment:
|
||||
SEMAPPS_APP_NAME: ${APP_NAME:-Festipod}
|
||||
SEMAPPS_APP_DESCRIPTION: ${APP_DESCRIPTION:-Points de rencontre sur les événements publics}
|
||||
SEMAPPS_APP_LANG: ${APP_LANG:-en,fr}
|
||||
SEMAPPS_HOME_URL: https://${SERVICE_FQDN_BACKEND}/api/
|
||||
SEMAPPS_FRONT_URL: https://${SERVICE_FQDN_FRONTEND}
|
||||
SEMAPPS_PORT: '3003'
|
||||
SEMAPPS_SHAPE_REPOSITORY_URL: ${SHAPE_REPOSITORY_URL:-https://shapes.activitypods.org/}
|
||||
SEMAPPS_SPARQL_ENDPOINT: ${SPARQL_ENDPOINT}
|
||||
SEMAPPS_JENA_USER: ${JENA_USER:-admin}
|
||||
SEMAPPS_JENA_PASSWORD: ${JENA_PASSWORD}
|
||||
# A pair of datasets named after DATASET_PREFIX and the backend's service name, e.g.
|
||||
# festipod-backend / settings-festipod-backend (…-backend-pr-<n> in a preview). Two
|
||||
# non-preview stacks on the same Fuseki would share them: give the second its own
|
||||
# DATASET_PREFIX (or MAIN_DATASET / SETTINGS_DATASET).
|
||||
SEMAPPS_MAIN_DATASET: ${MAIN_DATASET:-${DATASET_PREFIX:-festipod}-${SERVICE_NAME_BACKEND}}
|
||||
SEMAPPS_AUTH_ACCOUNTS_DATASET_NAME: ${SETTINGS_DATASET:-settings-${DATASET_PREFIX:-festipod}-${SERVICE_NAME_BACKEND}}
|
||||
SEMAPPS_REDIS_CACHE_URL: redis://${SERVICE_NAME_FESTIPOD_REDIS}:6379/0
|
||||
SEMAPPS_QUEUE_SERVICE_URL: redis://${SERVICE_NAME_FESTIPOD_REDIS}:6379/1
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
depends_on:
|
||||
- backend
|
||||
expose:
|
||||
- '3000'
|
||||
# Read by the server on every request to /festipod-config.json, not inlined at build
|
||||
# time: changing one needs a restart, not a rebuild.
|
||||
environment:
|
||||
PORT: '3000'
|
||||
VITE_BACKEND_URL: https://${SERVICE_FQDN_BACKEND}/api
|
||||
# The Pod provider the login screen offers in one click; any other can be typed in.
|
||||
VITE_POD_PROVIDER_URL: ${POD_PROVIDER_URL:-https://mypod.store}
|
||||
|
||||
festipod-redis:
|
||||
image: redis:7-alpine
|
||||
restart: always
|
||||
command: ['redis-server', '--appendonly', 'yes']
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
|
||||
networks:
|
||||
coolify:
|
||||
external: true
|
||||
Reference in New Issue
Block a user