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.
This commit is contained in:
Doxa Maps 2026-09-29 02:24:10 +00:00
parent 9b4db40443
commit 7dc634275e
7 changed files with 60 additions and 15 deletions

4
.dockerignore Normal file
View file

@ -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.
*

View file

@ -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"]

22
compose.yaml Normal file
View file

@ -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:

View file

@ -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

View file

@ -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://<host>/<owner>/<repo>.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.

View file

@ -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 = {

View file

@ -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`)