chore(devops): add Dockerfiles, docker-compose, CI workflow, docker-prep script [C1-C5]
- backend/Dockerfile: multi-stage Node.js build (install → build → runtime) - web/Dockerfile: multi-stage Next.js standalone build - docker-compose.yml: backend (4016) + web (3000) with health check - scripts/docker-prep.sh: pack @bytelyst/* tarballs + rewrite file: refs (--restore to undo) - .github/workflows/ci.yml: backend (typecheck+test+build), web (typecheck+test+build), mobile (typecheck)
This commit is contained in:
parent
a3267e4b1b
commit
a71747e3fb
64
.github/workflows/ci.yml
vendored
Normal file
64
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
backend:
|
||||||
|
name: Backend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: backend
|
||||||
|
env:
|
||||||
|
NODE_ENV: test
|
||||||
|
JWT_SECRET: ci-test-secret
|
||||||
|
DB_PROVIDER: memory
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: backend/package-lock.json
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run typecheck
|
||||||
|
- run: npm test
|
||||||
|
- run: npm run build
|
||||||
|
|
||||||
|
web:
|
||||||
|
name: Web
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: web
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: web/package-lock.json
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run typecheck
|
||||||
|
- run: npm test
|
||||||
|
- run: npm run build
|
||||||
|
|
||||||
|
mobile:
|
||||||
|
name: Mobile Typecheck
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: mobile
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: mobile/package-lock.json
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run typecheck
|
||||||
25
backend/Dockerfile
Normal file
25
backend/Dockerfile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# ── Stage 1: Install ──────────────────────────────────
|
||||||
|
FROM node:20-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci --production=false
|
||||||
|
|
||||||
|
# ── Stage 2: Build ────────────────────────────────────
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ── Stage 3: Runtime ──────────────────────────────────
|
||||||
|
FROM node:20-alpine AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
COPY --from=build /app/dist ./dist
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY package.json ./
|
||||||
|
|
||||||
|
EXPOSE 4016
|
||||||
|
|
||||||
|
CMD ["node", "dist/server.js"]
|
||||||
42
docker-compose.yml
Normal file
42
docker-compose.yml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "4016:4016"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=4016
|
||||||
|
- HOST=0.0.0.0
|
||||||
|
- JWT_SECRET=${JWT_SECRET:-dev-secret-change-me}
|
||||||
|
- COSMOS_ENDPOINT=${COSMOS_ENDPOINT:-}
|
||||||
|
- COSMOS_KEY=${COSMOS_KEY:-}
|
||||||
|
- COSMOS_DATABASE=${COSMOS_DATABASE:-bytelyst}
|
||||||
|
- DB_PROVIDER=${DB_PROVIDER:-memory}
|
||||||
|
- CORS_ORIGIN=${CORS_ORIGIN:-http://localhost:3000}
|
||||||
|
- PLATFORM_SERVICE_URL=${PLATFORM_SERVICE_URL:-http://localhost:4003}
|
||||||
|
- EXTRACTION_SERVICE_URL=${EXTRACTION_SERVICE_URL:-http://localhost:4005}
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:4016/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ./web
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- NEXT_PUBLIC_BACKEND_URL=http://backend:4016
|
||||||
|
- NEXT_PUBLIC_PLATFORM_URL=${PLATFORM_SERVICE_URL:-http://localhost:4003}
|
||||||
|
depends_on:
|
||||||
|
backend:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
85
scripts/docker-prep.sh
Executable file
85
scripts/docker-prep.sh
Executable file
@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# docker-prep.sh — Pack @bytelyst/* packages into tarballs for Docker builds
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/docker-prep.sh # pack tarballs + rewrite package.json
|
||||||
|
# ./scripts/docker-prep.sh --restore # undo changes
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
COMMON_PLAT="${ROOT_DIR}/../learning_ai_common_plat"
|
||||||
|
TARBALL_DIR="${ROOT_DIR}/.tarballs"
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--restore" ]]; then
|
||||||
|
echo "Restoring package.json files..."
|
||||||
|
for pkg_json in backend/package.json web/package.json; do
|
||||||
|
backup="${ROOT_DIR}/${pkg_json}.bak"
|
||||||
|
if [[ -f "$backup" ]]; then
|
||||||
|
mv "$backup" "${ROOT_DIR}/${pkg_json}"
|
||||||
|
echo " Restored ${pkg_json}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
rm -rf "$TARBALL_DIR"
|
||||||
|
echo "Done."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$COMMON_PLAT/packages" ]]; then
|
||||||
|
echo "ERROR: Cannot find $COMMON_PLAT/packages — run from the correct directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building @bytelyst/* packages..."
|
||||||
|
(cd "$COMMON_PLAT" && pnpm build)
|
||||||
|
|
||||||
|
echo "Packing tarballs..."
|
||||||
|
rm -rf "$TARBALL_DIR"
|
||||||
|
mkdir -p "$TARBALL_DIR"
|
||||||
|
|
||||||
|
for pkg_dir in "$COMMON_PLAT"/packages/*/; do
|
||||||
|
pkg_name=$(basename "$pkg_dir")
|
||||||
|
if [[ -f "$pkg_dir/package.json" ]]; then
|
||||||
|
(cd "$pkg_dir" && npm pack --pack-destination "$TARBALL_DIR" 2>/dev/null) || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Tarballs created:"
|
||||||
|
ls -la "$TARBALL_DIR"/*.tgz 2>/dev/null || echo " (none)"
|
||||||
|
|
||||||
|
echo "Rewriting package.json file: refs..."
|
||||||
|
for pkg_json in backend/package.json web/package.json; do
|
||||||
|
full_path="${ROOT_DIR}/${pkg_json}"
|
||||||
|
if [[ ! -f "$full_path" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$full_path" "${full_path}.bak"
|
||||||
|
|
||||||
|
# Replace file:../../learning_ai_common_plat/packages/* refs with tarball paths
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const pkg = JSON.parse(fs.readFileSync('$full_path', 'utf8'));
|
||||||
|
const tarballDir = '$TARBALL_DIR';
|
||||||
|
const tarballs = fs.readdirSync(tarballDir).filter(f => f.endsWith('.tgz'));
|
||||||
|
|
||||||
|
for (const depType of ['dependencies', 'devDependencies']) {
|
||||||
|
if (!pkg[depType]) continue;
|
||||||
|
for (const [name, version] of Object.entries(pkg[depType])) {
|
||||||
|
if (typeof version === 'string' && version.startsWith('file:')) {
|
||||||
|
const shortName = name.replace('@bytelyst/', 'bytelyst-');
|
||||||
|
const tarball = tarballs.find(t => t.startsWith(shortName));
|
||||||
|
if (tarball) {
|
||||||
|
pkg[depType][name] = 'file:' + path.join(tarballDir, tarball);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync('$full_path', JSON.stringify(pkg, null, 2) + '\n');
|
||||||
|
"
|
||||||
|
echo " Rewrote ${pkg_json}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Done. Run 'docker build' now. Use --restore to undo."
|
||||||
30
web/Dockerfile
Normal file
30
web/Dockerfile
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# ── Stage 1: Install ──────────────────────────────────
|
||||||
|
FROM node:20-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# ── Stage 2: Build ────────────────────────────────────
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Dummy env vars for Next.js build-time page data collection
|
||||||
|
ENV NEXT_PUBLIC_BACKEND_URL=http://localhost:4016
|
||||||
|
ENV NEXT_PUBLIC_PLATFORM_URL=http://localhost:4003
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ── Stage 3: Runtime ──────────────────────────────────
|
||||||
|
FROM node:20-alpine AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
COPY --from=build /app/.next/standalone ./
|
||||||
|
COPY --from=build /app/.next/static ./.next/static
|
||||||
|
COPY --from=build /app/public ./public
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
Loading…
Reference in New Issue
Block a user