From d8b7b9735b2f1d3e2b74f2b4b2fcf8a39c11ef3a Mon Sep 17 00:00:00 2001 From: Marco Chan Date: Fri, 4 Sep 2026 12:38:53 +0800 Subject: [PATCH] feat: add Dockerfiles and workflow configurations for CRUD, Ingest, and Web APIs --- .env.production | 21 ++++++++ .gitea/workflows/release-crud-api.yml | 72 +++++++++++++++++++++++++ .gitea/workflows/release-ingest-api.yml | 72 +++++++++++++++++++++++++ .gitea/workflows/release-web.yml | 72 +++++++++++++++++++++++++ apps/crud-api/Dockerfile | 36 +++++++++++++ apps/ingest-api/Dockerfile | 36 +++++++++++++ apps/web/Dockerfile | 25 +++++++++ 7 files changed, 334 insertions(+) create mode 100644 .env.production create mode 100644 .gitea/workflows/release-crud-api.yml create mode 100644 .gitea/workflows/release-ingest-api.yml create mode 100644 .gitea/workflows/release-web.yml create mode 100644 apps/crud-api/Dockerfile create mode 100644 apps/ingest-api/Dockerfile create mode 100644 apps/web/Dockerfile diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..da2f2a1 --- /dev/null +++ b/.env.production @@ -0,0 +1,21 @@ +# Unified production environment template +# Copy and fill values with your real production endpoints/secrets. + +NODE_ENV=production + +# Shared infrastructure +DATABASE_URL=postgres://user:password@db-host:5432/app +MONGO_URL=mongodb://user:password@mongo-host:27017/app +REDIS_URL=redis://redis-host:6379 + +# API ports (optional, if your runtime uses explicit ports) +CRUD_PORT=3000 +INGEST_PORT=3001 + +# Service-specific prefixes used by release filtering +CRUD_HOST=0.0.0.0 +INGEST_HOST=0.0.0.0 + +# Frontend build/runtime values +VITE_API_BASE_URL=https://api.example.com +VITE_INGEST_BASE_URL=https://ingest.example.com diff --git a/.gitea/workflows/release-crud-api.yml b/.gitea/workflows/release-crud-api.yml new file mode 100644 index 0000000..8e1af9c --- /dev/null +++ b/.gitea/workflows/release-crud-api.yml @@ -0,0 +1,72 @@ +name: release-crud-api + +on: + push: + tags: + - 'crud-api/v*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Extract version from tag + id: meta + shell: bash + run: | + set -euo pipefail + TAG="${GITHUB_REF_NAME}" + VERSION="${TAG#crud-api/v}" + echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" + + - name: Build Docker image (root context) + shell: bash + run: | + set -euo pipefail + docker build \ + --file apps/crud-api/Dockerfile \ + --tag crud-api:${{ steps.meta.outputs.version }} \ + . + + - name: Package tar + filtered env + id: bundle + shell: bash + run: | + set -euo pipefail + VERSION="${{ steps.meta.outputs.version }}" + OUT_DIR="dist/release" + TAR_PATH="${OUT_DIR}/crud-api-${VERSION}.tar" + ZIP_PATH="${OUT_DIR}/crud-api-${VERSION}.zip" + ENV_PATH="${OUT_DIR}/.env" + + mkdir -p "${OUT_DIR}" + docker save --output "${TAR_PATH}" "crud-api:${VERSION}" + + if [ ! -f .env.production ]; then + echo ".env.production is missing at repository root" >&2 + exit 1 + fi + + # Keep only variables needed by crud-api deployment. + grep -E '^(NODE_ENV|CRUD_|DATABASE_URL|REDIS_URL)=' .env.production > "${ENV_PATH}" + + zip -j "${ZIP_PATH}" "${TAR_PATH}" "${ENV_PATH}" + echo "zip_path=${ZIP_PATH}" >> "${GITHUB_OUTPUT}" + + - name: Create Gitea release with artifact + uses: https://gitea.com/actions/gitea-release-action@v1 + with: + token: ${{ secrets.RELEASE_TOKEN }} + tag_name: ${{ github.ref_name }} + name: crud-api v${{ steps.meta.outputs.version }} + body: | + Automated release bundle for crud-api. + Includes: + - Docker image tar + - filtered .env for deploy + files: | + ${{ steps.bundle.outputs.zip_path }} diff --git a/.gitea/workflows/release-ingest-api.yml b/.gitea/workflows/release-ingest-api.yml new file mode 100644 index 0000000..27fa57b --- /dev/null +++ b/.gitea/workflows/release-ingest-api.yml @@ -0,0 +1,72 @@ +name: release-ingest-api + +on: + push: + tags: + - 'ingest-api/v*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Extract version from tag + id: meta + shell: bash + run: | + set -euo pipefail + TAG="${GITHUB_REF_NAME}" + VERSION="${TAG#ingest-api/v}" + echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" + + - name: Build Docker image (root context) + shell: bash + run: | + set -euo pipefail + docker build \ + --file apps/ingest-api/Dockerfile \ + --tag ingest-api:${{ steps.meta.outputs.version }} \ + . + + - name: Package tar + filtered env + id: bundle + shell: bash + run: | + set -euo pipefail + VERSION="${{ steps.meta.outputs.version }}" + OUT_DIR="dist/release" + TAR_PATH="${OUT_DIR}/ingest-api-${VERSION}.tar" + ZIP_PATH="${OUT_DIR}/ingest-api-${VERSION}.zip" + ENV_PATH="${OUT_DIR}/.env" + + mkdir -p "${OUT_DIR}" + docker save --output "${TAR_PATH}" "ingest-api:${VERSION}" + + if [ ! -f .env.production ]; then + echo ".env.production is missing at repository root" >&2 + exit 1 + fi + + # Keep only variables needed by ingest-api deployment. + grep -E '^(NODE_ENV|INGEST_|DATABASE_URL|MONGO_URL|REDIS_URL)=' .env.production > "${ENV_PATH}" + + zip -j "${ZIP_PATH}" "${TAR_PATH}" "${ENV_PATH}" + echo "zip_path=${ZIP_PATH}" >> "${GITHUB_OUTPUT}" + + - name: Create Gitea release with artifact + uses: https://gitea.com/actions/gitea-release-action@v1 + with: + token: ${{ secrets.RELEASE_TOKEN }} + tag_name: ${{ github.ref_name }} + name: ingest-api v${{ steps.meta.outputs.version }} + body: | + Automated release bundle for ingest-api. + Includes: + - Docker image tar + - filtered .env for deploy + files: | + ${{ steps.bundle.outputs.zip_path }} diff --git a/.gitea/workflows/release-web.yml b/.gitea/workflows/release-web.yml new file mode 100644 index 0000000..c7cb755 --- /dev/null +++ b/.gitea/workflows/release-web.yml @@ -0,0 +1,72 @@ +name: release-web + +on: + push: + tags: + - 'web/v*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Extract version from tag + id: meta + shell: bash + run: | + set -euo pipefail + TAG="${GITHUB_REF_NAME}" + VERSION="${TAG#web/v}" + echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" + + - name: Build Docker image (root context) + shell: bash + run: | + set -euo pipefail + docker build \ + --file apps/web/Dockerfile \ + --tag web:${{ steps.meta.outputs.version }} \ + . + + - name: Package tar + filtered env + id: bundle + shell: bash + run: | + set -euo pipefail + VERSION="${{ steps.meta.outputs.version }}" + OUT_DIR="dist/release" + TAR_PATH="${OUT_DIR}/web-${VERSION}.tar" + ZIP_PATH="${OUT_DIR}/web-${VERSION}.zip" + ENV_PATH="${OUT_DIR}/.env" + + mkdir -p "${OUT_DIR}" + docker save --output "${TAR_PATH}" "web:${VERSION}" + + if [ ! -f .env.production ]; then + echo ".env.production is missing at repository root" >&2 + exit 1 + fi + + # Keep only variables needed by web deployment. + grep -E '^(NODE_ENV|VITE_)=' .env.production > "${ENV_PATH}" + + zip -j "${ZIP_PATH}" "${TAR_PATH}" "${ENV_PATH}" + echo "zip_path=${ZIP_PATH}" >> "${GITHUB_OUTPUT}" + + - name: Create Gitea release with artifact + uses: https://gitea.com/actions/gitea-release-action@v1 + with: + token: ${{ secrets.RELEASE_TOKEN }} + tag_name: ${{ github.ref_name }} + name: web v${{ steps.meta.outputs.version }} + body: | + Automated release bundle for web. + Includes: + - Docker image tar + - filtered .env for deploy + files: | + ${{ steps.bundle.outputs.zip_path }} diff --git a/apps/crud-api/Dockerfile b/apps/crud-api/Dockerfile new file mode 100644 index 0000000..d2d955a --- /dev/null +++ b/apps/crud-api/Dockerfile @@ -0,0 +1,36 @@ +# syntax=docker/dockerfile:1.7 + +# Build from the monorepo root context so this image can use: +# - apps/crud-api +# - packages/contracts +FROM node:20-alpine AS base +WORKDIR /workspace +RUN corepack enable + +FROM base AS deps +COPY package.json yarn.lock .yarnrc.yml ./ +COPY .yarn/ ./.yarn/ +COPY apps/crud-api/package.json ./apps/crud-api/package.json +COPY packages/contracts/package.json ./packages/contracts/package.json + +# Use node_modules inside the container for predictable runtime behavior. +RUN YARN_NODE_LINKER=node-modules yarn install --immutable + +FROM deps AS build +COPY apps/crud-api ./apps/crud-api +COPY packages/contracts ./packages/contracts +RUN yarn workspace crud-api build + +FROM node:20-alpine AS runtime +WORKDIR /workspace +ENV NODE_ENV=production +RUN corepack enable + +COPY --from=build /workspace/package.json /workspace/yarn.lock /workspace/.yarnrc.yml ./ +COPY --from=build /workspace/.yarn ./.yarn +COPY --from=build /workspace/node_modules ./node_modules +COPY --from=build /workspace/apps/crud-api ./apps/crud-api +COPY --from=build /workspace/packages/contracts ./packages/contracts + +EXPOSE 3000 +CMD ["yarn", "workspace", "crud-api", "start"] diff --git a/apps/ingest-api/Dockerfile b/apps/ingest-api/Dockerfile new file mode 100644 index 0000000..93cf4ac --- /dev/null +++ b/apps/ingest-api/Dockerfile @@ -0,0 +1,36 @@ +# syntax=docker/dockerfile:1.7 + +# Build from the monorepo root context so this image can use: +# - apps/ingest-api +# - packages/contracts +FROM node:20-alpine AS base +WORKDIR /workspace +RUN corepack enable + +FROM base AS deps +COPY package.json yarn.lock .yarnrc.yml ./ +COPY .yarn/ ./.yarn/ +COPY apps/ingest-api/package.json ./apps/ingest-api/package.json +COPY packages/contracts/package.json ./packages/contracts/package.json + +# Use node_modules inside the container for predictable runtime behavior. +RUN YARN_NODE_LINKER=node-modules yarn install --immutable + +FROM deps AS build +COPY apps/ingest-api ./apps/ingest-api +COPY packages/contracts ./packages/contracts +RUN yarn workspace ingest-api build + +FROM node:20-alpine AS runtime +WORKDIR /workspace +ENV NODE_ENV=production +RUN corepack enable + +COPY --from=build /workspace/package.json /workspace/yarn.lock /workspace/.yarnrc.yml ./ +COPY --from=build /workspace/.yarn ./.yarn +COPY --from=build /workspace/node_modules ./node_modules +COPY --from=build /workspace/apps/ingest-api ./apps/ingest-api +COPY --from=build /workspace/packages/contracts ./packages/contracts + +EXPOSE 3001 +CMD ["yarn", "workspace", "ingest-api", "start"] diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..fae5b6e --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,25 @@ +# syntax=docker/dockerfile:1.7 + +# Build from the monorepo root context so this image can use: +# - apps/web +# - packages/contracts +FROM node:20-alpine AS base +WORKDIR /workspace +RUN corepack enable + +FROM base AS deps +COPY package.json yarn.lock .yarnrc.yml ./ +COPY .yarn/ ./.yarn/ +COPY apps/web/package.json ./apps/web/package.json +COPY packages/contracts/package.json ./packages/contracts/package.json +RUN YARN_NODE_LINKER=node-modules yarn install --immutable + +FROM deps AS build +COPY apps/web ./apps/web +COPY packages/contracts ./packages/contracts +RUN yarn workspace web build + +FROM nginx:1.27-alpine AS runtime +COPY --from=build /workspace/apps/web/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"]