From 75e21646b334c02f039ba554918b9939d0aa87b0 Mon Sep 17 00:00:00 2001 From: valknarness Date: Fri, 7 Nov 2025 14:36:30 +0100 Subject: [PATCH] feat: add Docker support with GitHub Actions CI/CD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive Docker deployment with automated builds: **Docker Configuration:** - Multi-stage Dockerfile for optimized Next.js production builds - Stage 1: Install dependencies with pnpm - Stage 2: Build application with standalone output - Stage 3: Minimal runtime image with non-root user - Includes health check endpoint - Final image size optimized - .dockerignore for efficient build context - Enable standalone output in next.config.ts for Docker **GitHub Actions Workflow:** - Automated Docker image builds on push to main and tags - Multi-platform support (linux/amd64, linux/arm64) - Push to GitHub Container Registry (ghcr.io) - Smart tagging strategy: - `latest` for main branch - `vX.X.X` for semver tags - `main-SHA` for commit-specific images - Build cache optimization with GitHub Actions cache - Artifact attestation for supply chain security **Docker Compose:** - Combined stack for UI + API - Environment variable configuration - Health checks for both services - Automatic restart policies - Shared network configuration **Documentation:** - Updated README with Docker deployment instructions - Pre-built image usage from GHCR - Docker Compose setup guide - Local build instructions - Available image tags reference **Production Ready:** - Images automatically published to ghcr.io/valknarness/pastel-ui - Supports both x64 and ARM64 architectures - Health checks for container orchestration - Environment-based configuration - Non-root user for security 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .dockerignore | 39 +++++++++++++++++++ .github/workflows/docker.yml | 72 ++++++++++++++++++++++++++++++++++++ Dockerfile | 63 +++++++++++++++++++++++++++++++ README.md | 47 +++++++++++++++++++++-- docker-compose.yml | 43 +++++++++++++++++++++ next.config.ts | 3 ++ 6 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..27312a0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,39 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Build outputs +.next +out +dist +build + +# Testing +coverage +.nyc_output + +# Environment +.env +.env*.local + +# Git +.git +.gitignore + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Misc +*.log +.turbo diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..5270a07 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,72 @@ +name: Docker Build & Push + +on: + push: + branches: + - main + tags: + - 'v*.*.*' + pull_request: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in 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 (tags, labels) + 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=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + + - 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: | + NEXT_PUBLIC_API_URL=${{ vars.NEXT_PUBLIC_API_URL || 'http://localhost:3001' }} + + - name: Generate artifact attestation + if: github.event_name != 'pull_request' + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.build-and-push.outputs.digest }} + push-to-registry: true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..11bb799 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,63 @@ +# Pastel UI - Production Docker Image +# Multi-stage build for optimized Next.js 16 application + +# Stage 1: Dependencies +FROM node:20-alpine AS deps +RUN corepack enable && corepack prepare pnpm@latest --activate + +WORKDIR /app + +# Copy package files +COPY package.json pnpm-lock.yaml ./ + +# Install dependencies +RUN pnpm install --frozen-lockfile --prod=false + +# Stage 2: Builder +FROM node:20-alpine AS builder +RUN corepack enable && corepack prepare pnpm@latest --activate + +WORKDIR /app + +# Copy dependencies from deps stage +COPY --from=deps /app/node_modules ./node_modules + +# Copy source files +COPY . . + +# Set build-time environment variables +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production + +# Build the application +RUN pnpm build + +# Stage 3: Runner +FROM node:20-alpine AS runner + +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +# Create non-root user +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +# Copy built application +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +CMD ["node", "server.js"] diff --git a/README.md b/README.md index df5d174..87d02fd 100644 --- a/README.md +++ b/README.md @@ -356,14 +356,55 @@ vercel --prod ### Docker +#### Using Pre-built Image from GHCR + ```bash -# Build +# Pull the latest image +docker pull ghcr.io/valknarness/pastel-ui:latest + +# Run the container +docker run -p 3000:3000 \ + -e NEXT_PUBLIC_API_URL=http://localhost:3001 \ + ghcr.io/valknarness/pastel-ui:latest +``` + +#### Docker Compose (UI + API) + +Run both Pastel UI and Pastel API together: + +```bash +# Using docker-compose +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop +docker-compose down +``` + +#### Building Locally + +```bash +# Build the image docker build -t pastel-ui . -# Run -docker run -p 3000:3000 -e NEXT_PUBLIC_API_URL=https://api.pastel.com pastel-ui +# Run locally built image +docker run -p 3000:3000 \ + -e NEXT_PUBLIC_API_URL=http://localhost:3001 \ + pastel-ui ``` +#### Available Docker Images + +Images are automatically built and published to GitHub Container Registry: + +- `ghcr.io/valknarness/pastel-ui:latest` - Latest main branch +- `ghcr.io/valknarness/pastel-ui:v1.0.0` - Specific version +- `ghcr.io/valknarness/pastel-ui:main-abc1234` - Commit SHA + +Supported platforms: `linux/amd64`, `linux/arm64` + ### Static Export ```bash diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3a5102d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +version: '3.8' + +services: + pastel-ui: + build: + context: . + dockerfile: Dockerfile + image: ghcr.io/valknarness/pastel-ui:latest + container_name: pastel-ui + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:3001} + - NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000} + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + pastel-api: + image: ghcr.io/valknarness/pastel-api:latest + container_name: pastel-api + ports: + - "3001:3001" + environment: + - RUST_LOG=info + - PORT=3001 + - HOST=0.0.0.0 + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + +networks: + default: + name: pastel-network diff --git a/next.config.ts b/next.config.ts index a68ebaf..6026076 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,6 +3,9 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { reactStrictMode: true, + // Enable standalone output for Docker + output: 'standalone', + // React Compiler disabled for now (requires babel-plugin-react-compiler) // experimental: { // reactCompiler: true,