Best Practices
This guide covers recommended patterns, approaches, and best practices for using JD.MSBuild.Containers effectively.
Configuration Best Practices
1. Use Directory.Build.props for Shared Settings
For multi-project solutions, define common Docker settings in Directory.Build.props:
<!-- Directory.Build.props at solution root -->
<Project>
<PropertyGroup>
<!-- Common Docker settings -->
<DockerRegistry>myregistry.azurecr.io</DockerRegistry>
<DockerBaseImageRuntime>mcr.microsoft.com/dotnet/aspnet</DockerBaseImageRuntime>
<DockerBaseImageSdk>mcr.microsoft.com/dotnet/sdk</DockerBaseImageSdk>
<DockerBaseImageVersion>8.0</DockerBaseImageVersion>
<!-- Default to generate-only in Debug -->
<DockerEnabled Condition="'$(Configuration)' == 'Debug'">true</DockerEnabled>
<DockerBuildImage Condition="'$(Configuration)' == 'Debug'">false</DockerBuildImage>
<!-- Full automation in Release -->
<DockerEnabled Condition="'$(Configuration)' == 'Release'">true</DockerEnabled>
<DockerBuildImage Condition="'$(Configuration)' == 'Release'">true</DockerBuildImage>
<DockerBuildOnPublish Condition="'$(Configuration)' == 'Release'">true</DockerBuildOnPublish>
</PropertyGroup>
</Project>
Benefits:
- Consistency across all projects
- Single source of truth for Docker configuration
- Easier to maintain and update
2. Use Conditional Properties for Different Environments
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DockerImageTag>dev-$(USERNAME)</DockerImageTag>
<DockerBuildImage>false</DockerBuildImage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DockerImageTag>$(Version)</DockerImageTag>
<DockerBuildImage>true</DockerBuildImage>
<DockerPushImage>true</DockerPushImage>
</PropertyGroup>
3. Pin Base Image Versions
Always specify explicit versions for base images in production:
<!-- ❌ Bad: Unpredictable -->
<DockerBaseImageVersion>latest</DockerBaseImageVersion>
<!-- ✅ Good: Explicit version -->
<DockerBaseImageVersion>8.0.1</DockerBaseImageVersion>
<!-- ✅ Better: Full digest for immutability -->
<DockerBaseImageRuntime>mcr.microsoft.com/dotnet/aspnet@sha256:abc123...</DockerBaseImageRuntime>
4. Use Semantic Versioning for Image Tags
<!-- Use GitVersion or similar for automatic versioning -->
<PropertyGroup>
<DockerImageTag>$(GitVersion_SemVer)</DockerImageTag>
<!-- Results in: myapp:1.2.3 -->
</PropertyGroup>
5. Externalize Sensitive Configuration
Never hardcode credentials or secrets:
<!-- ❌ Bad: Hardcoded credentials -->
<DockerRegistry>myregistry.azurecr.io</DockerRegistry>
<DockerRegistryUsername>admin</DockerRegistryUsername>
<DockerRegistryPassword>MyP@ssw0rd</DockerRegistryPassword>
<!-- ✅ Good: Use environment variables or CI/CD secrets -->
<DockerRegistry>$(DOCKER_REGISTRY)</DockerRegistry>
<!-- Authentication handled by docker login separately -->
Performance Best Practices
1. Enable Fingerprinting (Default)
Keep fingerprinting enabled for incremental builds:
<!-- Default behavior - no need to set -->
<DockerUseFingerprinting>true</DockerUseFingerprinting>
Impact: Skips Dockerfile regeneration when project hasn't changed, saving 1-5 seconds per build.
2. Optimize Docker Build Context
Exclude unnecessary files from Docker build context:
Create .dockerignore:
# .dockerignore
**/bin/
**/obj/
**/.git/
**/.vs/
**/.vscode/
**/node_modules/
**/TestResults/
*.md
.gitignore
.editorconfig
Impact: Reduces context size, speeds up builds by 30-50%.
3. Use Multi-Stage Builds (Default)
Keep multi-stage builds enabled:
<!-- Default behavior - no need to set -->
<DockerUseMultiStage>true</DockerUseMultiStage>
Benefits:
- Smaller final images (50-70% reduction)
- Better layer caching
- Faster subsequent builds
4. Minimize Dockerfile Regeneration
Only regenerate when necessary:
<!-- Generate once, then use existing Dockerfile -->
<PropertyGroup Condition="Exists('Dockerfile')">
<DockerGenerateDockerfile>false</DockerGenerateDockerfile>
<DockerBuildImage>true</DockerBuildImage>
</PropertyGroup>
Security Best Practices
1. Use Non-Root Users (Default)
JD.MSBuild.Containers automatically creates non-root users:
<!-- Default behavior - recommended to keep enabled -->
<DockerCreateUser>true</DockerCreateUser>
<DockerUser>app</DockerUser>
Why: Running as root in containers is a security risk.
2. Scan Images for Vulnerabilities
Integrate security scanning in CI/CD:
# GitHub Actions
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
3. Keep Base Images Updated
Regularly update base image versions:
# Check for updates
docker pull mcr.microsoft.com/dotnet/aspnet:8.0
# Update in .csproj
<DockerBaseImageVersion>8.0.2</DockerBaseImageVersion>
4. Don't Embed Secrets in Images
Never include secrets in Docker images:
<!-- ❌ Bad: Secrets in environment variables -->
<DockerEnvironmentVariables>API_KEY=sk-12345;DB_PASSWORD=secret</DockerEnvironmentVariables>
<!-- ✅ Good: Pass secrets at runtime -->
<!-- docker run -e API_KEY=${API_KEY} myapp:latest -->
5. Use Minimal Base Images
Prefer minimal/slim images when possible:
<DockerBaseImageRuntime>mcr.microsoft.com/dotnet/aspnet:8.0-alpine</DockerBaseImageRuntime>
Benefits:
- Smaller attack surface
- Fewer vulnerabilities
- Smaller image size
CI/CD Best Practices
1. Separate Build and Deploy Steps
# Build job - create and test image
build:
- dotnet build
- dotnet test
- dotnet publish # Creates Docker image
- docker run myapp:test
- run-integration-tests
# Deploy job - push to registry
deploy:
needs: build
- docker push myapp:$VERSION
2. Tag Images with Commit SHA
- name: Build with commit SHA
run: |
dotnet publish \
/p:DockerImageTag=${{ github.sha }}
Benefits:
- Traceability
- Ability to rollback to any commit
- Prevents tag collisions
3. Use Build Matrix for Multi-Platform
strategy:
matrix:
platform: [linux/amd64, linux/arm64]
steps:
- name: Build for platform
run: |
dotnet publish \
/p:DockerBuildPlatform=${{ matrix.platform }}
4. Cache Docker Layers
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build with cache
uses: docker/build-push-action@v5
with:
cache-from: type=gha
cache-to: type=gha,mode=max
5. Validate Before Pushing
Always test images before pushing to registry:
- name: Build image
run: dotnet publish
- name: Test image
run: |
docker run -d -p 8080:8080 --name test myapp:latest
sleep 5
curl --fail http://localhost:8080/health || exit 1
docker stop test
- name: Push image
run: docker push myapp:latest
Development Best Practices
1. Review Generated Dockerfiles
Periodically review generated Dockerfiles to understand what's being created:
dotnet build
cat Dockerfile
2. Commit Generated Dockerfiles
For transparency and review:
# Generate Dockerfile
dotnet build
# Review changes
git diff Dockerfile
# Commit if acceptable
git add Dockerfile
git commit -m "Update Dockerfile for new dependencies"
3. Use Generate-Only Mode Locally
For local development, prefer generate-only mode:
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DockerEnabled>true</DockerEnabled>
<DockerBuildImage>false</DockerBuildImage>
</PropertyGroup>
Rationale: Faster builds, review Dockerfiles before building images.
4. Test in Containers Regularly
Don't wait until CI to test in containers:
# Local container test
dotnet publish
docker run -p 8080:8080 myapp:dev
curl http://localhost:8080/health
5. Keep Docker Running Locally
Ensure Docker is always running during development:
# Add to shell profile
if ! docker info > /dev/null 2>&1; then
echo "⚠️ Docker is not running!"
fi
Project Structure Best Practices
1. Organize Multi-Project Solutions
MySolution/
├── Directory.Build.props ← Shared Docker settings
├── src/
│ ├── MyApp.Api/
│ │ ├── MyApp.Api.csproj ← API-specific Docker settings
│ │ └── Dockerfile ← Generated Dockerfile
│ └── MyApp.Worker/
│ ├── MyApp.Worker.csproj ← Worker-specific Docker settings
│ └── Dockerfile ← Generated Dockerfile
└── tests/
└── MyApp.Tests/
└── MyApp.Tests.csproj ← No Docker settings
2. Use Scripts for Complex Scenarios
For complex pre/post build logic, use external scripts:
<PropertyGroup>
<DockerPreBuildScript>$(MSBuildProjectDirectory)/scripts/pre-build.sh</DockerPreBuildScript>
<DockerPostPublishScript>$(MSBuildProjectDirectory)/scripts/deploy.ps1</DockerPostPublishScript>
</PropertyGroup>
scripts/pre-build.sh:
#!/bin/bash
set -e
# Complex logic here
echo "Running pre-build tasks..."
3. Maintain Separate .dockerignore
Keep .dockerignore alongside Dockerfile:
MyApp/
├── MyApp.csproj
├── Dockerfile ← Generated
├── .dockerignore ← Maintained manually
└── Program.cs
Image Tagging Best Practices
1. Use Multiple Tags
Tag images with multiple identifiers:
# In CI/CD pipeline
VERSION=1.2.3
COMMIT=$(git rev-parse --short HEAD)
# Tag with version
docker tag myapp:build myapp:$VERSION
# Tag with commit
docker tag myapp:build myapp:$COMMIT
# Tag as latest
docker tag myapp:build myapp:latest
# Push all tags
docker push myapp:$VERSION
docker push myapp:$COMMIT
docker push myapp:latest
2. Semantic Versioning Tags
myapp:1.2.3 ← Full version
myapp:1.2 ← Minor version
myapp:1 ← Major version
myapp:latest ← Latest stable
3. Environment Tags
myapp:dev ← Development
myapp:staging ← Staging
myapp:prod ← Production
Monitoring and Logging Best Practices
1. Include Health Checks
Ensure your application has health endpoints:
// ASP.NET Core
app.MapHealthChecks("/health");
Configure Docker health check:
<PropertyGroup>
<DockerHealthCheck>CMD curl --fail http://localhost:8080/health || exit 1</DockerHealthCheck>
<DockerHealthCheckInterval>30s</DockerHealthCheckInterval>
<DockerHealthCheckTimeout>3s</DockerHealthCheckTimeout>
<DockerHealthCheckRetries>3</DockerHealthCheckRetries>
</PropertyGroup>
2. Use Structured Logging
Configure proper logging in containers:
builder.Services.AddLogging(logging =>
{
logging.AddJsonConsole(); // Structured JSON logs
});
3. Set Log Verbosity
Control MSBuild/Docker log verbosity:
<!-- Minimal logs for CI -->
<DockerLogVerbosity Condition="'$(CI)' == 'true'">minimal</DockerLogVerbosity>
<!-- Detailed logs for troubleshooting -->
<DockerLogVerbosity Condition="'$(DEBUG_BUILD)' == 'true'">detailed</DockerLogVerbosity>
Testing Best Practices
1. Test Generated Dockerfiles
Create tests for Dockerfile generation:
[Test]
public void GeneratedDockerfile_ShouldUseCorrectBaseImage()
{
// Build project
var result = DotNetBuild("MyApp.csproj");
// Read generated Dockerfile
var dockerfile = File.ReadAllText("Dockerfile");
// Assert
Assert.That(dockerfile, Contains.Substring("FROM mcr.microsoft.com/dotnet/aspnet:8.0"));
}
2. Integration Tests with Containers
Test your application in containers:
[Test]
public async Task Container_ShouldRespondToHealthCheck()
{
// Start container
var container = await DockerHelper.RunAsync("myapp:test", port: 8080);
try
{
// Wait for startup
await Task.Delay(5000);
// Test health endpoint
var response = await httpClient.GetAsync("http://localhost:8080/health");
Assert.That(response.IsSuccessStatusCode, Is.True);
}
finally
{
await container.StopAsync();
}
}
3. Validate Image Size
Ensure images don't grow unexpectedly:
[Test]
public void Image_ShouldBeUnder500MB()
{
var image = DockerHelper.InspectImage("myapp:latest");
var sizeMB = image.Size / 1024 / 1024;
Assert.That(sizeMB, Is.LessThan(500));
}
Documentation Best Practices
1. Document Docker Configuration
Add comments to explain Docker settings:
<PropertyGroup>
<!-- Enable Docker for production builds only -->
<DockerEnabled Condition="'$(Configuration)' == 'Release'">true</DockerEnabled>
<!-- Use Azure Container Registry for production images -->
<DockerRegistry>myregistry.azurecr.io</DockerRegistry>
<!-- Version images using GitVersion -->
<DockerImageTag>$(GitVersion_SemVer)</DockerImageTag>
</PropertyGroup>
2. Maintain README with Docker Instructions
Include Docker commands in project README:
## Building Docker Image
### Local Development
```bash
dotnet build # Generates Dockerfile
docker build -t myapp:dev .
docker run -p 8080:8080 myapp:dev
Production Build
dotnet publish --configuration Release # Builds image
docker push myregistry.azurecr.io/myapp:latest
### 3. Document Custom Dockerfile Modifications
If using custom Dockerfiles, document changes:
```dockerfile
# Custom Dockerfile
# Modifications from generated version:
# 1. Added nginx for reverse proxy
# 2. Custom healthcheck interval
# 3. Additional apt packages for dependencies
Anti-Patterns to Avoid
❌ Don't Commit Build Artifacts
<!-- Bad: Building inside source control -->
<DockerBuildOnBuild>true</DockerBuildOnBuild>
❌ Don't Use latest Tag in Production
<!-- Bad: Unpredictable -->
<DockerImageTag>latest</DockerImageTag>
<!-- Good: Explicit version -->
<DockerImageTag>$(Version)</DockerImageTag>
❌ Don't Hardcode Environment-Specific Values
<!-- Bad: Hardcoded -->
<DockerRegistry>prod-registry.azurecr.io</DockerRegistry>
<!-- Good: Configurable -->
<DockerRegistry>$(DOCKER_REGISTRY)</DockerRegistry>
❌ Don't Ignore .dockerignore
Always maintain a .dockerignore file to exclude unnecessary files.
❌ Don't Run as Root
<!-- Bad: Security risk -->
<DockerCreateUser>false</DockerCreateUser>
<!-- Good: Non-root user -->
<DockerCreateUser>true</DockerCreateUser>
Next Steps
- Samples - See best practices in action
- API Reference - Explore all configuration options
- Workflows - Implement these practices in your workflows