Skip to main content

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:

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:

Invoke-psake -buildFile .\psakefile.ps1

Complete Docker Build Example

Here's a comprehensive psakefile.ps1 for Docker-based builds:

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:

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:

# 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:

# 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

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)

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)

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)

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:

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:

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:

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

# 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

# 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

# Create and use non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001

USER nodejs

5. Health Checks

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

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

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:

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:

Task RebuildNoCache {
exec { docker build --no-cache -t $ImageFullName . }
}

Permission Denied in Container

Problem: EACCES or permission denied errors

Solution: Fix file ownership:

# 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:

# 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:

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:

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