Mirrored from GitHub github.com/roostorg/coop
0

Configure Feed

Select the types of activity you want to include in your feed.

[docker] Create docker image for migrations scripts to run (#708)

* [docker] Create docker image for migrations scripts to run

* minor fixes

* code review changes

author
Juan Mrad
committer
GitHub
date (Jun 7, 2026, 12:44 PM -0500) commit ef12e7ae parent 0884cf8a
+136 -34
+50
.github/workflows/publish-docker.yaml
··· 6 6 paths: 7 7 - 'server/**' 8 8 - 'client/**' 9 + - 'db/**' 9 10 - 'Dockerfile' 10 11 - 'client/Dockerfile' 11 12 - '.github/workflows/publish-docker.yaml' ··· 25 26 outputs: 26 27 server: ${{ steps.filter.outputs.server }} 27 28 client: ${{ steps.filter.outputs.client }} 29 + db: ${{ steps.filter.outputs.db }} 28 30 steps: 29 31 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 30 32 ··· 38 40 - 'Dockerfile' 39 41 client: 40 42 - 'client/**' 43 + db: 44 + - 'db/**' 41 45 42 46 # On release/dispatch, paths-filter is skipped so outputs are empty. 43 47 # The downstream jobs use != 'false', so empty triggers a build. ··· 138 142 cache-from: type=gha 139 143 cache-to: type=gha,mode=max 140 144 platforms: linux/amd64,linux/arm64 145 + 146 + # Bundles the db/ migration files + configs into an image meant to be run as a 147 + # one-shot task on deploy. 148 + # On push to main it publishes only when something under db/ changed (e.g. a 149 + # new migration script). On a release it always publishes a versioned tag. 150 + build-migrations: 151 + needs: changes 152 + if: needs.changes.outputs.db != 'false' 153 + runs-on: ubuntu-latest 154 + permissions: 155 + contents: read 156 + packages: write 157 + steps: 158 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 159 + 160 + - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 161 + 162 + - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 163 + 164 + - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 165 + with: 166 + registry: ${{ env.REGISTRY }} 167 + username: ${{ github.actor }} 168 + password: ${{ secrets.GITHUB_TOKEN }} 169 + 170 + - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 171 + id: meta 172 + with: 173 + images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/coop-migrations 174 + tags: | 175 + type=sha,prefix= 176 + type=ref,event=branch 177 + type=semver,pattern={{version}} 178 + type=semver,pattern={{major}}.{{minor}} 179 + type=raw,value=latest,enable={{is_default_branch}} 180 + 181 + - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 182 + with: 183 + context: db 184 + file: db/Dockerfile 185 + push: true 186 + tags: ${{ steps.meta.outputs.tags }} 187 + labels: ${{ steps.meta.outputs.labels }} 188 + cache-from: type=gha 189 + cache-to: type=gha,mode=max 190 + platforms: linux/amd64,linux/arm64
+11 -2
db/Dockerfile
··· 17 17 18 18 WORKDIR /app 19 19 20 - COPY --from=builder /app/build ./ 20 + # Keep output under ./build (not flattened) so the `db:*` scripts resolve 21 + # `build/index.js` both here and locally after `npm run build`. 22 + COPY --from=builder /app/build ./build 23 + COPY --from=builder /app/package.json ./package.json 21 24 COPY --from=builder /app/node_modules ./node_modules 22 25 23 26 # I create a separate stage for these steps because they are not needed for 24 27 # development and chown can take a long time to run 25 28 FROM base 26 29 27 - RUN groupadd -r coop && useradd -r -g coop -u 1001 coop 30 + # -m creates a home dir for `coop` so `npm run` has a writable HOME for its 31 + # cache/logs (the container runs `npm run db:*` as this non-root user). 32 + RUN groupadd -r coop && useradd -r -g coop -u 1001 -m coop 28 33 29 34 RUN chown -R coop:coop /app 30 35 31 36 # kubernetes requires us to use a numeric id for the user 32 37 USER 1001 38 + 39 + # One-shot migrations task; override the command at runtime, e.g. 40 + # npm run db:update -- --db api-server-pg --env prod 41 + CMD ["node", "build/index.js"]
+6 -1
db/package.json
··· 6 6 "type": "module", 7 7 "scripts": { 8 8 "build": "tsc && cp -R src/scripts package.json build/", 9 - "test": "echo \"Error: no test specified\" && exit 1" 9 + "test": "echo \"Error: no test specified\" && exit 1", 10 + "db:add": "node build/index.js add", 11 + "db:clean": "node build/index.js clean", 12 + "db:update": "node build/index.js apply", 13 + "db:create": "node build/index.js create", 14 + "db:drop": "node build/index.js drop" 10 15 }, 11 16 "author": "Roostorg", 12 17 "license": "ISC",
+11 -9
docker-compose.images.yaml
··· 29 29 volumes: 30 30 - scylla_data:/var/lib/scylla 31 31 healthcheck: 32 - test: ['CMD-SHELL', 'nodetool status || exit 1'] 32 + # Real CQL query, not `nodetool status`, so 9042 accepts connections 33 + # before migrations start. 34 + test: 35 + [ 36 + 'CMD-SHELL', 37 + 'cqlsh -e "SELECT release_version FROM system.local" || exit 1', 38 + ] 33 39 interval: 5s 34 - timeout: 5s 35 - retries: 28 40 + timeout: 10s 41 + retries: 30 36 42 start_period: 35s 37 43 38 44 clickhouse: ··· 51 57 retries: 5 52 58 53 59 migrations: 54 - image: node:24.14.1-bullseye-slim 55 - command: bash -c 'set -e 56 - && npm i 60 + image: ghcr.io/roostorg/coop-migrations:latest 61 + command: sh -c 'set -e 57 62 && for db in api-server-pg scylla clickhouse; do npm run db:create -- --db "$$db" --env staging; npm run db:update -- --db "$$db" --env staging; done' 58 - working_dir: /src 59 63 env_file: ./.env.docker 60 - volumes: 61 - - .:/src 62 64 depends_on: 63 65 postgres: 64 66 condition: service_healthy
+50 -9
docs/development/docker.md
··· 3 3 Pre-built images are published to the GitHub Container Registry on every push to `main`: 4 4 5 5 ``` 6 - ghcr.io/roostorg/coop-server # API server 7 - ghcr.io/roostorg/coop-worker # Background worker 8 - ghcr.io/roostorg/coop-client # Frontend (nginx) 6 + ghcr.io/roostorg/coop-server # API server 7 + ghcr.io/roostorg/coop-worker # Background worker 8 + ghcr.io/roostorg/coop-client # Frontend (nginx) 9 + ghcr.io/roostorg/coop-migrations # One-shot database migrations runner 9 10 ``` 10 11 11 - Images are tagged with `latest`, the git SHA, and semver tags on release. 12 + Images are tagged with `latest`, the git SHA, and semver tags on release. The 13 + `coop-migrations` image is only rebuilt when something under `db/` changes (e.g. 14 + a new migration script) or on a release, so its tags track the schema rather 15 + than every server/client change. 12 16 13 17 ## Quick start 14 18 ··· 75 79 76 80 ## Image details 77 81 78 - | Image | Dockerfile | Build target | Base | 79 - | ------------- | ------------------- | --------------------- | --------------------------------- | 80 - | `coop-server` | `Dockerfile` | `build_server` | node:24-bullseye-slim + dumb-init | 81 - | `coop-worker` | `Dockerfile` | `build_worker_runner` | node:24-bullseye-slim + dumb-init | 82 - | `coop-client` | `client/Dockerfile` | `serve` | nginx:1.27-bookworm | 82 + | Image | Dockerfile | Build target | Base | 83 + | ----------------- | ------------------- | --------------------- | --------------------------------- | 84 + | `coop-server` | `Dockerfile` | `build_server` | node:24-bullseye-slim + dumb-init | 85 + | `coop-worker` | `Dockerfile` | `build_worker_runner` | node:24-bullseye-slim + dumb-init | 86 + | `coop-client` | `client/Dockerfile` | `serve` | nginx:1.27-bookworm | 87 + | `coop-migrations` | `db/Dockerfile` | _(final stage)_ | node:24-bullseye-slim | 83 88 84 89 The client image serves the Vite-built SPA via nginx and proxies `/api/` requests (including `/api/v1/graphql`) to a backend service named `server` on port 8080. 90 + 91 + ## Running migrations 92 + 93 + The `coop-migrations` image bundles the migration scripts under `db/` together 94 + with the migrator engine, so it can run as a one-shot task (an ECS `RunTask`, a 95 + Kubernetes `Job`, or the `migrations` service in `docker-compose.images.yaml`). 96 + It exposes the same `npm run db:*` commands used in local development, so the 97 + invocation matches what you'd run from the repo root: 98 + 99 + ```bash 100 + # Apply all pending migrations to an existing prod database 101 + docker run --rm --env-file .env.docker ghcr.io/roostorg/coop-migrations:latest \ 102 + npm run db:update -- --db api-server-pg --env prod 103 + ``` 104 + 105 + Supported `--db` values are `api-server-pg`, `scylla`, and `clickhouse`. 106 + Connection settings come from environment variables (see `db/.env.example`), 107 + which must be present for any command since the database configs are read at 108 + startup. 109 + 110 + ### What `--env` controls 111 + 112 + `--env` (`staging` or `prod`) only affects **seed scripts**, not migrations: 113 + 114 + - Migration scripts run in every environment. 115 + - A seed named `*.seed.<env>.sql` runs **only** when `--env` matches it. Seed 116 + files are timestamp-prefixed; the repo currently ships 117 + `db/src/scripts/api-server-pg/2025.12.01T00.00.01.initial-test-data.seed.staging.sql`, 118 + which creates a sample org with default-password users — so **use `--env prod` 119 + in production** to skip it. Running `--env staging` against a prod database 120 + would seed that test data. 121 + - `db:create` ignores `--env` functionally and is allowed in prod, so you can 122 + provision schemas for self-hosted Scylla/ClickHouse (which have no managed 123 + "create the database" step). 124 + - `db:clean` and `db:drop` are destructive and **reject `--env prod`** as a 125 + safety guard.
+8 -13
migrator/cli/index.ts
··· 1 - import path from 'path'; 2 1 import { glob } from 'node:fs/promises'; 2 + import path from 'path'; 3 3 4 4 import '@total-typescript/ts-reset/array-includes'; 5 5 ··· 330 330 command: 'create', 331 331 describe: 'Creates the databases specified in the ENV vars.', 332 332 builder: (yargs) => { 333 - return yargs 334 - .option('db', dbOpt) 335 - .option('env', { 336 - ...envOpt, 337 - demand: 338 - 'Must provide an environment (even though it has no effect; the ' + 339 - 'connection-related env vars determine which db(s) are cleaned) ' + 340 - 'to help prevent accidentally deleting prod!', 341 - }) 342 - .check((opts) => { 343 - return opts.env !== 'prod'; 344 - }); 333 + return yargs.option('db', dbOpt).option('env', { 334 + ...envOpt, 335 + demand: 336 + 'Must provide an environment (even though it has no functional ' + 337 + 'effect; the connection-related env vars determine which db(s) ' + 338 + 'are created).', 339 + }); 345 340 }, 346 341 handler: async ({ db: optDbs }) => { 347 342 await Promise.all(