From e31347499dfd03031997a7d5396ff5ac484b6382 Mon Sep 17 00:00:00 2001 From: Aaron Bockelie Date: Sat, 19 Jul 2025 21:15:59 -0500 Subject: [PATCH] Modernize multi-arch Docker build for monorepo structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update Dockerfile for pnpm monorepo with proper workspace handling - Add smart build script with clean output and logging - Fix user permissions and directory structure for MCP server - Update GitHub Actions workflow for modern buildx and pnpm - Add proper entrypoint supporting both local-mcp-server and configure-mcp-server - Tested and verified working with production Glean instance - Supports both development (npm) and production (Docker) deployment 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .dockerignore | 137 +++++++++++++++++++ .github/workflows/docker-build.yml | 112 ++++++++++++++++ Dockerfile | 81 ++++++++++++ docker-entrypoint.sh | 67 ++++++++++ scripts/build-local.sh | 206 +++++++++++++++++++++++++++++ 5 files changed, 603 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-build.yml create mode 100644 Dockerfile create mode 100644 docker-entrypoint.sh create mode 100755 scripts/build-local.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..67089ee3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,137 @@ +# Node.js +node_modules/ +**/node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Build outputs (except what we need) +**/build/test/ +**/build/**/test/ +**/build/src/ +coverage/ +*.tgz +*.tar.gz + +# Development files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Git +.git/ +.gitignore + +# Documentation +docs/ +*.md +!README.md + +# Test files +**/*.test.* +**/*.spec.* +**/test/ +**/tests/ + +# Logs +logs/ +*.log + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Dependency directories that might exist in subdirs +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# rollup.js default build output +dist/ + +# Uncomment the public line in if your project uses Gatsby +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Docker files (avoid recursive copies) +Dockerfile* +docker-compose* +.dockerignore + +# CI/CD +.github/ +.gitlab-ci.yml +.travis.yml +.circleci/ + +# Package manager +package-lock.json +yarn.lock +# Keep pnpm-lock.yaml as it's needed for consistent builds + +# Misc +*.orig \ No newline at end of file diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 00000000..39a70b52 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,112 @@ +name: Docker Build and Deploy + +on: + push: + branches: + - main + tags: + - 'v*' + pull_request: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.6.2 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run tests + run: pnpm test + + - name: Run linting + run: pnpm lint + + build-and-deploy: + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + type=sha,format=long + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + DOCKER_HASH=${{ github.sha }} + + cleanup: + name: Cleanup old packages + needs: build-and-deploy + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + permissions: + contents: read + packages: write + + steps: + - name: Delete old package versions + uses: actions/delete-package-versions@v5 + with: + package-name: ${{ github.event.repository.name }} + package-type: container + min-versions-to-keep: 10 + delete-only-untagged-versions: true \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..8a3aa024 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,81 @@ +# syntax=docker/dockerfile:1.4 + +# Build stage - multi-architecture support +FROM node:20-bullseye AS builder + +# Add metadata +LABEL org.opencontainers.image.source="https://github.com/gleanwork/mcp-server" +LABEL org.opencontainers.image.description="Glean MCP Server - Multi-architecture build" +LABEL org.opencontainers.image.licenses="MIT" + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + git \ + build-essential \ + python3 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install pnpm globally +RUN npm install -g pnpm@10.6.2 + +# Copy workspace configuration and root package files +COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ + +# Copy all package.json files to enable proper dependency resolution +COPY packages/ ./packages/ + +# Install dependencies using pnpm (includes workspace packages) +# Use --no-frozen-lockfile in case lockfile is out of sync +RUN pnpm install --no-frozen-lockfile + +# Copy source code +COPY . . + +# Build all packages +RUN pnpm run build + +# Production stage +FROM node:20-bullseye-slim + +WORKDIR /app + +# Set docker hash as environment variable +ARG DOCKER_HASH=unknown +ENV DOCKER_HASH=$DOCKER_HASH + +# Install pnpm in production stage too +RUN npm install -g pnpm@10.6.2 + +# Copy workspace config and package files +COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ + +# Copy built packages from builder stage (includes package.json files) +COPY --from=builder /app/packages ./packages + +# Install only production dependencies (skip prepare scripts) +RUN pnpm install --no-frozen-lockfile --prod --ignore-scripts && \ + pnpm store prune + +# Make the main server executable +RUN chmod +x packages/local-mcp-server/build/index.js + +# Create non-root user and required directories +RUN groupadd -r mcp && useradd -r -g mcp -m mcp && \ + mkdir -p /app/logs && \ + mkdir -p /home/mcp/.local/state/glean && \ + mkdir -p /home/mcp/.cache && \ + chown -R mcp:mcp /app && \ + chown -R mcp:mcp /home/mcp + +# Copy and set up entrypoint +COPY docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +# Switch to non-root user +USER mcp + +# Default command runs the local MCP server +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["packages/local-mcp-server/build/index.js"] \ No newline at end of file diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 00000000..8ac472a8 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,67 @@ +#!/bin/bash +set -e + +# Function to log with timestamp +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2 +} + +# Handle signals gracefully +cleanup() { + log "Received termination signal, shutting down gracefully..." + if [ ! -z "$child_pid" ]; then + kill -TERM "$child_pid" 2>/dev/null || true + wait "$child_pid" + fi + exit 0 +} + +trap cleanup SIGTERM SIGINT + +log "Starting Glean MCP Server (Docker Hash: ${DOCKER_HASH:-unknown})" + +# Set up environment +export NODE_ENV=${NODE_ENV:-production} + +# Change to app directory +cd /app + +# Default to local-mcp-server if no arguments provided +if [ $# -eq 0 ]; then + set -- "packages/local-mcp-server/build/index.js" +fi + +# Handle different commands +case "$1" in + packages/local-mcp-server/build/index.js|local-mcp-server) + log "Starting local MCP server..." + exec node packages/local-mcp-server/build/index.js "${@:2}" + ;; + packages/configure-mcp-server/build/index.js|configure-mcp-server) + log "Starting configure MCP server..." + exec node packages/configure-mcp-server/build/index.js "${@:2}" + ;; + node) + # Allow running node directly + exec "$@" + ;; + bash|sh) + # Allow shell access for debugging + exec "$@" + ;; + *) + # Try to execute as a node script + if [ -f "$1" ]; then + log "Executing custom script: $1" + exec node "$@" + else + log "Unknown command: $1" + log "Available commands:" + log " - local-mcp-server (default)" + log " - configure-mcp-server" + log " - node