diff --git a/docs/build-types/docker.md b/docs/build-types/docker.md new file mode 100644 index 0000000..364a0f3 --- /dev/null +++ b/docs/build-types/docker.md @@ -0,0 +1,1097 @@ +--- +title: "Docker Container Builds" +description: "Build, tag, and deploy Docker containers using psake with multi-stage builds, registry publishing, and Docker Compose orchestration" +--- + +# Docker Container Builds + +psake can orchestrate Docker-based build workflows, providing a consistent PowerShell automation layer for containerized applications. This guide shows you how to build Docker images, use multi-stage builds, push to registries, and integrate with Docker Compose. + +## Quick Start + +Here's a basic psake build script for Docker: + +```powershell +Properties { + $ImageName = 'myapp' + $ImageTag = 'latest' + $ContainerName = 'myapp-container' +} + +Task Default -depends Build + +Task Build { + Write-Host "Building Docker image..." -ForegroundColor Green + exec { docker build -t "${ImageName}:${ImageTag}" . } +} + +Task Run -depends Build { + Write-Host "Running Docker container..." -ForegroundColor Green + exec { docker run -d --name $ContainerName -p 8080:80 "${ImageName}:${ImageTag}" } +} + +Task Stop { + Write-Host "Stopping container..." -ForegroundColor Green + exec { docker stop $ContainerName } -errorMessage "Container not running" + exec { docker rm $ContainerName } -errorMessage "Container not found" +} +``` + +Run the build: + +```powershell +Invoke-psake -buildFile .\psakefile.ps1 +``` + +## Complete Docker Build Example + +Here's a comprehensive psakefile.ps1 for Docker-based builds: + +```powershell +Properties { + $ProjectRoot = $PSScriptRoot + $DockerfilePath = Join-Path $ProjectRoot 'Dockerfile' + $DockerComposeFile = Join-Path $ProjectRoot 'docker-compose.yml' + + # Image configuration + $ImageName = 'myapp' + $ImageTag = if ($env:BUILD_NUMBER) { "1.0.$env:BUILD_NUMBER" } else { 'latest' } + $ImageFullName = "${ImageName}:${ImageTag}" + + # Registry configuration + $Registry = 'docker.io' + $RegistryUsername = $env:DOCKER_USERNAME + $RegistryToken = $env:DOCKER_TOKEN + $RegistryImage = "${Registry}/${RegistryUsername}/${ImageName}:${ImageTag}" + + # Container configuration + $ContainerName = 'myapp-container' + $ContainerPort = 8080 + $HostPort = 8080 + + # Build configuration + $BuildArgs = @{} + $Platform = 'linux/amd64' + $NoCache = $false +} + +FormatTaskName { + param($taskName) + Write-Host "Executing task: $taskName" -ForegroundColor Cyan +} + +Task Default -depends Build + +Task Verify { + Write-Host "Verifying Docker installation..." -ForegroundColor Green + + try { + $dockerVersion = docker --version + Write-Host " Docker: $dockerVersion" -ForegroundColor Gray + } + catch { + throw "Docker is not installed or not in PATH. Install from https://docker.com/" + } + + if (-not (Test-Path $DockerfilePath)) { + throw "Dockerfile not found at: $DockerfilePath" + } +} + +Task Clean { + Write-Host "Cleaning up Docker resources..." -ForegroundColor Green + + # Stop and remove container if exists + $container = docker ps -a --filter "name=$ContainerName" --format "{{.Names}}" 2>$null + if ($container -eq $ContainerName) { + Write-Host " Stopping container: $ContainerName" -ForegroundColor Gray + exec { docker stop $ContainerName } -errorMessage "Failed to stop container" + exec { docker rm $ContainerName } -errorMessage "Failed to remove container" + } + + # Remove dangling images + $danglingImages = docker images -f "dangling=true" -q 2>$null + if ($danglingImages) { + Write-Host " Removing dangling images" -ForegroundColor Gray + docker rmi $danglingImages 2>$null + } +} + +Task Build -depends Verify { + Write-Host "Building Docker image: $ImageFullName" -ForegroundColor Green + + $buildCmd = "docker build" + + # Add platform if specified + if ($Platform) { + $buildCmd += " --platform $Platform" + } + + # Add no-cache flag if specified + if ($NoCache) { + $buildCmd += " --no-cache" + } + + # Add build args + foreach ($key in $BuildArgs.Keys) { + $buildCmd += " --build-arg ${key}=$($BuildArgs[$key])" + } + + # Add tag and context + $buildCmd += " -t $ImageFullName ." + + Write-Host " Build command: $buildCmd" -ForegroundColor Gray + exec { Invoke-Expression $buildCmd } + + Write-Host "Docker image built successfully: $ImageFullName" -ForegroundColor Green +} + +Task BuildNoCache { + $script:NoCache = $true + Invoke-psake -taskList Build +} + +Task Tag -depends Build { + Write-Host "Tagging image for registry..." -ForegroundColor Green + + if ([string]::IsNullOrEmpty($RegistryUsername)) { + throw "DOCKER_USERNAME environment variable is required" + } + + exec { docker tag $ImageFullName $RegistryImage } + + Write-Host "Tagged: $RegistryImage" -ForegroundColor Green +} + +Task Inspect -depends Build { + Write-Host "Inspecting Docker image..." -ForegroundColor Green + + $imageInfo = docker inspect $ImageFullName | ConvertFrom-Json + + Write-Host " Image ID: $($imageInfo[0].Id)" -ForegroundColor Gray + Write-Host " Created: $($imageInfo[0].Created)" -ForegroundColor Gray + Write-Host " Size: $([math]::Round($imageInfo[0].Size / 1MB, 2)) MB" -ForegroundColor Gray + Write-Host " Architecture: $($imageInfo[0].Architecture)" -ForegroundColor Gray + + # List image layers + Write-Host " Layers: $($imageInfo[0].RootFS.Layers.Count)" -ForegroundColor Gray +} + +Task Scan -depends Build { + Write-Host "Scanning image for vulnerabilities..." -ForegroundColor Green + + # Check if docker scan is available (requires Docker Desktop) + try { + exec { docker scan $ImageFullName } + } + catch { + Write-Warning "Docker scan not available. Consider using Trivy or Snyk for vulnerability scanning." + } +} + +Task Run -depends Build, Clean { + Write-Host "Running Docker container: $ContainerName" -ForegroundColor Green + + exec { + docker run -d ` + --name $ContainerName ` + -p "${HostPort}:${ContainerPort}" ` + $ImageFullName + } + + Write-Host "Container started: $ContainerName" -ForegroundColor Green + Write-Host "Access application at: http://localhost:${HostPort}" -ForegroundColor Yellow +} + +Task RunInteractive -depends Build { + Write-Host "Running Docker container interactively..." -ForegroundColor Green + + exec { docker run -it --rm -p "${HostPort}:${ContainerPort}" $ImageFullName } +} + +Task Exec { + Write-Host "Executing shell in running container..." -ForegroundColor Green + + $container = docker ps --filter "name=$ContainerName" --format "{{.Names}}" 2>$null + if ($container -ne $ContainerName) { + throw "Container $ContainerName is not running. Start it first with 'Run' task." + } + + exec { docker exec -it $ContainerName /bin/sh } +} + +Task Logs { + Write-Host "Viewing container logs..." -ForegroundColor Green + + $container = docker ps -a --filter "name=$ContainerName" --format "{{.Names}}" 2>$null + if ($container -ne $ContainerName) { + throw "Container $ContainerName not found" + } + + exec { docker logs -f $ContainerName } +} + +Task Stop { + Write-Host "Stopping container: $ContainerName" -ForegroundColor Green + + $container = docker ps --filter "name=$ContainerName" --format "{{.Names}}" 2>$null + if ($container -eq $ContainerName) { + exec { docker stop $ContainerName } + Write-Host "Container stopped: $ContainerName" -ForegroundColor Green + } + else { + Write-Warning "Container $ContainerName is not running" + } +} + +Task Remove -depends Stop { + Write-Host "Removing container: $ContainerName" -ForegroundColor Green + + $container = docker ps -a --filter "name=$ContainerName" --format "{{.Names}}" 2>$null + if ($container -eq $ContainerName) { + exec { docker rm $ContainerName } + Write-Host "Container removed: $ContainerName" -ForegroundColor Green + } +} + +Task Login { + Write-Host "Logging in to Docker registry..." -ForegroundColor Green + + if ([string]::IsNullOrEmpty($RegistryUsername)) { + throw "DOCKER_USERNAME environment variable is required" + } + + if ([string]::IsNullOrEmpty($RegistryToken)) { + throw "DOCKER_TOKEN environment variable is required" + } + + # Use token authentication (recommended for CI/CD) + $env:DOCKER_TOKEN | docker login $Registry --username $RegistryUsername --password-stdin + + Write-Host "Successfully logged in to $Registry" -ForegroundColor Green +} + +Task Push -depends Tag, Login { + Write-Host "Pushing image to registry..." -ForegroundColor Green + + exec { docker push $RegistryImage } + + Write-Host "Successfully pushed: $RegistryImage" -ForegroundColor Green +} + +Task Pull { + Write-Host "Pulling image from registry..." -ForegroundColor Green + + if ([string]::IsNullOrEmpty($RegistryUsername)) { + throw "DOCKER_USERNAME environment variable is required" + } + + exec { docker pull $RegistryImage } + + Write-Host "Successfully pulled: $RegistryImage" -ForegroundColor Green +} + +Task Prune { + Write-Host "Pruning unused Docker resources..." -ForegroundColor Green + + $confirmation = Read-Host "This will remove all unused images, containers, and networks. Continue? (yes/no)" + if ($confirmation -eq 'yes') { + exec { docker system prune -a -f --volumes } + Write-Host "Docker system pruned" -ForegroundColor Green + } + else { + Write-Host "Prune cancelled" -ForegroundColor Yellow + } +} +``` + +## Multi-Stage Builds + +Multi-stage builds create smaller, more secure production images: + +```powershell +Properties { + $ImageName = 'myapp' + $ImageTag = 'latest' + $BuildStage = 'production' # Options: development, production +} + +Task BuildDevelopment { + Write-Host "Building development image..." -ForegroundColor Green + + exec { + docker build ` + --target development ` + -t "${ImageName}:dev" ` + . + } +} + +Task BuildProduction { + Write-Host "Building production image..." -ForegroundColor Green + + exec { + docker build ` + --target production ` + -t "${ImageName}:${ImageTag}" ` + . + } +} + +Task BuildAll { + Write-Host "Building all stages..." -ForegroundColor Green + + # Build development stage + Invoke-psake -taskList BuildDevelopment + + # Build production stage + Invoke-psake -taskList BuildProduction +} +``` + +Example multi-stage `Dockerfile`: + +```dockerfile +# Stage 1: Build stage +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copy dependency files +COPY package*.json ./ + +# Install all dependencies (including dev dependencies) +RUN npm ci + +# Copy source code +COPY . . + +# Run tests and build +RUN npm run test +RUN npm run build + +# Stage 2: Development stage (includes dev tools) +FROM node:18-alpine AS development + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . + +EXPOSE 3000 + +CMD ["npm", "run", "dev"] + +# Stage 3: Production stage (optimized) +FROM node:18-alpine AS production + +WORKDIR /app + +# Copy only production dependencies +COPY package*.json ./ +RUN npm ci --only=production + +# Copy built artifacts from builder +COPY --from=builder /app/dist ./dist + +# Create non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +USER nodejs + +EXPOSE 3000 + +CMD ["node", "dist/index.js"] +``` + +For .NET applications: + +```dockerfile +# Stage 1: Build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build + +WORKDIR /src + +COPY ["MyApp/MyApp.csproj", "MyApp/"] +RUN dotnet restore "MyApp/MyApp.csproj" + +COPY . . +WORKDIR "/src/MyApp" +RUN dotnet build "MyApp.csproj" -c Release -o /app/build + +# Stage 2: Publish +FROM build AS publish +RUN dotnet publish "MyApp.csproj" -c Release -o /app/publish + +# Stage 3: Runtime +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime + +WORKDIR /app + +COPY --from=publish /app/publish . + +EXPOSE 80 +EXPOSE 443 + +ENTRYPOINT ["dotnet", "MyApp.dll"] +``` + +## Pushing to Container Registries + +### Docker Hub + +```powershell +Properties { + $DockerHubUsername = $env:DOCKER_USERNAME + $DockerHubToken = $env:DOCKER_TOKEN + $Repository = 'myapp' + $Tag = 'latest' +} + +Task PushDockerHub -depends Build { + Write-Host "Pushing to Docker Hub..." -ForegroundColor Green + + if ([string]::IsNullOrEmpty($DockerHubUsername) -or [string]::IsNullOrEmpty($DockerHubToken)) { + throw "DOCKER_USERNAME and DOCKER_TOKEN environment variables are required" + } + + # Login + $env:DOCKER_TOKEN | docker login --username $DockerHubUsername --password-stdin + + # Tag image + $fullImage = "${DockerHubUsername}/${Repository}:${Tag}" + exec { docker tag "${Repository}:${Tag}" $fullImage } + + # Push + exec { docker push $fullImage } + + Write-Host "Successfully pushed to Docker Hub: $fullImage" -ForegroundColor Green +} +``` + +### AWS Elastic Container Registry (ECR) + +```powershell +Properties { + $AwsRegion = if ($env:AWS_REGION) { $env:AWS_REGION } else { 'us-east-1' } + $AwsAccountId = $env:AWS_ACCOUNT_ID + $EcrRepository = 'myapp' + $ImageTag = 'latest' +} + +Task VerifyAwsCli { + try { + $awsVersion = aws --version + Write-Host "AWS CLI: $awsVersion" -ForegroundColor Gray + } + catch { + throw "AWS CLI is not installed. Install from https://aws.amazon.com/cli/" + } +} + +Task EcrLogin -depends VerifyAwsCli { + Write-Host "Logging in to AWS ECR..." -ForegroundColor Green + + if ([string]::IsNullOrEmpty($AwsAccountId)) { + throw "AWS_ACCOUNT_ID environment variable is required" + } + + # Get ECR login password and login to Docker + $loginCmd = "aws ecr get-login-password --region $AwsRegion | docker login --username AWS --password-stdin ${AwsAccountId}.dkr.ecr.${AwsRegion}.amazonaws.com" + + exec { Invoke-Expression $loginCmd } + + Write-Host "Successfully logged in to ECR" -ForegroundColor Green +} + +Task EcrPush -depends Build, EcrLogin { + Write-Host "Pushing to AWS ECR..." -ForegroundColor Green + + $ecrImage = "${AwsAccountId}.dkr.ecr.${AwsRegion}.amazonaws.com/${EcrRepository}:${ImageTag}" + + # Tag image + exec { docker tag "${EcrRepository}:${ImageTag}" $ecrImage } + + # Push image + exec { docker push $ecrImage } + + Write-Host "Successfully pushed to ECR: $ecrImage" -ForegroundColor Green +} + +Task EcrCreateRepo -depends VerifyAwsCli { + Write-Host "Creating ECR repository..." -ForegroundColor Green + + try { + exec { + aws ecr create-repository ` + --repository-name $EcrRepository ` + --region $AwsRegion ` + --image-scanning-configuration scanOnPush=true + } + Write-Host "Repository created: $EcrRepository" -ForegroundColor Green + } + catch { + Write-Warning "Repository may already exist or creation failed" + } +} +``` + +### Azure Container Registry (ACR) + +```powershell +Properties { + $AcrName = $env:ACR_NAME # e.g., 'myregistry' + $AcrResourceGroup = $env:ACR_RESOURCE_GROUP + $Repository = 'myapp' + $ImageTag = 'latest' +} + +Task VerifyAzCli { + try { + $azVersion = az --version | Select-Object -First 1 + Write-Host "Azure CLI: $azVersion" -ForegroundColor Gray + } + catch { + throw "Azure CLI is not installed. Install from https://docs.microsoft.com/cli/azure/" + } +} + +Task AcrLogin -depends VerifyAzCli { + Write-Host "Logging in to Azure Container Registry..." -ForegroundColor Green + + if ([string]::IsNullOrEmpty($AcrName)) { + throw "ACR_NAME environment variable is required" + } + + exec { az acr login --name $AcrName } + + Write-Host "Successfully logged in to ACR: $AcrName" -ForegroundColor Green +} + +Task AcrPush -depends Build, AcrLogin { + Write-Host "Pushing to Azure Container Registry..." -ForegroundColor Green + + $acrImage = "${AcrName}.azurecr.io/${Repository}:${ImageTag}" + + # Tag image + exec { docker tag "${Repository}:${ImageTag}" $acrImage } + + # Push image + exec { docker push $acrImage } + + Write-Host "Successfully pushed to ACR: $acrImage" -ForegroundColor Green +} + +Task AcrCreateRepo -depends VerifyAzCli { + Write-Host "Creating ACR..." -ForegroundColor Green + + if ([string]::IsNullOrEmpty($AcrResourceGroup)) { + throw "ACR_RESOURCE_GROUP environment variable is required" + } + + try { + exec { + az acr create ` + --resource-group $AcrResourceGroup ` + --name $AcrName ` + --sku Basic + } + Write-Host "ACR created: $AcrName" -ForegroundColor Green + } + catch { + Write-Warning "ACR may already exist or creation failed" + } +} +``` + +### Google Container Registry (GCR) + +```powershell +Properties { + $GcpProject = $env:GCP_PROJECT_ID + $GcrHostname = 'gcr.io' # Options: gcr.io, us.gcr.io, eu.gcr.io, asia.gcr.io + $Repository = 'myapp' + $ImageTag = 'latest' +} + +Task VerifyGcloud { + try { + $gcloudVersion = gcloud --version | Select-Object -First 1 + Write-Host "Google Cloud SDK: $gcloudVersion" -ForegroundColor Gray + } + catch { + throw "Google Cloud SDK is not installed. Install from https://cloud.google.com/sdk/" + } +} + +Task GcrLogin -depends VerifyGcloud { + Write-Host "Configuring Docker for GCR..." -ForegroundColor Green + + exec { gcloud auth configure-docker $GcrHostname } + + Write-Host "Successfully configured Docker for GCR" -ForegroundColor Green +} + +Task GcrPush -depends Build, GcrLogin { + Write-Host "Pushing to Google Container Registry..." -ForegroundColor Green + + if ([string]::IsNullOrEmpty($GcpProject)) { + throw "GCP_PROJECT_ID environment variable is required" + } + + $gcrImage = "${GcrHostname}/${GcpProject}/${Repository}:${ImageTag}" + + # Tag image + exec { docker tag "${Repository}:${ImageTag}" $gcrImage } + + # Push image + exec { docker push $gcrImage } + + Write-Host "Successfully pushed to GCR: $gcrImage" -ForegroundColor Green +} +``` + +## Docker Compose Integration + +For multi-container applications, integrate Docker Compose: + +```powershell +Properties { + $ProjectRoot = $PSScriptRoot + $ComposeFile = Join-Path $ProjectRoot 'docker-compose.yml' + $ComposeProjectName = 'myapp' + $Environment = 'development' +} + +Task VerifyCompose { + Write-Host "Verifying Docker Compose installation..." -ForegroundColor Green + + try { + $composeVersion = docker compose version + Write-Host " Docker Compose: $composeVersion" -ForegroundColor Gray + } + catch { + throw "Docker Compose is not available. Install Docker Desktop or Docker Compose plugin." + } + + if (-not (Test-Path $ComposeFile)) { + throw "docker-compose.yml not found at: $ComposeFile" + } +} + +Task ComposeUp -depends VerifyCompose { + Write-Host "Starting Docker Compose services..." -ForegroundColor Green + + $env:COMPOSE_PROJECT_NAME = $ComposeProjectName + $env:ENVIRONMENT = $Environment + + exec { docker compose -f $ComposeFile up -d } + + Write-Host "Services started. Use 'docker compose ps' to view status." -ForegroundColor Green +} + +Task ComposeBuild -depends VerifyCompose { + Write-Host "Building Docker Compose services..." -ForegroundColor Green + + $env:COMPOSE_PROJECT_NAME = $ComposeProjectName + + exec { docker compose -f $ComposeFile build --no-cache } + + Write-Host "Services built successfully" -ForegroundColor Green +} + +Task ComposeDown { + Write-Host "Stopping Docker Compose services..." -ForegroundColor Green + + $env:COMPOSE_PROJECT_NAME = $ComposeProjectName + + exec { docker compose -f $ComposeFile down } + + Write-Host "Services stopped" -ForegroundColor Green +} + +Task ComposeDownVolumes { + Write-Host "Stopping services and removing volumes..." -ForegroundColor Green + + $env:COMPOSE_PROJECT_NAME = $ComposeProjectName + + exec { docker compose -f $ComposeFile down -v } + + Write-Host "Services stopped and volumes removed" -ForegroundColor Green +} + +Task ComposeLogs { + Write-Host "Viewing Docker Compose logs..." -ForegroundColor Green + + $env:COMPOSE_PROJECT_NAME = $ComposeProjectName + + exec { docker compose -f $ComposeFile logs -f } +} + +Task ComposePs { + Write-Host "Listing Docker Compose services..." -ForegroundColor Green + + $env:COMPOSE_PROJECT_NAME = $ComposeProjectName + + exec { docker compose -f $ComposeFile ps } +} + +Task ComposeRestart { + Write-Host "Restarting Docker Compose services..." -ForegroundColor Green + + $env:COMPOSE_PROJECT_NAME = $ComposeProjectName + + exec { docker compose -f $ComposeFile restart } + + Write-Host "Services restarted" -ForegroundColor Green +} +``` + +Example `docker-compose.yml`: + +```yaml +version: '3.8' + +services: + web: + build: + context: . + dockerfile: Dockerfile + target: ${ENVIRONMENT:-production} + ports: + - "8080:80" + environment: + - NODE_ENV=${ENVIRONMENT:-production} + - DATABASE_URL=postgresql://postgres:password@db:5432/myapp + depends_on: + - db + - redis + volumes: + - ./logs:/app/logs + networks: + - app-network + + db: + image: postgres:15-alpine + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_DB=myapp + volumes: + - db-data:/var/lib/postgresql/data + networks: + - app-network + ports: + - "5432:5432" + + redis: + image: redis:7-alpine + volumes: + - redis-data:/data + networks: + - app-network + ports: + - "6379:6379" + +volumes: + db-data: + redis-data: + +networks: + app-network: + driver: bridge +``` + +## Cross-Platform Builds + +Build images for multiple architectures: + +```powershell +Properties { + $ImageName = 'myapp' + $ImageTag = 'latest' + $Registry = 'docker.io' + $Username = $env:DOCKER_USERNAME + $Platforms = 'linux/amd64,linux/arm64,linux/arm/v7' + $BuilderName = 'multiplatform-builder' +} + +Task CreateBuilder { + Write-Host "Creating buildx builder..." -ForegroundColor Green + + # Check if builder exists + $existingBuilder = docker buildx ls | Select-String $BuilderName + + if (-not $existingBuilder) { + exec { docker buildx create --name $BuilderName --use } + Write-Host "Builder created: $BuilderName" -ForegroundColor Green + } + else { + exec { docker buildx use $BuilderName } + Write-Host "Using existing builder: $BuilderName" -ForegroundColor Gray + } + + # Bootstrap builder + exec { docker buildx inspect --bootstrap } +} + +Task BuildMultiPlatform -depends CreateBuilder { + Write-Host "Building multi-platform image..." -ForegroundColor Green + + $fullImage = "${Registry}/${Username}/${ImageName}:${ImageTag}" + + exec { + docker buildx build ` + --platform $Platforms ` + --tag $fullImage ` + --push ` + . + } + + Write-Host "Multi-platform image built and pushed: $fullImage" -ForegroundColor Green +} + +Task RemoveBuilder { + Write-Host "Removing buildx builder..." -ForegroundColor Green + + exec { docker buildx rm $BuilderName } -errorMessage "Builder not found" +} +``` + +## Best Practices + +### 1. Use .dockerignore + +Create a `.dockerignore` file to exclude unnecessary files: + +``` +node_modules +npm-debug.log +dist +build +.git +.gitignore +.env +.env.local +*.md +coverage +.vscode +.idea +``` + +### 2. Optimize Layer Caching + +```dockerfile +# Good: Copy dependency files first (cached unless they change) +COPY package*.json ./ +RUN npm ci + +# Then copy source code (changes frequently) +COPY . . +``` + +### 3. Use Specific Base Image Tags + +```dockerfile +# Bad: Latest can change unexpectedly +FROM node:latest + +# Good: Pin specific version +FROM node:18.17.1-alpine + +# Better: Use digest for immutability +FROM node:18.17.1-alpine@sha256:abc123... +``` + +### 4. Run as Non-Root User + +```dockerfile +# Create and use non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +USER nodejs +``` + +### 5. Health Checks + +```dockerfile +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 +``` + +### 6. Small Image Sizes + +```powershell +Task AnalyzeSize -depends Build { + Write-Host "Analyzing image sizes..." -ForegroundColor Green + + $images = docker images --format "{{.Repository}}:{{.Tag}}\t{{.Size}}" | Where-Object { $_ -like "*${ImageName}*" } + + foreach ($image in $images) { + Write-Host " $image" -ForegroundColor Gray + } + + # Use dive tool for detailed analysis + if (Get-Command dive -ErrorAction SilentlyContinue) { + exec { dive $ImageFullName } + } + else { + Write-Warning "Install 'dive' tool for detailed layer analysis: https://github.com/wagoodman/dive" + } +} +``` + +## CI/CD Integration + +### GitHub Actions + +```yaml +name: Docker Build + +on: [push, pull_request] + +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 Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Install psake + shell: pwsh + run: Install-Module -Name psake -Scope CurrentUser -Force + + - name: Build and push with psake + shell: pwsh + run: | + Invoke-psake -buildFile .\psakefile.ps1 -taskList Push + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} +``` + +## Troubleshooting + +### Docker Daemon Not Running + +**Problem:** `Cannot connect to the Docker daemon` + +**Solution:** + +```powershell +Task VerifyDocker { + try { + exec { docker info } | Out-Null + Write-Host "Docker daemon is running" -ForegroundColor Green + } + catch { + throw "Docker daemon is not running. Start Docker Desktop or Docker service." + } +} +``` + +### Build Cache Issues + +**Problem:** Changes not reflected in build + +**Solution:** Force rebuild without cache: + +```powershell +Task RebuildNoCache { + exec { docker build --no-cache -t $ImageFullName . } +} +``` + +### Permission Denied in Container + +**Problem:** EACCES or permission denied errors + +**Solution:** Fix file ownership: + +```dockerfile +# Change ownership to app user +COPY --chown=nodejs:nodejs . . + +# Or use chmod +RUN chmod -R 755 /app +``` + +### Large Image Sizes + +**Problem:** Images are too large + +**Solution:** Use Alpine base images and multi-stage builds: + +```dockerfile +# Use Alpine variants +FROM node:18-alpine AS base + +# Use multi-stage builds +FROM build AS production +COPY --from=build /app/dist ./dist + +# Remove unnecessary files +RUN rm -rf /tmp/* /var/cache/apk/* +``` + +### Port Already in Use + +**Problem:** Container fails to start due to port conflict + +**Solution:** + +```powershell +Task CheckPort { + param([int]$Port = 8080) + + $listener = Get-NetTCPConnection -LocalPort $Port -ErrorAction SilentlyContinue + + if ($listener) { + Write-Warning "Port $Port is already in use by process $($listener.OwningProcess)" + throw "Port conflict on $Port" + } +} + +Task Run -depends CheckPort, Build { + exec { docker run -p "${HostPort}:${ContainerPort}" $ImageFullName } +} +``` + +### Registry Authentication Failures + +**Problem:** Push fails with authentication error + +**Solution:** Use token authentication: + +```powershell +Task SecureLogin { + # Use token from environment variable + if ([string]::IsNullOrEmpty($env:DOCKER_TOKEN)) { + throw "DOCKER_TOKEN environment variable is required" + } + + # Use stdin to avoid exposing token in command + $env:DOCKER_TOKEN | docker login --username $env:DOCKER_USERNAME --password-stdin +} +``` + +## See Also + +- [Installing psake](/docs/tutorial-basics/installing) - Installation guide +- [GitHub Actions Integration](/docs/ci-examples/github-actions) - CI/CD automation with Docker +- [Node.js Builds](/docs/build-types/nodejs) - Node.js build examples +- [.NET Solution Builds](/docs/build-types/dot-net-solution) - .NET build examples +- [Error Handling](/docs/tutorial-advanced/logging-errors) - Build error management diff --git a/docs/build-types/nodejs.md b/docs/build-types/nodejs.md new file mode 100644 index 0000000..a08b32d --- /dev/null +++ b/docs/build-types/nodejs.md @@ -0,0 +1,964 @@ +--- +title: "Node.js and npm Projects" +description: "Build, test, and deploy Node.js applications using psake with npm, TypeScript, Webpack, and npm registry publishing" +--- + +# Node.js and npm Projects + +psake can orchestrate Node.js and npm-based builds, providing a consistent PowerShell-based build automation layer across your development workflow. This guide shows you how to build, test, bundle, and publish Node.js projects using psake. + +## Quick Start + +Here's a basic psake build script for a Node.js project: + +```powershell +Properties { + $ProjectRoot = $PSScriptRoot + $NodeModules = Join-Path $ProjectRoot 'node_modules' + $BuildDir = Join-Path $ProjectRoot 'build' + $DistDir = Join-Path $ProjectRoot 'dist' +} + +Task Default -depends Test + +Task Clean { + if (Test-Path $BuildDir) { + Remove-Item $BuildDir -Recurse -Force + } + if (Test-Path $DistDir) { + Remove-Item $DistDir -Recurse -Force + } +} + +Task Install { + exec { npm install } +} + +Task Build -depends Install, Clean { + exec { npm run build } +} + +Task Test -depends Build { + exec { npm test } +} +``` + +Run the build: + +```powershell +Invoke-psake -buildFile .\psakefile.ps1 +``` + +## Complete Node.js Build Example + +Here's a comprehensive psakefile.ps1 for a production Node.js application: + +```powershell +Properties { + $ProjectRoot = $PSScriptRoot + $SrcDir = Join-Path $ProjectRoot 'src' + $TestDir = Join-Path $ProjectRoot 'tests' + $BuildDir = Join-Path $ProjectRoot 'build' + $DistDir = Join-Path $ProjectRoot 'dist' + $CoverageDir = Join-Path $ProjectRoot 'coverage' + $NodeModules = Join-Path $ProjectRoot 'node_modules' + + $Environment = 'development' + $Version = '1.0.0' + $Verbose = $false +} + +FormatTaskName { + param($taskName) + Write-Host "Executing task: $taskName" -ForegroundColor Cyan +} + +Task Default -depends Test + +Task Clean { + Write-Host "Cleaning build artifacts..." -ForegroundColor Green + + @($BuildDir, $DistDir, $CoverageDir) | ForEach-Object { + if (Test-Path $_) { + Remove-Item $_ -Recurse -Force + Write-Host " Removed: $_" -ForegroundColor Gray + } + } + + New-Item -ItemType Directory -Path $BuildDir -Force | Out-Null + New-Item -ItemType Directory -Path $DistDir -Force | Out-Null +} + +Task Install { + Write-Host "Installing npm dependencies..." -ForegroundColor Green + + if (-not (Test-Path $NodeModules)) { + exec { npm install } + } else { + exec { npm ci } + } +} + +Task Lint -depends Install { + Write-Host "Running ESLint..." -ForegroundColor Green + exec { npm run lint } +} + +Task Build -depends Install, Clean { + Write-Host "Building application..." -ForegroundColor Green + + $env:NODE_ENV = $Environment + + if ($Verbose) { + exec { npm run build -- --verbose } + } else { + exec { npm run build } + } + + Write-Host "Build complete: $BuildDir" -ForegroundColor Green +} + +Task Test -depends Build { + Write-Host "Running tests..." -ForegroundColor Green + exec { npm test } +} + +Task TestWatch { + Write-Host "Running tests in watch mode..." -ForegroundColor Green + exec { npm run test:watch } +} + +Task Coverage -depends Install { + Write-Host "Running tests with coverage..." -ForegroundColor Green + exec { npm run test:coverage } + + if (Test-Path (Join-Path $CoverageDir 'lcov-report/index.html')) { + Write-Host "Coverage report: $CoverageDir/lcov-report/index.html" -ForegroundColor Yellow + } +} + +Task Bundle -depends Test { + Write-Host "Creating production bundle..." -ForegroundColor Green + + $env:NODE_ENV = 'production' + exec { npm run bundle } + + Write-Host "Bundle complete: $DistDir" -ForegroundColor Green +} + +Task Package -depends Bundle { + Write-Host "Creating package..." -ForegroundColor Green + + exec { npm pack --pack-destination $DistDir } + + $packageFile = Get-ChildItem "$DistDir/*.tgz" | Select-Object -First 1 + Write-Host "Package created: $($packageFile.Name)" -ForegroundColor Green +} + +Task Verify { + Write-Host "Verifying package.json..." -ForegroundColor Green + + if (-not (Test-Path 'package.json')) { + throw "package.json not found" + } + + $packageJson = Get-Content 'package.json' | ConvertFrom-Json + + if ([string]::IsNullOrEmpty($packageJson.name)) { + throw "Package name is required in package.json" + } + + if ([string]::IsNullOrEmpty($packageJson.version)) { + throw "Package version is required in package.json" + } + + Write-Host " Package: $($packageJson.name)@$($packageJson.version)" -ForegroundColor Gray +} + +Task Publish -depends Test, Verify, Package { + Write-Host "Publishing to npm registry..." -ForegroundColor Green + + $npmToken = $env:NPM_TOKEN + if ([string]::IsNullOrEmpty($npmToken)) { + throw "NPM_TOKEN environment variable is required for publishing" + } + + # Configure npm authentication + exec { npm config set //registry.npmjs.org/:_authToken $npmToken } + + try { + exec { npm publish --access public } + Write-Host "Successfully published to npm registry" -ForegroundColor Green + } + finally { + # Clean up authentication + exec { npm config delete //registry.npmjs.org/:_authToken } + } +} + +Task Dev { + Write-Host "Starting development server..." -ForegroundColor Green + exec { npm run dev } +} + +Task Serve -depends Build { + Write-Host "Starting production server..." -ForegroundColor Green + + $env:NODE_ENV = 'production' + exec { npm start } +} + +Task Audit { + Write-Host "Running security audit..." -ForegroundColor Green + exec { npm audit } +} + +Task AuditFix { + Write-Host "Fixing security vulnerabilities..." -ForegroundColor Green + exec { npm audit fix } +} + +Task Outdated { + Write-Host "Checking for outdated packages..." -ForegroundColor Green + exec { npm outdated } -errorMessage "Some packages are outdated (this is informational)" +} + +Task UpdateDeps { + Write-Host "Updating dependencies..." -ForegroundColor Green + exec { npm update } + exec { npm outdated } -errorMessage "Dependencies updated" +} +``` + +## TypeScript Compilation + +For TypeScript projects, add TypeScript-specific tasks: + +```powershell +Properties { + $ProjectRoot = $PSScriptRoot + $SrcDir = Join-Path $ProjectRoot 'src' + $OutDir = Join-Path $ProjectRoot 'dist' + $TsConfig = Join-Path $ProjectRoot 'tsconfig.json' +} + +Task TypeCheck -depends Install { + Write-Host "Running TypeScript type checking..." -ForegroundColor Green + + if (-not (Test-Path $TsConfig)) { + throw "tsconfig.json not found" + } + + exec { npx tsc --noEmit } + Write-Host "Type checking passed" -ForegroundColor Green +} + +Task CompileTS -depends Install, Clean { + Write-Host "Compiling TypeScript..." -ForegroundColor Green + + exec { npx tsc --project $TsConfig } + + Write-Host "TypeScript compilation complete: $OutDir" -ForegroundColor Green +} + +Task CompileTSWatch { + Write-Host "Compiling TypeScript in watch mode..." -ForegroundColor Green + exec { npx tsc --watch --project $TsConfig } +} + +Task Build -depends TypeCheck, CompileTS { + Write-Host "Build complete" -ForegroundColor Green +} +``` + +Update your `tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} +``` + +## Webpack Bundling + +For projects using Webpack, integrate bundling tasks: + +```powershell +Properties { + $ProjectRoot = $PSScriptRoot + $SrcDir = Join-Path $ProjectRoot 'src' + $DistDir = Join-Path $ProjectRoot 'dist' + $WebpackConfig = Join-Path $ProjectRoot 'webpack.config.js' + $Environment = 'development' +} + +Task WebpackBuild -depends Install { + Write-Host "Running Webpack build ($Environment)..." -ForegroundColor Green + + if (-not (Test-Path $WebpackConfig)) { + throw "webpack.config.js not found" + } + + $env:NODE_ENV = $Environment + + if ($Environment -eq 'production') { + exec { npx webpack --config $WebpackConfig --mode production } + } else { + exec { npx webpack --config $WebpackConfig --mode development } + } + + Write-Host "Webpack bundle complete: $DistDir" -ForegroundColor Green +} + +Task WebpackWatch -depends Install { + Write-Host "Running Webpack in watch mode..." -ForegroundColor Green + exec { npx webpack --config $WebpackConfig --mode development --watch } +} + +Task WebpackAnalyze -depends Install { + Write-Host "Analyzing Webpack bundle..." -ForegroundColor Green + + $env:ANALYZE = 'true' + exec { npx webpack --config $WebpackConfig --mode production } +} + +Task OptimizeBundle -depends WebpackBuild { + Write-Host "Optimizing bundle size..." -ForegroundColor Green + + # Run bundle size analysis + exec { npx bundlesize } + + # Check bundle sizes + $jsFiles = Get-ChildItem "$DistDir/*.js" -File + foreach ($file in $jsFiles) { + $sizeKB = [math]::Round($file.Length / 1KB, 2) + Write-Host " $($file.Name): ${sizeKB} KB" -ForegroundColor Gray + + if ($sizeKB -gt 500) { + Write-Warning "Bundle size exceeds 500 KB: $($file.Name)" + } + } +} +``` + +Example `webpack.config.js`: + +```javascript +const path = require('path'); +const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); + +module.exports = { + entry: './src/index.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + }, + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + ], + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'], + }, + plugins: [ + ...(process.env.ANALYZE ? [new BundleAnalyzerPlugin()] : []), + ], +}; +``` + +## Testing with Jest + +Integrate Jest testing into your psake build: + +```powershell +Properties { + $ProjectRoot = $PSScriptRoot + $TestDir = Join-Path $ProjectRoot 'tests' + $CoverageDir = Join-Path $ProjectRoot 'coverage' + $CoverageThreshold = 80 +} + +Task Test -depends Install { + Write-Host "Running Jest tests..." -ForegroundColor Green + exec { npx jest --coverage=false } +} + +Task TestWatch -depends Install { + Write-Host "Running Jest in watch mode..." -ForegroundColor Green + exec { npx jest --watch } +} + +Task TestCoverage -depends Install { + Write-Host "Running tests with coverage..." -ForegroundColor Green + exec { npx jest --coverage --coverageReporters=text --coverageReporters=html } + + # Parse coverage summary + $coverageSummary = Join-Path $CoverageDir 'coverage-summary.json' + if (Test-Path $coverageSummary) { + $coverage = Get-Content $coverageSummary | ConvertFrom-Json + $totalCoverage = $coverage.total.lines.pct + + Write-Host "Total line coverage: ${totalCoverage}%" -ForegroundColor Cyan + + if ($totalCoverage -lt $CoverageThreshold) { + throw "Coverage ${totalCoverage}% is below threshold ${CoverageThreshold}%" + } + } +} + +Task TestCI -depends Install { + Write-Host "Running tests for CI..." -ForegroundColor Green + + # Use CI-friendly options + exec { npx jest --ci --coverage --maxWorkers=2 } +} +``` + +Example `jest.config.js`: + +```javascript +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src', '/tests'], + testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/**/*.test.ts', + ], + coverageThreshold: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80, + }, + }, +}; +``` + +## Linting with ESLint + +Add linting tasks to maintain code quality: + +```powershell +Task Lint -depends Install { + Write-Host "Running ESLint..." -ForegroundColor Green + exec { npx eslint src --ext .js,.ts,.tsx } +} + +Task LintFix -depends Install { + Write-Host "Running ESLint with auto-fix..." -ForegroundColor Green + exec { npx eslint src --ext .js,.ts,.tsx --fix } +} + +Task Format -depends Install { + Write-Host "Formatting code with Prettier..." -ForegroundColor Green + exec { npx prettier --write "src/**/*.{js,ts,tsx,json,css,md}" } +} + +Task FormatCheck -depends Install { + Write-Host "Checking code formatting..." -ForegroundColor Green + exec { npx prettier --check "src/**/*.{js,ts,tsx,json,css,md}" } +} +``` + +## Publishing to npm Registry + +Here's a complete workflow for publishing packages to npm: + +```powershell +Properties { + $ProjectRoot = $PSScriptRoot + $DistDir = Join-Path $ProjectRoot 'dist' + $Registry = 'https://registry.npmjs.org/' + $NpmToken = $env:NPM_TOKEN + $DryRun = $false +} + +Task ValidatePackage { + Write-Host "Validating package..." -ForegroundColor Green + + # Check package.json + if (-not (Test-Path 'package.json')) { + throw "package.json not found" + } + + $pkg = Get-Content 'package.json' | ConvertFrom-Json + + # Validate required fields + $requiredFields = @('name', 'version', 'description', 'main', 'license') + foreach ($field in $requiredFields) { + if ([string]::IsNullOrEmpty($pkg.$field)) { + throw "package.json is missing required field: $field" + } + } + + # Check if version already exists + $packageName = $pkg.name + $version = $pkg.version + + Write-Host " Package: $packageName" -ForegroundColor Gray + Write-Host " Version: $version" -ForegroundColor Gray + Write-Host " License: $($pkg.license)" -ForegroundColor Gray + + try { + $existingVersions = npm view $packageName versions --json | ConvertFrom-Json + if ($existingVersions -contains $version) { + throw "Version $version already exists in registry" + } + } + catch { + Write-Host " Package not yet published (this is OK for first release)" -ForegroundColor Yellow + } +} + +Task PrepareRelease -depends Test, ValidatePackage { + Write-Host "Preparing release..." -ForegroundColor Green + + # Clean and build + exec { Invoke-psake -taskList Clean, Build } + + # Verify dist directory exists + if (-not (Test-Path $DistDir)) { + throw "Distribution directory not found: $DistDir" + } + + # Check for required files + $requiredFiles = @('package.json', 'README.md') + foreach ($file in $requiredFiles) { + if (-not (Test-Path $file)) { + throw "Required file not found: $file" + } + } +} + +Task PublishPackage -depends PrepareRelease { + Write-Host "Publishing package to npm..." -ForegroundColor Green + + if ([string]::IsNullOrEmpty($NpmToken)) { + throw "NPM_TOKEN environment variable is required" + } + + # Configure authentication + $npmrcPath = Join-Path $ProjectRoot '.npmrc' + try { + # Create temporary .npmrc + @" +//registry.npmjs.org/:_authToken=$NpmToken +registry=$Registry +"@ | Set-Content $npmrcPath + + if ($DryRun) { + Write-Host "DRY RUN: Would publish package" -ForegroundColor Yellow + exec { npm publish --dry-run --access public } + } + else { + exec { npm publish --access public } + + $pkg = Get-Content 'package.json' | ConvertFrom-Json + Write-Host "Successfully published $($pkg.name)@$($pkg.version)" -ForegroundColor Green + } + } + finally { + # Clean up .npmrc + if (Test-Path $npmrcPath) { + Remove-Item $npmrcPath -Force + } + } +} + +Task PublishBeta -depends Test, ValidatePackage { + Write-Host "Publishing beta version to npm..." -ForegroundColor Green + + if ([string]::IsNullOrEmpty($NpmToken)) { + throw "NPM_TOKEN environment variable is required" + } + + # Configure authentication + exec { npm config set //registry.npmjs.org/:_authToken $NpmToken } + + try { + exec { npm publish --tag beta --access public } + Write-Host "Successfully published beta version" -ForegroundColor Green + } + finally { + exec { npm config delete //registry.npmjs.org/:_authToken } + } +} + +Task UnpublishPackage { + Write-Host "WARNING: Unpublishing package..." -ForegroundColor Red + + $pkg = Get-Content 'package.json' | ConvertFrom-Json + $packageName = $pkg.name + $version = $pkg.version + + $confirmation = Read-Host "Are you sure you want to unpublish ${packageName}@${version}? (yes/no)" + if ($confirmation -ne 'yes') { + Write-Host "Unpublish cancelled" -ForegroundColor Yellow + return + } + + if ([string]::IsNullOrEmpty($NpmToken)) { + throw "NPM_TOKEN environment variable is required" + } + + exec { npm config set //registry.npmjs.org/:_authToken $NpmToken } + + try { + exec { npm unpublish "${packageName}@${version}" } + Write-Host "Successfully unpublished ${packageName}@${version}" -ForegroundColor Green + } + finally { + exec { npm config delete //registry.npmjs.org/:_authToken } + } +} +``` + +## Monorepo Support (npm Workspaces) + +For monorepo projects using npm workspaces: + +```powershell +Properties { + $ProjectRoot = $PSScriptRoot + $Workspaces = @('packages/core', 'packages/cli', 'packages/utils') +} + +Task InstallAll { + Write-Host "Installing all workspace dependencies..." -ForegroundColor Green + exec { npm install } +} + +Task BuildAll { + Write-Host "Building all workspaces..." -ForegroundColor Green + + foreach ($workspace in $Workspaces) { + Write-Host " Building $workspace..." -ForegroundColor Cyan + exec { npm run build --workspace=$workspace } + } +} + +Task TestAll { + Write-Host "Testing all workspaces..." -ForegroundColor Green + exec { npm test --workspaces } +} + +Task BuildWorkspace { + param([string]$Name) + + if ([string]::IsNullOrEmpty($Name)) { + throw "Workspace name is required. Usage: Invoke-psake BuildWorkspace -parameters @{Name='packages/core'}" + } + + Write-Host "Building workspace: $Name" -ForegroundColor Green + exec { npm run build --workspace=$Name } +} + +Task PublishWorkspace { + param([string]$Name) + + if ([string]::IsNullOrEmpty($Name)) { + throw "Workspace name is required" + } + + Write-Host "Publishing workspace: $Name" -ForegroundColor Green + exec { npm publish --workspace=$Name --access public } +} +``` + +## Docker Integration + +Combine psake with Docker for containerized Node.js builds: + +```powershell +Properties { + $ImageName = 'myapp' + $ImageTag = 'latest' + $DockerRegistry = 'docker.io' +} + +Task DockerBuild -depends Test { + Write-Host "Building Docker image..." -ForegroundColor Green + + $fullImageName = "${DockerRegistry}/${ImageName}:${ImageTag}" + exec { docker build -t $fullImageName . } + + Write-Host "Docker image built: $fullImageName" -ForegroundColor Green +} + +Task DockerRun { + Write-Host "Running Docker container..." -ForegroundColor Green + + $fullImageName = "${DockerRegistry}/${ImageName}:${ImageTag}" + exec { docker run -p 3000:3000 $fullImageName } +} + +Task DockerPush -depends DockerBuild { + Write-Host "Pushing Docker image to registry..." -ForegroundColor Green + + $fullImageName = "${DockerRegistry}/${ImageName}:${ImageTag}" + exec { docker push $fullImageName } +} +``` + +Example `Dockerfile` for Node.js: + +```dockerfile +FROM node:18-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci --only=production + +COPY . . +RUN npm run build + +FROM node:18-alpine + +WORKDIR /app + +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +COPY package*.json ./ + +EXPOSE 3000 + +CMD ["node", "dist/index.js"] +``` + +## CI/CD Integration + +Example integration with CI/CD platforms: + +### GitHub Actions + +```yaml +name: Build + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install psake + shell: pwsh + run: Install-Module -Name psake -Scope CurrentUser -Force + + - name: Run psake build + shell: pwsh + run: | + Invoke-psake -buildFile .\psakefile.ps1 -taskList Test + + - name: Publish to npm + if: github.ref == 'refs/heads/main' + shell: pwsh + run: | + Invoke-psake -buildFile .\psakefile.ps1 -taskList PublishPackage + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} +``` + +## Best Practices + +### 1. Lock Dependencies + +Always commit `package-lock.json` and use `npm ci` in CI/CD: + +```powershell +Task Install { + if ($env:CI -eq 'true') { + exec { npm ci } # Clean install from lockfile + } else { + exec { npm install } # Allow updates locally + } +} +``` + +### 2. Environment-Specific Builds + +Use environment variables and different configurations: + +```powershell +Properties { + $Environment = if ($env:NODE_ENV) { $env:NODE_ENV } else { 'development' } +} + +Task Build { + Write-Host "Building for environment: $Environment" -ForegroundColor Green + + $env:NODE_ENV = $Environment + exec { npm run build } +} +``` + +### 3. Version Bumping + +Automate version bumping: + +```powershell +Task BumpVersion { + param([string]$Type = 'patch') + + Write-Host "Bumping $Type version..." -ForegroundColor Green + exec { npm version $Type --no-git-tag-version } + + $pkg = Get-Content 'package.json' | ConvertFrom-Json + Write-Host "New version: $($pkg.version)" -ForegroundColor Green +} +``` + +### 4. Clean Node Modules + +Periodically clean and reinstall: + +```powershell +Task CleanInstall { + Write-Host "Cleaning node_modules..." -ForegroundColor Green + + if (Test-Path $NodeModules) { + Remove-Item $NodeModules -Recurse -Force + } + + if (Test-Path 'package-lock.json') { + Remove-Item 'package-lock.json' -Force + } + + exec { npm install } +} +``` + +## Troubleshooting + +### npm Command Not Found + +**Problem:** `exec: npm: The term 'npm' is not recognized` + +**Solution:** Ensure Node.js is installed and in PATH: + +```powershell +Task VerifyNode { + try { + $nodeVersion = node --version + $npmVersion = npm --version + Write-Host "Node.js: $nodeVersion" -ForegroundColor Green + Write-Host "npm: $npmVersion" -ForegroundColor Green + } + catch { + throw "Node.js and npm are required. Install from https://nodejs.org/" + } +} + +Task Build -depends VerifyNode { + exec { npm run build } +} +``` + +### Module Not Found Errors + +**Problem:** Build fails with "Cannot find module" errors + +**Solution:** Ensure dependencies are installed: + +```powershell +Task Build -depends Install { + if (-not (Test-Path $NodeModules)) { + throw "node_modules not found. Run Install task first." + } + + exec { npm run build } +} +``` + +### Permission Errors on Linux/macOS + +**Problem:** EACCES errors when installing global packages + +**Solution:** Use `--prefix` or configure npm properly: + +```powershell +Task InstallGlobal { + $npmPrefix = if ($IsLinux -or $IsMacOS) { + "$HOME/.npm-global" + } else { + "$env:APPDATA\npm" + } + + exec { npm config set prefix $npmPrefix } + exec { npm install -g typescript } +} +``` + +### Build Fails Due to Memory Issues + +**Problem:** JavaScript heap out of memory + +**Solution:** Increase Node.js memory limit: + +```powershell +Task Build { + $env:NODE_OPTIONS = '--max-old-space-size=4096' + exec { npm run build } +} +``` + +### TypeScript Compilation Errors + +**Problem:** Type errors break the build + +**Solution:** Add separate type-checking task: + +```powershell +Task TypeCheck { + Write-Host "Type checking..." -ForegroundColor Green + exec { npx tsc --noEmit } +} + +Task Build -depends TypeCheck { + exec { npx tsc } +} +``` + +## See Also + +- [Installing psake](/docs/tutorial-basics/installing) - Installation guide +- [GitHub Actions Integration](/docs/ci-examples/github-actions) - CI/CD automation +- [Docker Builds](/docs/build-types/docker) - Docker integration +- [.NET Solution Builds](/docs/build-types/dot-net-solution) - .NET build examples +- [Error Handling](/docs/tutorial-advanced/logging-errors) - Build error management diff --git a/sidebars.ts b/sidebars.ts index d06f651..92a4abb 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -46,7 +46,9 @@ const sidebars: SidebarsConfig = { type: 'category', label: 'Build Types', items: [ - 'build-types/dot-net-solution' + 'build-types/dot-net-solution', + 'build-types/nodejs', + 'build-types/docker' ] }, {