Workflows
This guide covers common development and CI/CD workflows using JD.MSBuild.Containers.
Local Development Workflows
Workflow 1: Review-First (Generate-Only)
Use Case: You want to review Dockerfiles before building images.
Configuration
<PropertyGroup>
<DockerEnabled>true</DockerEnabled>
<!-- DockerGenerateDockerfile defaults to true -->
<!-- DockerBuildImage defaults to false -->
</PropertyGroup>
Workflow Steps
# 1. Make code changes
vim Program.cs
# 2. Build project (generates Dockerfile)
dotnet build
# 3. Review generated Dockerfile
cat Dockerfile
# 4. Commit Dockerfile to source control
git add Dockerfile
git commit -m "Update Dockerfile"
# 5. Build Docker image manually when ready
docker build -t myapp:latest .
# 6. Run container
docker run -p 8080:8080 myapp:latest
Benefits:
- Review and understand Dockerfiles before building
- Commit Dockerfiles for team visibility
- Manual control over image builds
Workflow 2: Fully Automated (Build-On-Publish)
Use Case: You want complete automation during publish.
Configuration
<PropertyGroup>
<DockerEnabled>true</DockerEnabled>
<DockerGenerateDockerfile>true</DockerGenerateDockerfile>
<DockerBuildImage>true</DockerBuildImage>
<DockerBuildOnPublish>true</DockerBuildOnPublish>
<DockerImageName>myapp</DockerImageName>
<DockerImageTag>dev</DockerImageTag>
</PropertyGroup>
Workflow Steps
# 1. Make code changes
vim Program.cs
# 2. Publish (generates Dockerfile and builds image)
dotnet publish
# 3. Run container immediately
docker run -p 8080:8080 myapp:dev
# 4. Test your changes
curl http://localhost:8080/api/health
Benefits:
- Single command builds everything
- Fast inner-loop development
- Immediate container testing
Workflow 3: Build-Only (Custom Dockerfile)
Use Case: You maintain a custom Dockerfile but want automated builds.
Configuration
<PropertyGroup>
<DockerEnabled>true</DockerEnabled>
<DockerGenerateDockerfile>false</DockerGenerateDockerfile>
<DockerBuildImage>true</DockerBuildImage>
<DockerBuildOnPublish>true</DockerBuildOnPublish>
<!-- Optional: specify custom Dockerfile location -->
<DockerfileSource>$(MSBuildProjectDirectory)/deploy/Dockerfile</DockerfileSource>
</PropertyGroup>
Workflow Steps
# 1. Edit custom Dockerfile
vim deploy/Dockerfile
# 2. Make code changes
vim Program.cs
# 3. Publish (builds image using custom Dockerfile)
dotnet publish
# 4. Run container
docker run -p 8080:8080 myapp:latest
Benefits:
- Full control over Dockerfile
- Automated build process
- Flexibility for complex scenarios
Workflow 4: Hot Reload with Docker
Use Case: Develop inside containers with live reload.
Configuration
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DockerEnabled>true</DockerEnabled>
<DockerBuildImage>true</DockerBuildImage>
<DockerBuildOnBuild>true</DockerBuildOnBuild>
<DockerRunContainer>true</DockerRunContainer>
<DockerRunOnBuild>true</DockerRunOnBuild>
<!-- Mount source for hot reload -->
<DockerVolumeMappings>$(MSBuildProjectDirectory):/src:ro</DockerVolumeMappings>
<DockerEnvironmentVariables>ASPNETCORE_ENVIRONMENT=Development;DOTNET_USE_POLLING_FILE_WATCHER=true</DockerEnvironmentVariables>
</PropertyGroup>
Workflow Steps
# 1. Start development with hot reload
dotnet watch
# Container automatically:
# - Rebuilds on code changes
# - Restarts application
# - Reloads browser
# 2. Make changes and see them immediately
vim Program.cs
# Auto-reload happens
Benefits:
- Container-based development
- Hot reload support
- Production-like environment
CI/CD Workflows
GitHub Actions
Basic CI Workflow
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-containerize:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Test
run: dotnet test --no-build --configuration Release
- name: Publish and Build Docker Image
run: dotnet publish --configuration Release
# JD.MSBuild.Containers handles Docker build
- name: Verify Image
run: docker images | grep myapp
Versioned Release Workflow
name: Release
on:
push:
tags:
- 'v*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Get version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Publish
run: |
dotnet publish \
--configuration Release \
/p:DockerImageTag=${{ steps.version.outputs.VERSION }} \
/p:DockerRegistry=${{ env.REGISTRY }} \
/p:DockerImageName=${{ env.IMAGE_NAME }} \
/p:DockerPushImage=true \
/p:DockerPushOnPublish=true
Multi-Environment Deployment
name: Deploy
on:
push:
branches: [ main, develop, staging ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Determine Environment
id: env
run: |
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "ENV=production" >> $GITHUB_OUTPUT
echo "TAG=latest" >> $GITHUB_OUTPUT
elif [ "${{ github.ref }}" == "refs/heads/staging" ]; then
echo "ENV=staging" >> $GITHUB_OUTPUT
echo "TAG=staging" >> $GITHUB_OUTPUT
else
echo "ENV=development" >> $GITHUB_OUTPUT
echo "TAG=dev" >> $GITHUB_OUTPUT
fi
- name: Build and Push
run: |
dotnet publish \
--configuration Release \
/p:DockerImageTag=${{ steps.env.outputs.TAG }} \
/p:DockerRegistry=myregistry.azurecr.io \
/p:DockerPushImage=true
- name: Deploy to Environment
run: |
# Your deployment logic here
echo "Deploying to ${{ steps.env.outputs.ENV }}"
Azure DevOps
Pipeline YAML
trigger:
branches:
include:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
dockerRegistry: 'myregistry.azurecr.io'
stages:
- stage: Build
jobs:
- job: BuildAndContainerize
steps:
- task: UseDotNet@2
inputs:
version: '8.x'
- task: DotNetCoreCLI@2
displayName: 'Restore'
inputs:
command: 'restore'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
arguments: '--configuration $(buildConfiguration)'
- task: DotNetCoreCLI@2
displayName: 'Test'
inputs:
command: 'test'
arguments: '--configuration $(buildConfiguration) --no-build'
- task: DotNetCoreCLI@2
displayName: 'Publish and Build Docker Image'
inputs:
command: 'publish'
arguments: >
--configuration $(buildConfiguration)
/p:DockerBuildImage=true
/p:DockerRegistry=$(dockerRegistry)
/p:DockerImageTag=$(Build.BuildId)
- task: Docker@2
displayName: 'Push Docker Image'
inputs:
command: 'push'
repository: '$(dockerRegistry)/myapp'
tags: '$(Build.BuildId)'
GitLab CI
.gitlab-ci.yml
stages:
- build
- test
- containerize
- deploy
variables:
DOCKER_REGISTRY: registry.gitlab.com
IMAGE_NAME: $CI_REGISTRY_IMAGE
build:
stage: build
image: mcr.microsoft.com/dotnet/sdk:8.0
script:
- dotnet restore
- dotnet build --configuration Release --no-restore
artifacts:
paths:
- bin/
- obj/
test:
stage: test
image: mcr.microsoft.com/dotnet/sdk:8.0
script:
- dotnet test --configuration Release --no-build
containerize:
stage: containerize
image: mcr.microsoft.com/dotnet/sdk:8.0
services:
- docker:dind
script:
- dotnet publish --configuration Release
/p:DockerBuildImage=true
/p:DockerImageTag=$CI_COMMIT_SHORT_SHA
/p:DockerRegistry=$DOCKER_REGISTRY
only:
- main
- develop
deploy:
stage: deploy
script:
- docker push $IMAGE_NAME:$CI_COMMIT_SHORT_SHA
only:
- main
Local NuGet Package Workflow
When developing JD.MSBuild.Containers itself or testing local changes:
Step 1: Pack Local Version
# Build and pack the library
dotnet pack src/JD.MSBuild.Containers --configuration Release --output ./local-nuget
# Version will be in the .nupkg filename
ls ./local-nuget/
# JD.MSBuild.Containers.1.0.0-local.nupkg
Step 2: Configure Local Feed
Create or update NuGet.Config in your test project:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="local" value="../local-nuget" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
Step 3: Reference Local Package
<ItemGroup>
<PackageReference Include="JD.MSBuild.Containers" Version="1.0.0-local" />
</ItemGroup>
Step 4: Test Changes
# Clear NuGet cache to ensure fresh package
dotnet nuget locals all --clear
# Restore with local package
dotnet restore
# Build/publish to test
dotnet publish
Advanced Workflows
Pre-Build Script Workflow
Execute custom logic before Docker build:
Configuration
<PropertyGroup>
<DockerEnabled>true</DockerEnabled>
<DockerBuildImage>true</DockerBuildImage>
<DockerPreBuildScript>$(MSBuildProjectDirectory)/scripts/pre-build.sh</DockerPreBuildScript>
</PropertyGroup>
Pre-Build Script (pre-build.sh)
#!/bin/bash
set -e
echo "Running pre-build tasks..."
# Generate version file
echo "VERSION=$(git describe --tags --always)" > version.txt
# Download external dependencies
curl -o config.json https://config-service/api/config
# Validate environment
if [ -z "$API_KEY" ]; then
echo "ERROR: API_KEY not set"
exit 1
fi
echo "Pre-build completed successfully"
Post-Publish Script Workflow
Execute deployment after successful build:
Configuration
<PropertyGroup>
<DockerEnabled>true</DockerEnabled>
<DockerBuildImage>true</DockerBuildImage>
<DockerPushImage>true</DockerPushImage>
<DockerPostPublishScript>$(MSBuildProjectDirectory)/scripts/deploy.ps1</DockerPostPublishScript>
</PropertyGroup>
Post-Publish Script (deploy.ps1)
#!/usr/bin/env pwsh
param(
[string]$ImageName = "myapp:latest",
[string]$Environment = "staging"
)
Write-Host "Deploying $ImageName to $Environment..."
# Update Kubernetes deployment
kubectl set image deployment/myapp myapp=$ImageName -n $Environment
# Wait for rollout
kubectl rollout status deployment/myapp -n $Environment
# Run smoke tests
Invoke-RestMethod -Uri "https://$Environment.myapp.com/health"
Write-Host "Deployment completed successfully"
Conditional Containerization
Different settings per configuration:
<!-- Debug: Generate only -->
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DockerEnabled>true</DockerEnabled>
<DockerGenerateDockerfile>true</DockerGenerateDockerfile>
<DockerBuildImage>false</DockerBuildImage>
</PropertyGroup>
<!-- Release: Full automation -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DockerEnabled>true</DockerEnabled>
<DockerGenerateDockerfile>true</DockerGenerateDockerfile>
<DockerBuildImage>true</DockerBuildImage>
<DockerBuildOnPublish>true</DockerBuildOnPublish>
<DockerPushImage>true</DockerPushImage>
<DockerPushOnPublish>true</DockerPushOnPublish>
</PropertyGroup>
Troubleshooting Common Workflows
Issue: Slow Docker Builds in CI
Solution: Use Docker layer caching
# GitHub Actions
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and Push
uses: docker/build-push-action@v5
with:
context: .
cache-from: type=gha
cache-to: type=gha,mode=max
Issue: Credentials Not Available in CI
Solution: Use environment variables
<!-- .csproj -->
<PropertyGroup>
<DockerRegistry Condition="'$(DOCKER_REGISTRY)' != ''">$(DOCKER_REGISTRY)</DockerRegistry>
</PropertyGroup>
# GitHub Actions
- name: Publish
run: dotnet publish
env:
DOCKER_REGISTRY: ghcr.io
Issue: Different Image Names Per Branch
Solution: Dynamic image tags
# Bash script
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
TAG=$(echo $BRANCH_NAME | tr '/' '-')
dotnet publish /p:DockerImageTag=$TAG
Next Steps
- Best Practices - Learn recommended patterns
- Samples - See working examples
- API Reference - Explore all configuration options