ci(docker): versioned image builds + Gitea build/push workflow
Add comprehensive Docker image versioning and a Gitea Actions workflow that builds and pushes both the API and web images to the git.mancinas.io registry. Versioning: both Dockerfiles take APP_VERSION / GIT_SHA / BUILD_DATE build-args, surfaced as runtime ENV + OCI labels, so a running container self-reports the exact commit it was built from. metadata-action emits a tag set per build: semver (from vX.Y.Z git tags), branch ref, sha-<short>, and latest (default branch only). Also fix the Dockerfiles for the pnpm workspace: the old npm install could not resolve the "@jorgecuadros/database": "workspace:*" protocol dep and would abort the API build. Now pin pnpm 9.15.9 via corepack, install --frozen-lockfile with node-linker=hoisted (flat tree so the runtime stage copies a single node_modules), and build via --filter. The API build stage gets python3/make/g++ for argon2's musl source compile. Add .dockerignore to keep the build context lean and deterministic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
# Keep the build context small + deterministic. node_modules, build output, and
|
||||||
|
# the migration venv are all recreated inside the image, never copied from host.
|
||||||
|
**/node_modules
|
||||||
|
**/dist
|
||||||
|
**/.next
|
||||||
|
**/.turbo
|
||||||
|
apps/web/.next
|
||||||
|
packages/database/generated
|
||||||
|
migration/.venv
|
||||||
|
migration/**/__pycache__
|
||||||
|
**/*.log
|
||||||
|
.git
|
||||||
|
.idea
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Build + push the API and web container images to the git.mancinas.io registry.
|
||||||
|
#
|
||||||
|
# Two images from this one repo:
|
||||||
|
# git.mancinas.io/rmancinas/jorgecuadros-api
|
||||||
|
# git.mancinas.io/rmancinas/jorgecuadros-web
|
||||||
|
#
|
||||||
|
# Comprehensive versioning (docker/metadata-action). Every build pushes a set
|
||||||
|
# of tags so an image is addressable at several granularities:
|
||||||
|
# - vX.Y.Z / vX.Y when the trigger is a git tag vX.Y.Z (releases)
|
||||||
|
# - <branch> the branch that was pushed (e.g. master, feat-foo)
|
||||||
|
# - sha-<short> immutable per-commit id, always present
|
||||||
|
# - latest only on the default branch (master)
|
||||||
|
# The same version string + commit + build date are baked into the image as
|
||||||
|
# ARG/ENV (APP_VERSION / GIT_SHA / BUILD_DATE) and as OCI labels, so a running
|
||||||
|
# container can report exactly what is deployed.
|
||||||
|
#
|
||||||
|
# Release flow: git tag v1.2.0 && git push origin v1.2.0 -> versioned images.
|
||||||
|
|
||||||
|
name: Build and Push Images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
tags: ["v*"]
|
||||||
|
paths:
|
||||||
|
- "apps/**"
|
||||||
|
- "packages/**"
|
||||||
|
- "docker/**"
|
||||||
|
- "package.json"
|
||||||
|
- "pnpm-lock.yaml"
|
||||||
|
- ".gitea/workflows/build.yml"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.mancinas.io
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build ${{ matrix.image }}
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: docker:27-dind
|
||||||
|
options: --privileged
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- image: jorgecuadros-api
|
||||||
|
dockerfile: docker/api.Dockerfile
|
||||||
|
- image: jorgecuadros-web
|
||||||
|
dockerfile: docker/web.Dockerfile
|
||||||
|
steps:
|
||||||
|
- name: Install Node.js for actions
|
||||||
|
run: apk add --no-cache nodejs npm
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.image }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=ref,event=branch
|
||||||
|
type=sha,format=short,prefix=sha-
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ${{ matrix.dockerfile }}
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
platforms: linux/amd64
|
||||||
|
build-args: |
|
||||||
|
APP_VERSION=${{ steps.meta.outputs.version }}
|
||||||
|
GIT_SHA=${{ github.sha }}
|
||||||
|
BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
|
||||||
+49
-4
@@ -1,24 +1,69 @@
|
|||||||
FROM node:20-alpine AS base
|
FROM node:20-alpine AS base
|
||||||
WORKDIR /repo
|
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
|
FROM base AS deps
|
||||||
COPY package.json package-lock.json* ./
|
# argon2's native addon has no musl prebuild -> compiles from source here.
|
||||||
|
RUN apk add --no-cache python3 make g++
|
||||||
|
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||||
COPY apps/api/package.json apps/api/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
|
COPY packages/database/package.json packages/database/package.json
|
||||||
RUN npm install --workspace=packages/database --workspace=apps/api --no-audit --no-fund
|
# 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
|
FROM deps AS build
|
||||||
COPY packages/database packages/database
|
COPY packages/database packages/database
|
||||||
COPY apps/api apps/api
|
COPY apps/api apps/api
|
||||||
RUN npm run generate -w packages/database
|
RUN pnpm --filter @jorgecuadros/database generate
|
||||||
RUN npm run build -w apps/api
|
RUN pnpm --filter @jorgecuadros/api build
|
||||||
|
|
||||||
FROM node:20-alpine AS runtime
|
FROM node:20-alpine AS runtime
|
||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
ENV NODE_ENV=production
|
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.
|
||||||
|
RUN apk add --no-cache python3 mdbtools mysql-client \
|
||||||
|
&& 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/node_modules node_modules
|
||||||
COPY --from=build /repo/packages/database packages/database
|
COPY --from=build /repo/packages/database packages/database
|
||||||
COPY --from=build /repo/apps/api/dist apps/api/dist
|
COPY --from=build /repo/apps/api/dist apps/api/dist
|
||||||
COPY --from=build /repo/apps/api/package.json apps/api/package.json
|
COPY --from=build /repo/apps/api/package.json apps/api/package.json
|
||||||
|
|
||||||
|
# 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
|
EXPOSE 3001
|
||||||
CMD ["node", "apps/api/dist/main.js"]
|
CMD ["node", "apps/api/dist/main.js"]
|
||||||
|
|||||||
+17
-3
@@ -1,20 +1,34 @@
|
|||||||
FROM node:20-alpine AS base
|
FROM node:20-alpine AS base
|
||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
|
# Pin pnpm 9 to match pnpm-lock.yaml (lockfileVersion 9.0).
|
||||||
|
RUN corepack enable && corepack prepare pnpm@9.15.9 --activate
|
||||||
|
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
COPY package.json package-lock.json* ./
|
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 apps/web/package.json apps/web/package.json
|
||||||
RUN npm install --workspace=apps/web --no-audit --no-fund
|
COPY packages/database/package.json packages/database/package.json
|
||||||
|
# node-linker=hoisted -> single flat /repo/node_modules copied into runtime.
|
||||||
|
RUN pnpm install --frozen-lockfile --config.node-linker=hoisted
|
||||||
|
|
||||||
FROM deps AS build
|
FROM deps AS build
|
||||||
COPY apps/web apps/web
|
COPY apps/web apps/web
|
||||||
RUN npm run build -w apps/web
|
RUN pnpm --filter @jorgecuadros/web build
|
||||||
|
|
||||||
FROM node:20-alpine AS runtime
|
FROM node:20-alpine AS runtime
|
||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
COPY --from=build /repo/node_modules node_modules
|
COPY --from=build /repo/node_modules node_modules
|
||||||
COPY --from=build /repo/apps/web apps/web
|
COPY --from=build /repo/apps/web apps/web
|
||||||
|
|
||||||
|
# Build/version metadata baked in at image build time (see .gitea/workflows/build.yml).
|
||||||
|
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 3000
|
EXPOSE 3000
|
||||||
WORKDIR /repo/apps/web
|
WORKDIR /repo/apps/web
|
||||||
CMD ["npx", "next", "start"]
|
CMD ["npx", "next", "start"]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jorgecuadros-platform",
|
"name": "jorgecuadros-platform",
|
||||||
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
|
|||||||
Reference in New Issue
Block a user