Skip to content

Build Pipeline

Optimize Docker builds and artifact creation.

Docker Build Basics

Dockerfile Best Practices

# Use specific version tags
FROM python:3.12-slim AS base

# Set environment
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

# Create non-root user
RUN useradd -m -s /bin/bash app

WORKDIR /app

# Install dependencies first (better caching)
COPY requirements.txt .
RUN pip install -r requirements.txt

# Copy application code last
COPY --chown=app:app . .

# Switch to non-root user
USER app

# Run application
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0"]

Multi-Stage Builds

# Build stage
FROM python:3.12-slim AS builder

WORKDIR /app
COPY requirements.txt .

# Build wheels
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

# Production stage
FROM python:3.12-slim AS production

# Copy only wheels
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/*

COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]

Frontend Multi-Stage Build

# Build stage
FROM node:20-alpine AS builder

WORKDIR /app

COPY package.json bun.lock ./
RUN bun install --frozen-lockfile

COPY . .
RUN bun run build

# Production stage
FROM node:20-alpine AS production

WORKDIR /app

# Only copy built files and production dependencies
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

ENV NODE_ENV=production
EXPOSE 3000

CMD ["node", "server.js"]

GitHub Actions Build

Basic Docker Build

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

With Layer Caching

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: |
      ghcr.io/${{ github.repository }}:${{ github.sha }}
      ghcr.io/${{ github.repository }}:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

Multi-Platform Builds

- name: Set up QEMU
  uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3

- name: Build multi-platform
  uses: docker/build-push-action@v5
  with:
    context: .
    platforms: linux/amd64,linux/arm64
    push: true
    tags: ghcr.io/${{ github.repository }}:latest

Tagging Strategy

Semantic Versioning

- name: Extract version
  id: version
  run: |
    if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
      VERSION=${GITHUB_REF#refs/tags/v}
      echo "version=$VERSION" >> $GITHUB_OUTPUT
      echo "is_release=true" >> $GITHUB_OUTPUT
    else
      echo "version=${{ github.sha }}" >> $GITHUB_OUTPUT
      echo "is_release=false" >> $GITHUB_OUTPUT
    fi

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: |
      ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}
      ${{ steps.version.outputs.is_release == 'true' && format('ghcr.io/{0}:latest', github.repository) || '' }}

Auto-Tagging with Metadata

- name: Docker metadata
  id: meta
  uses: docker/metadata-action@v5
  with:
    images: ghcr.io/${{ github.repository }}
    tags: |
      type=ref,event=branch
      type=ref,event=pr
      type=semver,pattern={{version}}
      type=semver,pattern={{major}}.{{minor}}
      type=sha

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    labels: ${{ steps.meta.outputs.labels }}

Build Optimization

Dependency Caching

# Python
- name: Cache pip packages
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}

# Node.js with Bun
- name: Cache bun store
  uses: actions/cache@v4
  with:
    path: ~/.bun/install/cache
    key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}

Docker Build Cache

# Registry cache (recommended for teams)
- name: Build with registry cache
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: myapp:latest
    cache-from: type=registry,ref=myapp:buildcache
    cache-to: type=registry,ref=myapp:buildcache,mode=max

# GitHub Actions cache (simple setup)
- name: Build with GHA cache
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: myapp:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

Build Arguments and Secrets

- name: Build with args
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: myapp:latest
    build-args: |
      NODE_ENV=production
      API_URL=${{ vars.API_URL }}
    secrets: |
      npm_token=${{ secrets.NPM_TOKEN }}
# In Dockerfile
ARG NODE_ENV=development
ENV NODE_ENV=$NODE_ENV

# Use secrets securely (not stored in image)
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) bun install

Security Scanning

Trivy Scanner

- name: Build image
  uses: docker/build-push-action@v5
  with:
    context: .
    load: true  # Load to local Docker
    tags: myapp:scan

- name: Scan with Trivy
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:scan
    format: 'sarif'
    output: 'trivy-results.sarif'
    severity: 'CRITICAL,HIGH'

- name: Upload scan results
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: 'trivy-results.sarif'

Snyk Container Scan

- name: Snyk Container scan
  uses: snyk/actions/docker@master
  env:
    SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
  with:
    image: myapp:latest
    args: --severity-threshold=high

Artifact Management

Upload Build Artifacts

- name: Build application
  run: |
    bun run build
    tar -czf build.tar.gz dist/

- name: Upload artifact
  uses: actions/upload-artifact@v4
  with:
    name: build-${{ github.sha }}
    path: build.tar.gz
    retention-days: 7

Download in Deploy Job

deploy:
  needs: build
  runs-on: ubuntu-latest
  steps:
    - name: Download artifact
      uses: actions/download-artifact@v4
      with:
        name: build-${{ github.sha }}

    - name: Deploy
      run: |
        tar -xzf build.tar.gz
        ./deploy.sh dist/

Complete Build Workflow

name: Build and Push

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      security-events: write

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to 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
        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=sha

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          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

      - name: Scan for vulnerabilities
        if: github.event_name != 'pull_request'
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          format: 'sarif'
          output: 'trivy-results.sarif'

      - name: Upload scan results
        if: github.event_name != 'pull_request'
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: 'trivy-results.sarif'

Monorepo Builds

Build Changed Services Only

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      api: ${{ steps.changes.outputs.api }}
      web: ${{ steps.changes.outputs.web }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v2
        id: changes
        with:
          filters: |
            api:
              - 'services/api/**'
            web:
              - 'apps/web/**'

  build-api:
    needs: detect-changes
    if: needs.detect-changes.outputs.api == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t api services/api

  build-web:
    needs: detect-changes
    if: needs.detect-changes.outputs.web == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t web apps/web

Best Practices Summary

Practice Benefit
Multi-stage builds Smaller images
Layer caching Faster builds
Non-root user Security
Pin versions Reproducibility
Scan images Security
Use .dockerignore Faster builds
Cache dependencies Speed
Tag semantically Traceability