The pre-migrate backup failed with "mysqldump exited 2" and nothing else.
Reproduced on the host with stderr captured:
ERROR 1045: Plugin caching_sha2_password could not be loaded:
/usr/lib/mariadb/plugin/caching_sha2_password.so: No such file or directory
Alpine's `mysql-client` is MariaDB's client and ships an EMPTY plugin
directory, so it cannot perform caching_sha2_password — MySQL 8.4's default and
effectively only auth method. `mariadb-connector-c` provides the plugin.
This was never about the deploy backup alone. Every mysqldump/mysql call from
the API container was broken, which means the whole Operaciones panel — backup,
restore, sync, re-import — could not work in a container. It went unnoticed
because that feature had only ever been run with the API on a developer
machine, where the Oracle client is installed. Verified after the fix: dump
exits 0, gzip valid, 31 CREATE TABLEs.
Also fixed, both found while chasing the above:
- The backup script reported an exit code and nothing else, because a detached
exec captures no output — which is precisely why this needed a manual
reproduction. mysqldump's stderr is now redirected to a file and read back
through a short attached exec on failure, so the deploy log states the cause.
Verified against live prod: the log now carries the 1045 line itself.
- Listing ONLY 100.100.100.100 as the containers' resolver costs them public
DNS, since MagicDNS does not forward upstream unless the tailnet defines
global nameservers. Nothing at runtime needed it, but `apk` inside the
container stopped resolving, and anything outbound would have too. A public
fallback resolver is now listed after MagicDNS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
5.0 KiB
Docker
101 lines
5.0 KiB
Docker
FROM node:20-alpine AS base
|
|
WORKDIR /repo
|
|
# Pin pnpm 9 to match pnpm-lock.yaml (lockfileVersion 9.0). pnpm 9 runs
|
|
# dependency build scripts automatically (the v10 build-allowlist gating does
|
|
# not apply), so argon2's native addon + prisma engines build without extra
|
|
# approval config.
|
|
RUN corepack enable && corepack prepare pnpm@9.15.9 --activate
|
|
|
|
FROM base AS deps
|
|
# argon2's native addon has no musl prebuild -> compiles from source here.
|
|
# openssl so `prisma generate` in the build stage sees the same platform the
|
|
# runtime stage does (see the binaryTargets note in schema.prisma).
|
|
RUN apk add --no-cache python3 make g++ openssl
|
|
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
|
COPY apps/api/package.json apps/api/package.json
|
|
COPY apps/web/package.json apps/web/package.json
|
|
COPY packages/database/package.json packages/database/package.json
|
|
# node-linker=hoisted flattens the store into a single npm-style /repo/node_modules
|
|
# so the runtime stage can copy one tree (pnpm's default symlinked layout would
|
|
# break across COPY stages).
|
|
RUN pnpm install --frozen-lockfile --config.node-linker=hoisted
|
|
|
|
FROM deps AS build
|
|
COPY packages/database packages/database
|
|
COPY apps/api apps/api
|
|
RUN pnpm --filter @jorgecuadros/database generate
|
|
RUN pnpm --filter @jorgecuadros/api build
|
|
|
|
FROM node:20-alpine AS runtime
|
|
WORKDIR /repo
|
|
ENV NODE_ENV=production
|
|
|
|
# DB-ops toolchain baked in so the "Operaciones" admin panel can run backups
|
|
# (mysqldump), restores (mysql), and the re-import pipeline (python + mdbtools)
|
|
# from inside the API container. Build deps are installed in a throwaway virtual
|
|
# package so pandas/pyarrow build on musl, then dropped from the final layer.
|
|
# openssl is NOT optional: Prisma's query engine resolves its binary target at
|
|
# runtime (linux-musl-openssl-3.0.x) and aborts with "Please manually install
|
|
# OpenSSL" without it. Node bundles its own OpenSSL, so nothing else in this
|
|
# image pulls the system package in.
|
|
# mariadb-connector-c is REQUIRED, not incidental. Alpine's `mysql-client` is
|
|
# MariaDB's client, and it ships with an EMPTY /usr/lib/mariadb/plugin — so it
|
|
# cannot perform caching_sha2_password, which is MySQL 8.4's default and
|
|
# effectively only auth method. Without this package every mysqldump/mysql call
|
|
# from the container dies with:
|
|
# ERROR 1045: Plugin caching_sha2_password could not be loaded
|
|
# That breaks the pre-migrate deploy backup AND the whole "Operaciones" admin
|
|
# panel (backup, restore, sync, re-import all shell out to these binaries).
|
|
RUN apk add --no-cache python3 mdbtools mysql-client mariadb-connector-c openssl \
|
|
&& apk add --no-cache --virtual .pybuild python3-dev build-base \
|
|
&& rm -rf /var/cache/apk/*
|
|
|
|
COPY --from=build /repo/node_modules node_modules
|
|
COPY --from=build /repo/packages/database packages/database
|
|
COPY --from=build /repo/apps/api/dist apps/api/dist
|
|
COPY --from=build /repo/apps/api/package.json apps/api/package.json
|
|
# Operational scripts, run on demand — never automatically. seed-user.mjs is the
|
|
# only way to create the first sign-in account on a fresh database, and without
|
|
# it in the image that had to be done from a developer's machine against a
|
|
# production DATABASE_URL. Run it with:
|
|
# docker exec <api> node apps/api/scripts/seed-user.mjs
|
|
# honouring SEED_EMAIL / SEED_PASSWORD / SEED_NAME. It upserts, so re-running is
|
|
# safe — but note it RESETS the password of an existing account.
|
|
COPY --from=build /repo/apps/api/scripts apps/api/scripts
|
|
# node-linker=hoisted flattens EXTERNAL deps into /repo/node_modules, but the
|
|
# workspace dependency is still linked per-package:
|
|
# apps/api/node_modules/@jorgecuadros/database -> ../../../../packages/database
|
|
# Copying only /repo/node_modules therefore drops it and the API dies at boot
|
|
# with "Cannot find module '@jorgecuadros/database'". Copy just the scope dir —
|
|
# the rest of apps/api/node_modules is devDependencies (typescript) we don't
|
|
# want in the runtime layer. The relative link resolves because packages/database
|
|
# is copied to the same place above.
|
|
COPY --from=build /repo/apps/api/node_modules/@jorgecuadros apps/api/node_modules/@jorgecuadros
|
|
|
|
# Migration scripts + their own Python venv (ops.service.ts prefers this venv).
|
|
COPY migration migration
|
|
RUN python3 -m venv migration/.venv \
|
|
&& migration/.venv/bin/pip install --no-cache-dir -r migration/requirements.txt \
|
|
&& apk del .pybuild
|
|
|
|
# Ingest (uploaded Access files) and backups live on mounted volumes.
|
|
ENV MIGRATION_DIR=/repo/migration \
|
|
INGEST_DIR=/data/ingest \
|
|
BACKUP_DIR=/data/backups \
|
|
MIGRATION_ENV=dev
|
|
RUN mkdir -p /data/ingest /data/backups
|
|
|
|
# Build/version metadata baked in at image build time (see .gitea/workflows/build.yml).
|
|
# APP_VERSION is the metadata-action primary tag (semver tag, branch, or sha);
|
|
# GIT_SHA/BUILD_DATE pin the exact commit + build instant. Exposed as ENV so a
|
|
# running container can self-report what is deployed (e.g. a /version endpoint).
|
|
ARG APP_VERSION=dev
|
|
ARG GIT_SHA=unknown
|
|
ARG BUILD_DATE=unknown
|
|
ENV APP_VERSION=$APP_VERSION \
|
|
GIT_SHA=$GIT_SHA \
|
|
BUILD_DATE=$BUILD_DATE
|
|
|
|
EXPOSE 3001
|
|
CMD ["node", "apps/api/dist/main.js"]
|