From 7dc634275e042ca9fbbd8d02e8cffd36c2a7b9eb Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Tue, 29 Sep 2026 02:24:10 +0000 Subject: [PATCH] Put the cloud Dockerfile at the repo root, so a platform pointed at the repo finds it Railway, DigitalOcean and `docker build` use a root Dockerfile with no setting, and it is Coolify's and Dokploy's default path. The file moves; nothing is copied. - VOLUME /data is gone. Railway rejects the line, and a named volume is needed anyway (VOLUME alone gives each new container a fresh anonymous one). The platforms mount /data. - The baked PocketBase gate's comment named the old path. It now says the repo-root Dockerfile, and the gate's SHA-256 pin moves with it (2987055b... -> 2eceef03...). The hook's code is unchanged: the exact RUN passes `sha256sum -c`, and the old block still passes its old pin. - .dockerignore excludes everything: the Dockerfile copies nothing from the build context, so no platform sends the whole repository to the builder. - compose.yaml: the same image with its /data volume and memory limit; settings from .env. - deployment/cloud/README.md gains "Where it runs": Coolify, Dokploy, Railway, a server with Docker, Kubernetes, and why not DigitalOcean App Platform (no persistent volumes). DEPLOY.md and two comments follow the move. --- .dockerignore | 4 ++++ deployment/cloud/Dockerfile => Dockerfile | 12 +++++++----- compose.yaml | 22 +++++++++++++++++++++ deployment/cloud/README.md | 24 +++++++++++++++++++---- documentation/DEPLOY.md | 7 ++++--- internal/backend-dev-plugin.mjs | 2 +- internal/check-no-repo-pb-hooks.mjs | 4 ++-- 7 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 .dockerignore rename deployment/cloud/Dockerfile => Dockerfile (88%) create mode 100644 compose.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..bda63277b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +# The root Dockerfile copies nothing from the build context: at start the container clones the repo and +# runs deployment/cloud/entry.sh from it. Excluding everything keeps a platform from sending the whole +# repository to the builder. A Dockerfile that ever COPYs from the repo must re-include those paths here. +* diff --git a/deployment/cloud/Dockerfile b/Dockerfile similarity index 88% rename from deployment/cloud/Dockerfile rename to Dockerfile index ae048a1ce..27322f749 100644 --- a/deployment/cloud/Dockerfile +++ b/Dockerfile @@ -1,7 +1,10 @@ # dx-maps cloud dev server: one shared Vite dev server with hot reload for a whole team. # This image is only a bootstrap: at start it clones the repo and runs deployment/cloud/entry.sh -# FROM THE REPO, so the recipe lives in git and updates with it. Paste this file as-is into a -# container host (Coolify "Dockerfile" build pack, Railway, or `docker build`). +# FROM THE REPO, so the recipe lives in git and updates with it. It sits at the repo root so a +# platform pointed at the repo finds it (Railway, DigitalOcean and `docker build` with no setting; +# Coolify's and Dokploy's default Dockerfile path). It copies nothing from the build context +# (.dockerignore excludes everything), so it can also be pasted as-is into a container host. +# Mount a persistent volume at /data; the platforms are in deployment/cloud/README.md. FROM quay.io/oauth2-proxy/oauth2-proxy:v7.15.3 AS oauth2proxy FROM oven/bun:1-debian RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates curl unzip \ @@ -41,7 +44,7 @@ RUN curl -fsSL -o /tmp/pb.zip "https://github.com/pocketbase/pocketbase/releases # Changing it = edit these lines AND the SHA-256 below, then redeploy the image (an admin act). RUN mkdir -p /app/pb_hooks_gate && printf '%s\n' \ '// Team sign-in gate: only members of OIDC_ALLOWED_GROUPS (the groups claim from the identity provider) may sign in here.' \ - '// Baked into the image by deployment/cloud/Dockerfile and pinned by SHA-256: never read from the pulled repo.' \ + '// Baked into the image by the repo-root Dockerfile and pinned by SHA-256: never read from the pulled repo.' \ 'onRecordAuthWithOAuth2Request((e) => {' \ ' const allow = ($os.getenv("OIDC_ALLOWED_GROUPS") || "").split(",").map((s) => s.trim()).filter((s) => s)' \ ' const raw = (e.oAuth2User && e.oAuth2User.rawUser) || {}' \ @@ -53,7 +56,7 @@ RUN mkdir -p /app/pb_hooks_gate && printf '%s\n' \ ' e.next()' \ '}, "users")' \ > /app/pb_hooks_gate/team-gate.pb.js \ - && echo "2987055ba6067fc30d8162608da21c6c2b691fb6897a46d10b7075271a38f13d /app/pb_hooks_gate/team-gate.pb.js" | sha256sum -c - + && echo "2eceef03b18710b276b87ddab49e8aa9e65be5d54e27ef9ea6abf92c4cf122ee /app/pb_hooks_gate/team-gate.pb.js" | sha256sum -c - RUN printf '%s\n' \ '#!/bin/bash' \ 'set -u; export GIT_TERMINAL_PROMPT=0' \ @@ -64,6 +67,5 @@ RUN printf '%s\n' \ 'cp /app/repo/deployment/cloud/entry.sh /tmp/entry.sh && exec bash /tmp/entry.sh' \ > /boot.sh && chmod 755 /boot.sh ENV PORT=5173 BRANCH=main PULL_INTERVAL=15 -VOLUME /data EXPOSE 5173 CMD ["/boot.sh"] diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..63bcf135b --- /dev/null +++ b/compose.yaml @@ -0,0 +1,22 @@ +# One dx-maps team container, the same image a platform builds from ./Dockerfile: +# docker compose up -d --build +# Settings come from a .env file next to this one (never committed): REPO_URL is required, GIT_TOKEN +# for a private repo, and the OIDC_* settings for the sign-in; every setting is in deployment/cloud/README.md. +services: + dx-maps: + build: . + env_file: + - path: .env + required: false + environment: + REPO_URL: ${REPO_URL:?set REPO_URL to the dx-maps git URL, in .env} + BRANCH: ${BRANCH:-development} + PRODUCTION_BRANCH: ${PRODUCTION_BRANCH:-main} + ports: + - "${PORT:-5173}:${PORT:-5173}" + volumes: + - dx-data:/data + mem_limit: 2560m + restart: unless-stopped +volumes: + dx-data: diff --git a/deployment/cloud/README.md b/deployment/cloud/README.md index ed205bd31..c77756d3b 100644 --- a/deployment/cloud/README.md +++ b/deployment/cloud/README.md @@ -11,10 +11,10 @@ and the hot-reload websocket. Only people in the allowed groups get in. ## Deploy -1. Build the image from [`Dockerfile`](./Dockerfile). It's a small bootstrap (Bun, git, and the - [oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy) binary); at start it clones the repo - and runs [`entry.sh`](./entry.sh) from it, so the recipe updates with the repo. Paste it into any - container host (Coolify's "Dockerfile" build pack, Railway, `docker build`). +1. Build the image from the repo-root [`Dockerfile`](../../Dockerfile). It's a small bootstrap (Bun, git, + and the [oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy) binary); at start it clones the + repo and runs [`entry.sh`](./entry.sh) from it, so the recipe updates with the repo. Point a + container host at the repo, or paste the file into one: see [Where it runs](#where-it-runs). 2. Set the environment below and expose the container's `PORT` over HTTPS. 3. Give it about 2 GB of memory (Vite plus the dependency install). @@ -27,6 +27,22 @@ and the hot-reload websocket. Only people in the allowed groups get in. | `PORT` | no (`5173`) | the port the container serves on | | `HMR_CLIENT_PORT`, `HMR_PROTOCOL` | behind HTTPS | `443` and `wss`, so browsers reach the hot-reload websocket | +## Where it runs + +The container needs a Dockerfile build, a persistent volume at `/data` and about 2.5 GB of memory. + +| Platform | Build | `/data` | +|---|---|---| +| Coolify | the "Dockerfile" build pack (default location `/Dockerfile`) | Persistent Storage, mount path `/data` | +| Dokploy | Build Type "Dockerfile" (path `Dockerfile`) | Advanced, Volumes: mount path `/data` | +| Railway | finds the root `Dockerfile` by itself; set `PORT=5173` | add a volume at `/data`. Railway deploys from GitHub, a local directory or an image | +| A server with Docker | `docker compose up -d --build`, with the root [`compose.yaml`](../../compose.yaml) | the compose file's `dx-data` volume | +| Kubernetes | build the image in CI (BuildKit, Buildah) and push it to a registry | a PersistentVolumeClaim at `/data` | +| DigitalOcean App Platform | not suitable | it has no persistent volumes, so PocketBase's data would be lost on every deploy | + +There is no Railpack file. Railpack is Railway's default builder (Coolify and Dokploy offer it too), but it +can't reuse this Dockerfile; the container is defined here, once. + ## Sign-in (optional) Set all of these to turn the gate on. Register a **confidential** client at your OIDC provider diff --git a/documentation/DEPLOY.md b/documentation/DEPLOY.md index 0e66c7a0b..b0c4c0796 100644 --- a/documentation/DEPLOY.md +++ b/documentation/DEPLOY.md @@ -40,13 +40,14 @@ or for an agent that lives on a server. Nobody edits files on it: changes arrive `development`, the branch the team lands on, and publishes `main` (production) at `/cdn` (way 3). ```bash -docker build -t dx-maps-dev deployment/cloud +docker build -t dx-maps-dev . docker run -d -p 5173:5173 -v dx-maps-data:/data \ -e REPO_URL=https:////.git -e BRANCH=development -e PRODUCTION_BRANCH=main dx-maps-dev ``` -Or paste `deployment/cloud/Dockerfile` into a container host (Coolify's "Dockerfile" build pack) and give -it a domain. +Or `docker compose up -d --build` with the root `compose.yaml`, or point a container host at the repo: it +finds the root `Dockerfile` (the platforms are in [deployment/cloud/README.md](../deployment/cloud/README.md#where-it-runs)). +Give it a domain. - **Give it a persistent `/data` volume.** It holds the team PocketBase (dev and prod) and the last published build. Without one, every restart or redeploy starts empty. diff --git a/internal/backend-dev-plugin.mjs b/internal/backend-dev-plugin.mjs index 25b116ad4..3e31766c7 100644 --- a/internal/backend-dev-plugin.mjs +++ b/internal/backend-dev-plugin.mjs @@ -25,7 +25,7 @@ import { inflateRawSync } from 'node:zlib' import { resolve } from 'node:path' // PocketBase, the backend for plugins that need one: the SAME pinned version as the cloud container -// (deployment/cloud/Dockerfile), SHA-256 from the release's checksums.txt. Downloaded on the first +// (the repo-root Dockerfile), SHA-256 from the release's checksums.txt. Downloaded on the first // `bun run dev` into backend/bin (gitignored); its data lives in .pb/dev (gitignored). const PB_VERSION = '0.40.4' const PB_SHA256 = { diff --git a/internal/check-no-repo-pb-hooks.mjs b/internal/check-no-repo-pb-hooks.mjs index 728345f93..b70ce474e 100644 --- a/internal/check-no-repo-pb-hooks.mjs +++ b/internal/check-no-repo-pb-hooks.mjs @@ -5,7 +5,7 @@ * WHY: the cloud container pulls this repo every few seconds. A PocketBase hook is server-side JS; one pushed * by any plugin writer would run on the team backend within seconds, which is the rogue-writer hole the * roundtable test closed. The container never points PocketBase at the repo (--hooksDir is outside the - * pulled tree; the one team sign-in hook is baked into the image and pinned, deployment/cloud/Dockerfile). + * pulled tree; the one team sign-in hook is baked into the image and pinned, in the repo-root Dockerfile). * This gate keeps anyone from adding a hooks folder that LOOKS like it would run, or that a later recipe * change could start running. * @@ -26,7 +26,7 @@ const tracked = execFileSync('git', ['ls-files', '-z'], { cwd: ROOT }).toString( const bad = offending(tracked) if (bad.length) { console.error(`✗ PocketBase hooks in the repo (server-side JS must never ride the live pull):\n${bad.map((p) => ' ' + p).join('\n')}`) - console.error(' The team sign-in hook is baked into the image (deployment/cloud/Dockerfile); anything else needs a design review.') + console.error(' The team sign-in hook is baked into the image (the repo-root Dockerfile); anything else needs a design review.') process.exit(1) } console.log(`✓ no PocketBase hooks among ${tracked.length} tracked files`)