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. # 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 # 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 # FROM THE REPO, so the recipe lives in git and updates with it. It sits at the repo root so a
# container host (Coolify "Dockerfile" build pack, Railway, or `docker build`). # 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 quay.io/oauth2-proxy/oauth2-proxy:v7.15.3 AS oauth2proxy
FROM oven/bun:1-debian FROM oven/bun:1-debian
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates curl unzip \ 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). # 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' \ 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.' \ '// 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) => {' \ 'onRecordAuthWithOAuth2Request((e) => {' \
' const allow = ($os.getenv("OIDC_ALLOWED_GROUPS") || "").split(",").map((s) => s.trim()).filter((s) => s)' \ ' const allow = ($os.getenv("OIDC_ALLOWED_GROUPS") || "").split(",").map((s) => s.trim()).filter((s) => s)' \
' const raw = (e.oAuth2User && e.oAuth2User.rawUser) || {}' \ ' const raw = (e.oAuth2User && e.oAuth2User.rawUser) || {}' \
@ -53,7 +56,7 @@ RUN mkdir -p /app/pb_hooks_gate && printf '%s\n' \
' e.next()' \ ' e.next()' \
'}, "users")' \ '}, "users")' \
> /app/pb_hooks_gate/team-gate.pb.js \ > /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' \ RUN printf '%s\n' \
'#!/bin/bash' \ '#!/bin/bash' \
'set -u; export GIT_TERMINAL_PROMPT=0' \ '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' \ 'cp /app/repo/deployment/cloud/entry.sh /tmp/entry.sh && exec bash /tmp/entry.sh' \
> /boot.sh && chmod 755 /boot.sh > /boot.sh && chmod 755 /boot.sh
ENV PORT=5173 BRANCH=main PULL_INTERVAL=15 ENV PORT=5173 BRANCH=main PULL_INTERVAL=15
VOLUME /data
EXPOSE 5173 EXPOSE 5173
CMD ["/boot.sh"] 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 ## Deploy
1. Build the image from [`Dockerfile`](./Dockerfile). It's a small bootstrap (Bun, git, and the 1. Build the image from the repo-root [`Dockerfile`](../../Dockerfile). It's a small bootstrap (Bun, git,
[oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy) binary); at start it clones the repo and the [oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy) binary); at start it clones the
and runs [`entry.sh`](./entry.sh) from it, so the recipe updates with the repo. Paste it into any repo and runs [`entry.sh`](./entry.sh) from it, so the recipe updates with the repo. Point a
container host (Coolify's "Dockerfile" build pack, Railway, `docker build`). 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. 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). 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 | | `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 | | `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) ## Sign-in (optional)
Set all of these to turn the gate on. Register a **confidential** client at your OIDC provider 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). `development`, the branch the team lands on, and publishes `main` (production) at `/cdn` (way 3).
```bash ```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 \ 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 -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 Or `docker compose up -d --build` with the root `compose.yaml`, or point a container host at the repo: it
it a domain. 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 - **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. 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' import { resolve } from 'node:path'
// PocketBase, the backend for plugins that need one: the SAME pinned version as the cloud container // 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). // `bun run dev` into backend/bin (gitignored); its data lives in .pb/dev (gitignored).
const PB_VERSION = '0.40.4' const PB_VERSION = '0.40.4'
const PB_SHA256 = { 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 * 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 * 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 * 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 * This gate keeps anyone from adding a hooks folder that LOOKS like it would run, or that a later recipe
* change could start running. * change could start running.
* *
@ -26,7 +26,7 @@ const tracked = execFileSync('git', ['ls-files', '-z'], { cwd: ROOT }).toString(
const bad = offending(tracked) const bad = offending(tracked)
if (bad.length) { 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(`✗ 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) process.exit(1)
} }
console.log(`✓ no PocketBase hooks among ${tracked.length} tracked files`) console.log(`✓ no PocketBase hooks among ${tracked.length} tracked files`)