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,