feat: add Docker support with GitHub Actions CI/CD
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 <noreply@anthropic.com>
This commit is contained in:
39
.dockerignore
Normal file
39
.dockerignore
Normal file
@@ -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
|
||||
72
.github/workflows/docker.yml
vendored
Normal file
72
.github/workflows/docker.yml
vendored
Normal file
@@ -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
|
||||
63
Dockerfile
Normal file
63
Dockerfile
Normal file
@@ -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"]
|
||||
47
README.md
47
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
|
||||
|
||||
43
docker-compose.yml
Normal file
43
docker-compose.yml
Normal file
@@ -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
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user