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
|
### Docker
|
||||||
|
|
||||||
|
#### Using Pre-built Image from GHCR
|
||||||
|
|
||||||
```bash
|
```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 .
|
docker build -t pastel-ui .
|
||||||
|
|
||||||
# Run
|
# Run locally built image
|
||||||
docker run -p 3000:3000 -e NEXT_PUBLIC_API_URL=https://api.pastel.com pastel-ui
|
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
|
### Static Export
|
||||||
|
|
||||||
```bash
|
```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 = {
|
const nextConfig: NextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
|
||||||
|
// Enable standalone output for Docker
|
||||||
|
output: 'standalone',
|
||||||
|
|
||||||
// React Compiler disabled for now (requires babel-plugin-react-compiler)
|
// React Compiler disabled for now (requires babel-plugin-react-compiler)
|
||||||
// experimental: {
|
// experimental: {
|
||||||
// reactCompiler: true,
|
// reactCompiler: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user