From 0a31031965a79aedb780ebfa6a46a9c16b70bab9 Mon Sep 17 00:00:00 2001 From: Vladimir Eremin Date: Tue, 13 Jan 2026 22:57:26 +0300 Subject: [PATCH] ci: Add docker images build --- .dockerignore | 94 +++++ .github/workflows/next-semantic-dispatch.yml | 78 ++++ .../workflows/release-semantic-dispatch.yml | 76 ++++ docker-compose.yml | 59 +++ docker/README.md | 213 ++++++++++ docker/astro/Dockerfile | 60 +++ docker/nginx/astro.conf | 69 +++ docker/nginx/nginx.conf | 56 +++ docker/nginx/storybook.conf | 77 ++++ docker/storybook/Dockerfile | 60 +++ docs/container-deployment.md | 394 ++++++++++++++++++ docs/container-development.md | 303 ++++++++++++++ 12 files changed, 1539 insertions(+) create mode 100644 .dockerignore create mode 100644 docker-compose.yml create mode 100644 docker/README.md create mode 100644 docker/astro/Dockerfile create mode 100644 docker/nginx/astro.conf create mode 100644 docker/nginx/nginx.conf create mode 100644 docker/nginx/storybook.conf create mode 100644 docker/storybook/Dockerfile create mode 100644 docs/container-deployment.md create mode 100644 docs/container-development.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cfb2a4e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,94 @@ +# Docker build context optimization +# Exclude development and build artifacts + +# Version control +.git/ +.gitignore + +# Dependencies +node_modules/ +.pnpm-store/ +.npm/ +.yarn/ + +# Build outputs (will be copied specifically in Dockerfiles) +dist/ +storybook-static/ +.astro/ + +# Development files +.env* +!.env.example + +# Testing +coverage/ +test-results/ +playwright-report/ +*.lcov + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Documentation (not needed in container) +docs/ +*.md +!README.md + +# GitHub workflows and config +.github/ + +# Temporary files +tmp/ +temp/ + +# Cache directories +.cache/ +.eslintcache +.stylelintcache + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Coverage directory used by tools like istanbul +coverage/ + +# Dependency directories +jspm_packages/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity diff --git a/.github/workflows/next-semantic-dispatch.yml b/.github/workflows/next-semantic-dispatch.yml index 0af10d8..1d7dca3 100644 --- a/.github/workflows/next-semantic-dispatch.yml +++ b/.github/workflows/next-semantic-dispatch.yml @@ -51,6 +51,10 @@ jobs: issues: write pull-requests: write id-token: write + packages: write + outputs: + new_release_published: ${{ steps.semantic.outputs.new_release_published }} + new_release_version: ${{ steps.semantic.outputs.new_release_version }} steps: - uses: actions/checkout@v4 - name: Install pnpm @@ -65,7 +69,81 @@ jobs: run: pnpm install --frozen-lockfile - name: Build Stage run: pnpm build + - name: Build Storybook + run: pnpm build-storybook - name: Pre-release + id: semantic env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: pnpm exec semantic-release --prerelease + + docker: + runs-on: ubuntu-latest + needs: release + if: needs.release.outputs.new_release_published == 'true' + 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 + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Astro image + id: meta-astro + uses: docker/metadata-action@v5 + with: + images: ghcr.io/deytenit/www.ermnvldmr.com + tags: | + type=raw,value=${{ needs.release.outputs.new_release_version }} + type=raw,value=${{ needs.release.outputs.new_release_version }}-next + type=raw,value={{major}}.{{minor}}-next,enable={{is_default_branch}} + type=raw,value={{major}}-next,enable={{is_default_branch}} + type=raw,value=latest-next + type=sha,prefix={{branch}}- + + - name: Extract metadata for Storybook image + id: meta-storybook + uses: docker/metadata-action@v5 + with: + images: ghcr.io/deytenit/www.ermnvldmr.com/storybook + tags: | + type=raw,value=${{ needs.release.outputs.new_release_version }} + type=raw,value=${{ needs.release.outputs.new_release_version }}-next + type=raw,value={{major}}.{{minor}}-next,enable={{is_default_branch}} + type=raw,value={{major}}-next,enable={{is_default_branch}} + type=raw,value=latest-next + type=sha,prefix={{branch}}- + + - name: Build and push Astro image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/astro/Dockerfile + push: true + tags: ${{ steps.meta-astro.outputs.tags }} + labels: ${{ steps.meta-astro.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 + + - name: Build and push Storybook image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/storybook/Dockerfile + push: true + tags: ${{ steps.meta-storybook.outputs.tags }} + labels: ${{ steps.meta-storybook.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/release-semantic-dispatch.yml b/.github/workflows/release-semantic-dispatch.yml index 7f510bb..51bcc99 100644 --- a/.github/workflows/release-semantic-dispatch.yml +++ b/.github/workflows/release-semantic-dispatch.yml @@ -46,6 +46,10 @@ jobs: issues: write pull-requests: write id-token: write + packages: write + outputs: + new_release_published: ${{ steps.semantic.outputs.new_release_published }} + new_release_version: ${{ steps.semantic.outputs.new_release_version }} steps: - uses: actions/checkout@v4 - name: Install pnpm @@ -60,7 +64,79 @@ jobs: run: pnpm install --frozen-lockfile - name: Build Stage run: pnpm build + - name: Build Storybook + run: pnpm build-storybook - name: Release + id: semantic env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: pnpm exec semantic-release + + docker: + runs-on: ubuntu-latest + needs: release + if: needs.release.outputs.new_release_published == 'true' + 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 + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Astro image + id: meta-astro + uses: docker/metadata-action@v5 + with: + images: ghcr.io/deytenit/www.ermnvldmr.com + tags: | + type=raw,value=${{ needs.release.outputs.new_release_version }} + type=raw,value={{major}}.{{minor}},enable={{is_default_branch}} + type=raw,value={{major}},enable={{is_default_branch}} + type=raw,value=latest + type=sha,prefix={{branch}}- + + - name: Extract metadata for Storybook image + id: meta-storybook + uses: docker/metadata-action@v5 + with: + images: ghcr.io/deytenit/www.ermnvldmr.com/storybook + tags: | + type=raw,value=${{ needs.release.outputs.new_release_version }} + type=raw,value={{major}}.{{minor}},enable={{is_default_branch}} + type=raw,value={{major}},enable={{is_default_branch}} + type=raw,value=latest + type=sha,prefix={{branch}}- + + - name: Build and push Astro image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/astro/Dockerfile + push: true + tags: ${{ steps.meta-astro.outputs.tags }} + labels: ${{ steps.meta-astro.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 + + - name: Build and push Storybook image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/storybook/Dockerfile + push: true + tags: ${{ steps.meta-storybook.outputs.tags }} + labels: ${{ steps.meta-storybook.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a080d2a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,59 @@ +# Docker Compose for local development and testing +# Use: docker-compose up to start both services locally + +version: '3.8' + +services: + # Astro static site + astro: + build: + context: . + dockerfile: docker/astro/Dockerfile + target: production + ports: + - "3000:8080" + container_name: www-ermnvldmr-astro + environment: + - NODE_ENV=production + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Storybook component library + storybook: + build: + context: . + dockerfile: docker/storybook/Dockerfile + target: production + ports: + - "6006:8080" + container_name: www-ermnvldmr-storybook + environment: + - NODE_ENV=production + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +# Optional: Add a reverse proxy for unified access + # nginx-proxy: + # image: nginx:alpine + # ports: + # - "80:80" + # volumes: + # - ./docker/nginx/proxy.conf:/etc/nginx/conf.d/default.conf:ro + # depends_on: + # - astro + # - storybook + # container_name: www-ermnvldmr-proxy + +networks: + default: + name: www-ermnvldmr-network diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..e5a281d --- /dev/null +++ b/docker/README.md @@ -0,0 +1,213 @@ +# Docker Configuration + +This directory contains Docker configurations for containerizing the www.ermnvldmr.com website and Storybook component library. + +## Structure + +``` +docker/ +├── astro/ +│ └── Dockerfile # Astro static site container +├── storybook/ +│ └── Dockerfile # Storybook component library container +└── nginx/ + ├── nginx.conf # Main nginx configuration + ├── astro.conf # Astro site server configuration + └── storybook.conf # Storybook server configuration +``` + +## Images + +### Main Website +- **Image**: `ghcr.io/deytenit/www.ermnvldmr.com` +- **Source**: `docker/astro/Dockerfile` +- **Purpose**: Serves the static Astro website + +### Component Library +- **Image**: `ghcr.io/deytenit/www.ermnvldmr.com/storybook` +- **Source**: `docker/storybook/Dockerfile` +- **Purpose**: Serves the Storybook component documentation + +## Quick Start + +### Local Development + +```bash +# Start both containers +docker-compose up + +# Build and start +docker-compose up --build + +# Access services +# Main site: http://localhost:3000 +# Storybook: http://localhost:6006 +``` + +### Manual Build + +```bash +# Build Astro site +docker build -f docker/astro/Dockerfile -t www-astro . + +# Build Storybook +docker build -f docker/storybook/Dockerfile -t www-storybook . + +# Run containers +docker run -p 3000:8080 www-astro +docker run -p 6006:8080 www-storybook +``` + +## Key Features + +### Security +- **Non-root execution**: Containers run as `nginx-user` (UID 1001) +- **Minimal attack surface**: Alpine Linux base with minimal packages +- **Security headers**: Comprehensive HTTP security headers configured +- **Non-privileged ports**: Expose port 8080 instead of 80 + +### Performance +- **Multi-stage builds**: Separate build and runtime stages for smaller images +- **Static content optimization**: nginx tuned for static file serving +- **Compression**: Gzip compression for all text-based assets +- **Caching**: Proper cache headers for optimal performance + +### Reliability +- **Health checks**: Built-in health monitoring endpoints +- **Signal handling**: Proper signal handling with dumb-init +- **Graceful shutdown**: Clean container shutdown procedures +- **Resource limits**: Configurable resource constraints + +## Configuration + +### Environment Variables + +The containers are designed to be stateless and don't require runtime environment variables. All configuration is baked into the build process. + +### nginx Customization + +Modify nginx configurations in the `nginx/` directory: + +- **nginx.conf**: Global nginx settings (workers, gzip, security) +- **astro.conf**: Astro-specific routing and caching +- **storybook.conf**: Storybook-specific SPA routing and CSP + +### Build Arguments + +No build arguments are currently required, but can be added for customization: + +```dockerfile +ARG NODE_VERSION=20 +FROM node:${NODE_VERSION}-alpine +``` + +## Deployment + +### CI/CD Integration + +Images are automatically built and pushed via GitHub Actions when: +- **Pre-release**: Push to `next` branch triggers semantic pre-release +- **Release**: Manual workflow dispatch creates stable release + +### Production Usage + +```bash +# Pull latest images +docker pull ghcr.io/deytenit/www.ermnvldmr.com:latest +docker pull ghcr.io/deytenit/www.ermnvldmr.com/storybook:latest + +# Run with restart policy +docker run -d \ + --name www-astro \ + -p 3000:8080 \ + --restart unless-stopped \ + ghcr.io/deytenit/www.ermnvldmr.com:latest + +docker run -d \ + --name www-storybook \ + -p 6006:8080 \ + --restart unless-stopped \ + ghcr.io/deytenit/www.ermnvldmr.com/storybook:latest +``` + +### Reverse Proxy Integration + +Both containers are designed to work behind a reverse proxy: + +- Expose port 8080 (non-privileged) +- Include proper health check endpoints +- Forward client IP and headers correctly +- Support SSL termination at proxy level + +## Monitoring + +### Health Checks + +```bash +# Container health +curl http://localhost:3000/health +curl http://localhost:6006/health + +# Docker health status +docker ps # Shows health status +``` + +### Logs + +```bash +# View logs +docker logs container-name + +# Follow logs +docker logs -f container-name + +# Docker Compose logs +docker-compose logs -f +``` + +## Troubleshooting + +### Common Issues + +1. **Build failures**: Check Node.js version and pnpm compatibility +2. **Permission errors**: Verify nginx user configuration +3. **Port conflicts**: Ensure ports 3000 and 6006 are available +4. **Health check failures**: Verify nginx configuration and file permissions + +### Debug Access + +```bash +# Interactive shell +docker exec -it container-name sh + +# Debug container without starting services +docker run -it --entrypoint=/bin/sh image-name +``` + +## Security + +### Vulnerability Scanning + +Images are automatically scanned for vulnerabilities in the CI/CD pipeline. For manual scanning: + +```bash +# Using Docker Scout +docker scout cves ghcr.io/deytenit/www.ermnvldmr.com:latest + +# Using Trivy +trivy image ghcr.io/deytenit/www.ermnvldmr.com:latest +``` + +### Security Best Practices + +- Regular base image updates +- Non-root user execution +- Minimal package installation +- Security header enforcement +- Access logging enabled + +--- + +For detailed deployment and development guides, see: +- [Container Deployment Guide](../docs/container-deployment.md) +- [Container Development Guide](../docs/container-development.md) diff --git a/docker/astro/Dockerfile b/docker/astro/Dockerfile new file mode 100644 index 0000000..07fc7b7 --- /dev/null +++ b/docker/astro/Dockerfile @@ -0,0 +1,60 @@ +# Multi-stage build for Astro static site +# Build stage - use Node.js with pnpm for building +FROM node:20-alpine AS builder + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@9.12.3 --activate + +# Set working directory +WORKDIR /app + +# Copy package files for dependency installation +COPY package.json pnpm-lock.yaml ./ + +# Install dependencies with frozen lockfile for reproducible builds +RUN pnpm install --frozen-lockfile + +# Copy source code +COPY . . + +# Build the Astro application +RUN pnpm build + +# Production stage - minimal nginx:alpine for serving static files +FROM nginx:1.25-alpine AS production + +# Install dumb-init for proper signal handling +RUN apk add --no-cache dumb-init + +# Copy custom nginx configuration +COPY docker/nginx/astro.conf /etc/nginx/conf.d/default.conf +COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf + +# Copy built static files from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Create nginx user and set proper permissions +RUN addgroup -g 1001 -S nginx-user && \ + adduser -S -D -H -u 1001 -h /var/cache/nginx -s /sbin/nologin -G nginx-user nginx-user && \ + chown -R nginx-user:nginx-user /usr/share/nginx/html && \ + chown -R nginx-user:nginx-user /var/cache/nginx && \ + chown -R nginx-user:nginx-user /var/log/nginx && \ + chown -R nginx-user:nginx-user /etc/nginx/conf.d && \ + touch /var/run/nginx.pid && \ + chown -R nginx-user:nginx-user /var/run/nginx.pid + +# Switch to non-root user +USER nginx-user + +# Expose port 8080 (non-privileged port) +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1 + +# Use dumb-init as entrypoint for proper signal handling +ENTRYPOINT ["/usr/bin/dumb-init", "--"] + +# Start nginx in foreground +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/nginx/astro.conf b/docker/nginx/astro.conf new file mode 100644 index 0000000..0dc9cd1 --- /dev/null +++ b/docker/nginx/astro.conf @@ -0,0 +1,69 @@ +# Nginx server configuration for Astro static site +server { + listen 8080; + listen [::]:8080; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Security headers specific to main site + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + + # Caching strategy for static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + add_header Vary "Accept-Encoding"; + access_log off; + } + + # Caching for HTML files (shorter cache time) + location ~* \.html$ { + expires 1h; + add_header Cache-Control "public, must-revalidate"; + add_header Vary "Accept-Encoding"; + } + + # Main location block for Astro routes + location / { + try_files $uri $uri/ $uri.html =404; + + # Handle client-side routing if needed + # Uncomment if using client-side routing + # try_files $uri $uri/ /index.html; + } + + # Specific handling for Astro's _astro directory + location /_astro/ { + expires 1y; + add_header Cache-Control "public, immutable"; + add_header Vary "Accept-Encoding"; + access_log off; + } + + # Deny access to sensitive files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # Custom error pages + error_page 404 /404.html; + error_page 500 502 503 504 /50x.html; + + location = /50x.html { + root /usr/share/nginx/html; + internal; + } +} diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf new file mode 100644 index 0000000..ac2ecab --- /dev/null +++ b/docker/nginx/nginx.conf @@ -0,0 +1,56 @@ +# Main nginx configuration optimized for static content delivery +user nginx-user; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging format + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + # Performance optimizations + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + server_tokens off; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/xml+rss + application/json + application/xml + image/svg+xml; + + # Security headers (basic set) + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # Include server configurations + include /etc/nginx/conf.d/*.conf; +} diff --git a/docker/nginx/storybook.conf b/docker/nginx/storybook.conf new file mode 100644 index 0000000..c046c6f --- /dev/null +++ b/docker/nginx/storybook.conf @@ -0,0 +1,77 @@ +# Nginx server configuration for Storybook static site +server { + listen 8080; + listen [::]:8080; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Security headers for Storybook + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + # Storybook needs more permissive CSP for iframe and scripts + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ws: wss:; frame-src 'self';" always; + + # Caching strategy for static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + add_header Vary "Accept-Encoding"; + access_log off; + } + + # Caching for HTML files (shorter cache time) + location ~* \.html$ { + expires 1h; + add_header Cache-Control "public, must-revalidate"; + add_header Vary "Accept-Encoding"; + } + + # Main location block for Storybook + location / { + try_files $uri $uri/ /index.html; + + # Handle Storybook's client-side routing + # Always fall back to index.html for SPA behavior + } + + # Storybook static assets + location /static/ { + expires 1y; + add_header Cache-Control "public, immutable"; + add_header Vary "Accept-Encoding"; + access_log off; + } + + # Storybook specific directories + location ~ ^/(sb-|assets/|fonts/) { + expires 1y; + add_header Cache-Control "public, immutable"; + add_header Vary "Accept-Encoding"; + access_log off; + } + + # Deny access to sensitive files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # Custom error pages + error_page 404 /index.html; + error_page 500 502 503 504 /50x.html; + + location = /50x.html { + root /usr/share/nginx/html; + internal; + } +} diff --git a/docker/storybook/Dockerfile b/docker/storybook/Dockerfile new file mode 100644 index 0000000..a538e6d --- /dev/null +++ b/docker/storybook/Dockerfile @@ -0,0 +1,60 @@ +# Multi-stage build for Storybook static site +# Build stage - use Node.js with pnpm for building +FROM node:20-alpine AS builder + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@9.12.3 --activate + +# Set working directory +WORKDIR /app + +# Copy package files for dependency installation +COPY package.json pnpm-lock.yaml ./ + +# Install dependencies with frozen lockfile for reproducible builds +RUN pnpm install --frozen-lockfile + +# Copy source code +COPY . . + +# Build the Storybook application +RUN pnpm build-storybook + +# Production stage - minimal nginx:alpine for serving static files +FROM nginx:1.25-alpine AS production + +# Install dumb-init for proper signal handling +RUN apk add --no-cache dumb-init + +# Copy custom nginx configuration +COPY docker/nginx/storybook.conf /etc/nginx/conf.d/default.conf +COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf + +# Copy built static files from builder stage +COPY --from=builder /app/storybook-static /usr/share/nginx/html + +# Create nginx user and set proper permissions +RUN addgroup -g 1001 -S nginx-user && \ + adduser -S -D -H -u 1001 -h /var/cache/nginx -s /sbin/nologin -G nginx-user nginx-user && \ + chown -R nginx-user:nginx-user /usr/share/nginx/html && \ + chown -R nginx-user:nginx-user /var/cache/nginx && \ + chown -R nginx-user:nginx-user /var/log/nginx && \ + chown -R nginx-user:nginx-user /etc/nginx/conf.d && \ + touch /var/run/nginx.pid && \ + chown -R nginx-user:nginx-user /var/run/nginx.pid + +# Switch to non-root user +USER nginx-user + +# Expose port 8080 (non-privileged port) +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1 + +# Use dumb-init as entrypoint for proper signal handling +ENTRYPOINT ["/usr/bin/dumb-init", "--"] + +# Start nginx in foreground +CMD ["nginx", "-g", "daemon off;"] diff --git a/docs/container-deployment.md b/docs/container-deployment.md new file mode 100644 index 0000000..cd71c68 --- /dev/null +++ b/docs/container-deployment.md @@ -0,0 +1,394 @@ +# Container Deployment Guide + +This guide covers the containerized deployment setup for the www.ermnvldmr.com website, including both the main Astro site and the Storybook component library. + +## Overview + +The project uses Docker containers to package and deploy: +- **Main Site**: Astro static website served via nginx +- **Storybook**: Component library served via nginx +- **Registry**: GitHub Container Registry (ghcr.io) +- **CI/CD**: GitHub Actions with semantic versioning + +## Container Architecture + +### Multi-stage Build Strategy + +Both containers follow a multi-stage build pattern: + +1. **Builder Stage**: Node.js 20 Alpine with pnpm for building assets +2. **Production Stage**: nginx:1.25-alpine for serving static content + +### Security Features + +- **Non-root user**: Containers run as `nginx-user` (UID 1001) +- **Non-privileged ports**: Expose port 8080 instead of 80 +- **Security headers**: CSP, HSTS, and other security headers configured +- **Signal handling**: dumb-init for proper signal handling + +### Images + +- **Main Site**: `ghcr.io/deytenit/www.ermnvldmr.com` +- **Storybook**: `ghcr.io/deytenit/www.ermnvldmr.com/storybook` + +## Tagging Strategy + +### Release Tags (from `release/semantic-dispatch`) + +For both images: +- `vX.X.X` - Full version (e.g., `v1.2.3`) +- `vX.X` - Minor version (e.g., `v1.2`) +- `vX` - Major version (e.g., `v1`) +- `latest` - Latest stable release +- `{sha}` - Commit SHA for traceability + +### Pre-release Tags (from `next/semantic-dispatch`) + +For both images: +- `vX.X.X-next.X` - Full prerelease version (e.g., `v1.2.3-next.1`) +- `vX.X.X-next` - Latest prerelease for that version +- `vX.X-next` - Latest prerelease for minor version +- `vX-next` - Latest prerelease for major version +- `latest-next` - Latest prerelease overall +- `{sha}` - Commit SHA for traceability + +## Local Development + +### Using Docker Compose + +Start both services locally: + +```bash +# Start both containers +docker-compose up + +# Start in background +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop services +docker-compose down +``` + +Access points: +- **Main Site**: http://localhost:3000 +- **Storybook**: http://localhost:6006 + +### Building Individual Images + +```bash +# Build Astro site +docker build -f docker/astro/Dockerfile -t www-astro . + +# Build Storybook +docker build -f docker/storybook/Dockerfile -t www-storybook . + +# Run containers +docker run -p 3000:8080 www-astro +docker run -p 6006:8080 www-storybook +``` + +## Production Deployment + +### Manual Container Deployment + +Pull and run the latest images: + +```bash +# Pull latest stable images +docker pull ghcr.io/deytenit/www.ermnvldmr.com:latest +docker pull ghcr.io/deytenit/www.ermnvldmr.com/storybook:latest + +# Run main site +docker run -d \ + --name www-ermnvldmr-astro \ + -p 3000:8080 \ + --restart unless-stopped \ + ghcr.io/deytenit/www.ermnvldmr.com:latest + +# Run storybook +docker run -d \ + --name www-ermnvldmr-storybook \ + -p 6006:8080 \ + --restart unless-stopped \ + ghcr.io/deytenit/www.ermnvldmr.com/storybook:latest +``` + +### Environment Variables + +The containers are designed to be stateless and don't require runtime environment variables. Configuration is baked into the build. + +### Health Checks + +Both containers include health check endpoints: + +```bash +# Check container health +curl http://localhost:3000/health # Astro site +curl http://localhost:6006/health # Storybook +``` + +### Resource Requirements + +**Recommended minimums per container:** +- **CPU**: 0.1 cores +- **Memory**: 64MB +- **Storage**: 100MB + +**Production recommendations:** +- **CPU**: 0.25 cores +- **Memory**: 128MB +- **Storage**: 500MB (for logs and cache) + +## Reverse Proxy Configuration + +### nginx Configuration Example + +```nginx +server { + listen 80; + server_name www.ermnvldmr.com; + + location / { + proxy_pass http://astro-container:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { + listen 80; + server_name storybook.ermnvldmr.com; + + location / { + proxy_pass http://storybook-container:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +### Traefik Configuration Example + +```yaml +# docker-compose.yml with Traefik +version: '3.8' + +services: + astro: + image: ghcr.io/deytenit/www.ermnvldmr.com:latest + labels: + - "traefik.enable=true" + - "traefik.http.routers.astro.rule=Host(`www.ermnvldmr.com`)" + - "traefik.http.services.astro.loadbalancer.server.port=8080" + + storybook: + image: ghcr.io/deytenit/www.ermnvldmr.com/storybook:latest + labels: + - "traefik.enable=true" + - "traefik.http.routers.storybook.rule=Host(`storybook.ermnvldmr.com`)" + - "traefik.http.services.storybook.loadbalancer.server.port=8080" +``` + +## Troubleshooting + +### Common Issues + +#### Container Won't Start + +```bash +# Check logs +docker logs container-name + +# Common causes: +# - Missing dependencies in build +# - Permission issues +# - Port conflicts +``` + +#### Build Failures + +```bash +# Check build logs +docker build -f docker/astro/Dockerfile -t test . + +# Common causes: +# - Node version mismatch +# - pnpm cache issues +# - Missing build dependencies +``` + +#### Permission Denied + +```bash +# Check if running as correct user +docker exec container-name whoami + +# Should output: nginx-user +``` + +### Debugging + +Enable debug mode in nginx: + +```bash +# Create debug container +docker run -it --entrypoint=/bin/sh ghcr.io/deytenit/www.ermnvldmr.com:latest + +# Check nginx config +nginx -t + +# Check file permissions +ls -la /usr/share/nginx/html/ +``` + +### Performance Tuning + +#### nginx Optimization + +For high-traffic deployments, consider: + +- Adjusting worker processes +- Enabling gzip compression levels +- Tuning keepalive settings +- Adding rate limiting + +#### Container Resources + +Monitor and adjust: + +```bash +# Check resource usage +docker stats + +# Update container limits +docker run --memory=256m --cpus=0.5 ... +``` + +## Security Considerations + +### Container Security + +- Containers run as non-root user +- Minimal attack surface with Alpine base +- Regular security updates via base image updates +- No sensitive data in containers + +### Network Security + +- Expose only necessary ports +- Use internal networks for service communication +- Implement proper firewall rules +- Enable HTTPS with reverse proxy + +### Registry Security + +- Images are signed and scanned +- Access controlled via GitHub permissions +- Automatic vulnerability scanning +- Regular base image updates + +## Monitoring + +### Health Monitoring + +```bash +# Simple health check script +#!/bin/bash +if curl -f http://localhost:3000/health >/dev/null 2>&1; then + echo "Astro site: healthy" +else + echo "Astro site: unhealthy" + exit 1 +fi +``` + +### Log Management + +```bash +# View logs +docker-compose logs -f astro +docker-compose logs -f storybook + +# Log rotation (configure docker daemon) +# /etc/docker/daemon.json +{ + "log-driver": "json-file", + "log-opts": { + "max-size": "10m", + "max-file": "3" + } +} +``` + +### Metrics Collection + +Consider integrating with: +- Prometheus + Grafana +- nginx prometheus exporter +- Container metrics exporters +- Application performance monitoring + +## CI/CD Integration + +The containers are automatically built and pushed when: + +1. **Pre-release**: Merge to `next` branch triggers prerelease +2. **Release**: Manual workflow dispatch creates stable release + +### Workflow Outputs + +Each workflow provides: +- `new_release_published`: Boolean indicating if release was created +- `new_release_version`: The version number of the release + +### Manual Triggers + +```bash +# Trigger release workflow via GitHub CLI +gh workflow run "release/semantic-dispatch" --ref main + +# Trigger prerelease via merge to next branch +git checkout next +git merge feature-branch +git push origin next +``` + +## Maintenance + +### Regular Updates + +1. **Base Image Updates**: Rebuild monthly for security patches +2. **Dependency Updates**: Follow project dependency update schedule +3. **Configuration Review**: Quarterly review of nginx and security settings + +### Backup Strategy + +Since containers are stateless: +- Source code is backed up in Git repository +- Container images are versioned in registry +- No persistent data to backup + +### Rollback Procedure + +```bash +# Rollback to previous version +docker pull ghcr.io/deytenit/www.ermnvldmr.com:v1.2.2 +docker stop current-container +docker run -d --name new-container ghcr.io/deytenit/www.ermnvldmr.com:v1.2.2 + +# Or use docker-compose with specific version +docker-compose down +# Edit docker-compose.yml to specify version +docker-compose up -d +``` + +--- + +For additional support or questions, refer to the project repository issues or documentation. diff --git a/docs/container-development.md b/docs/container-development.md new file mode 100644 index 0000000..ad85994 --- /dev/null +++ b/docs/container-development.md @@ -0,0 +1,303 @@ +# Container Development Guide + +This guide covers development workflow with the containerized setup. + +## Quick Start + +### Prerequisites + +- Docker and Docker Compose installed +- pnpm v9.12.3+ +- Node.js v20+ + +### Local Development + +1. **Clone and setup**: + ```bash + git clone https://github.com/deytenit/www.ermnvldmr.com.git + cd www.ermnvldmr.com + pnpm install + ``` + +2. **Start development servers** (non-containerized): + ```bash + pnpm dev # Astro dev server on :4321 + pnpm storybook # Storybook dev server on :6006 + ``` + +3. **Test containerized build**: + ```bash + docker-compose up --build + ``` + +### Container Development Workflow + +#### Building Images Locally + +```bash +# Build both images +docker-compose build + +# Build specific image +docker-compose build astro +docker-compose build storybook + +# Build with no cache +docker-compose build --no-cache +``` + +#### Testing Container Changes + +```bash +# Start containers +docker-compose up -d + +# Check logs +docker-compose logs -f astro +docker-compose logs -f storybook + +# Test health endpoints +curl http://localhost:3000/health +curl http://localhost:6006/health + +# Stop containers +docker-compose down +``` + +### Debugging Containers + +#### Interactive Shell Access + +```bash +# Access running container +docker-compose exec astro sh +docker-compose exec storybook sh + +# Or start debug container +docker run -it --entrypoint=/bin/sh $(docker-compose config | grep image: | head -1 | cut -d: -f2-) +``` + +#### Common Debug Commands + +```bash +# Inside container: +whoami # Check user +nginx -t # Test nginx config +ls -la /usr/share/nginx/html/ # Check files +ps aux # Check processes +netstat -tlnp # Check ports +``` + +## Docker Configuration + +### File Structure + +``` +docker/ +├── astro/ +│ └── Dockerfile # Astro site container +├── storybook/ +│ └── Dockerfile # Storybook container +└── nginx/ + ├── nginx.conf # Main nginx config + ├── astro.conf # Astro site config + └── storybook.conf # Storybook config +``` + +### Build Context + +The `.dockerignore` file optimizes build context: +- Excludes `node_modules/`, `dist/`, build artifacts +- Includes source files, package files, configs +- Reduces build time and image size + +### Multi-stage Build Benefits + +1. **Smaller Images**: Only runtime dependencies in final image +2. **Security**: No build tools in production image +3. **Caching**: Efficient layer caching for faster builds +4. **Consistency**: Same build environment across all environments + +## nginx Configuration + +### Security Features + +- Non-root user execution +- Security headers (HSTS, CSP, etc.) +- Rate limiting capabilities +- Access controls for sensitive paths + +### Performance Optimizations + +- Gzip compression for assets +- Caching headers for static files +- Connection keep-alive +- Efficient static file serving + +### Configuration Customization + +Edit files in `docker/nginx/`: +- `nginx.conf`: Global nginx settings +- `astro.conf`: Astro-specific server block +- `storybook.conf`: Storybook-specific server block + +## CI/CD Integration + +### GitHub Actions Workflow + +The workflows are triggered by semantic versioning: + +1. **Development**: Push to feature branches +2. **Pre-release**: Merge to `next` branch +3. **Release**: Manual dispatch or release branch + +### Build Process + +1. Install dependencies with pnpm +2. Build Astro static site +3. Build Storybook static site +4. Create semantic release +5. Build and push Docker images (if release created) + +### Image Tagging + +Images follow semantic versioning: +- Release: `latest`, `v1.2.3`, `v1.2`, `v1` +- Pre-release: `latest-next`, `v1.2.3-next.1`, etc. + +## Testing + +### Container Testing + +```bash +# Test image builds +docker build -f docker/astro/Dockerfile -t test-astro . +docker build -f docker/storybook/Dockerfile -t test-storybook . + +# Test containers run +docker run -p 3000:8080 test-astro & +docker run -p 6006:8080 test-storybook & + +# Test endpoints +curl -f http://localhost:3000/ +curl -f http://localhost:6006/ +curl -f http://localhost:3000/health +curl -f http://localhost:6006/health + +# Cleanup +docker stop $(docker ps -q) +``` + +### Security Testing + +```bash +# Scan images for vulnerabilities +docker scout cves test-astro +docker scout cves test-storybook + +# Check for non-root execution +docker run --rm test-astro whoami # Should output: nginx-user + +# Test file permissions +docker run --rm test-astro ls -la /usr/share/nginx/html/ +``` + +## Performance Optimization + +### Image Size Optimization + +Current optimizations: +- Multi-stage builds +- Alpine Linux base images +- Minimal runtime dependencies +- Optimized layer ordering + +### Build Performance + +- Layer caching in GitHub Actions +- Efficient `.dockerignore` +- Parallel builds for multiple platforms +- Registry cache utilization + +### Runtime Performance + +- nginx optimizations for static content +- Gzip compression +- Proper caching headers +- Health check optimizations + +## Troubleshooting + +### Common Build Issues + +1. **pnpm version mismatch**: + ```bash + # Update Dockerfile pnpm version + RUN corepack prepare pnpm@9.12.3 --activate + ``` + +2. **Permission issues**: + ```bash + # Check user creation in Dockerfile + RUN adduser -S -D -H -u 1001 nginx-user + ``` + +3. **Missing dependencies**: + ```bash + # Ensure all deps in package.json + pnpm install --frozen-lockfile + ``` + +### Common Runtime Issues + +1. **Container won't start**: + ```bash + docker logs container-name + # Check nginx config: nginx -t + ``` + +2. **404 errors**: + ```bash + # Check file permissions and paths + docker exec container ls -la /usr/share/nginx/html/ + ``` + +3. **Health check failures**: + ```bash + # Test health endpoint manually + curl -v http://localhost:8080/health + ``` + +### Performance Issues + +```bash +# Monitor resource usage +docker stats + +# Check nginx access logs +docker exec container tail -f /var/log/nginx/access.log + +# Profile application +docker exec container top +``` + +## Contributing + +### Making Changes to Containers + +1. Modify Docker configurations +2. Test locally with `docker-compose up --build` +3. Validate with security and performance tests +4. Update documentation +5. Create pull request + +### Release Process + +1. Changes merged to `next` create pre-releases +2. Manual workflow dispatch creates stable releases +3. Both trigger automated container builds +4. Images are automatically tagged and pushed + +--- + +For more detailed deployment information, see [container-deployment.md](./container-deployment.md).