Integrating Docker with a Mature .NET Core Codebase
Retrofitting Docker into an established .NET Core application is rarely plug-and-play. It immediately raises thorny issues: configuration management, dependency disclosure, build reproducibility, ad-hoc local setups that diverge from production. Legacy configuration practices—hardcoded file paths or ignored environment overrides—typically surface in this transition.
Consider an internal .NET Core web API used across multiple business units. Developers report “no repro” on environment-specific bugs. Each CI run risks breaking due to library mismatches. A move to containerization resolves these problems by enforcing environment parity and predictable deployments, but unless carefully approached, introduces its own friction.
Reasons for Containerizing an Existing App
- Uniform build and run environments. With containers, applications and dependencies remain consistent from developer laptop to CI runner to production node.
- Rapid, atomic deployments. A Docker image bundles runtime and application code, minimizing risk of "drift".
- Portability across platforms. .NET Core supports Linux containers, essential for cloud-native workloads.
- Isolation and resource constraints. Containers encapsulate workloads, preventing resource-hungry processes from affecting neighbors.
Containerization also exposes historical configuration flaws. Gotcha: if your app loads settings solely via local files, expect to revisit your config pipeline.
1. Audit the Project State
First, ensure a reproducible build:
- Project should build cleanly:
dotnet build
(test with SDK version pinned, e.g.,dotnet build --framework net7.0
) - Confirm successful local run:
dotnet run
or via IDE with environment matching target deployment (ideally Linux if targeting Linux containers). - Identify any local dependencies: untracked local files, database mounts, hardcoded hostnames.
Applications tightly coupled to the local environment (e.g., "C:\\Temp\\output.log"
) will require refactoring.
2. Author a Multi-Stage Dockerfile
At the project root, commit to version control a Dockerfile
. For .NET 7, a standard two-stage Dockerfile is:
# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY *.csproj ./
RUN dotnet restore --disable-parallel
COPY . ./
RUN dotnet publish -c Release -o /app/out
# Stage 2: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /app
COPY --from=build /app/out ./
ENV ASPNETCORE_URLS=http://+:80
EXPOSE 80
ENTRYPOINT ["dotnet", "MyWebApi.dll"]
Replace MyWebApi.dll
with your real publish output, exact casing matters on Linux containers.
Note: For projects referencing private NuGet feeds, pre-seed a nuget.config
(COPY before dotnet restore
). Miss this and restore will fail cryptically:
error : Unable to load the service index for source https://internal-feed/nuget/v3/index.json.
3. .dockerignore
Shouldn’t Be Overlooked
A common trap: Docker context bloat. Create a .dockerignore
file in the root with:
bin/
obj/
.vscode/
.git/
*.user
docker-compose*
*.suo
This prevents copying user settings and workspace junk into build context, preserves CI secrets, and accelerates build. Untracked secrets in .git
will still leak—validate with docker build
’s output verbosity.
4. Build the Container Image
From the same directory as your Dockerfile, run:
docker build --pull --no-cache -t mywebapi:local .
Recommended: always --pull
for base images to avoid outdated (and potentially insecure) SDK/runtimes.
Build output includes layer caching, so intermediate changes are incremental—unless left uncached by a missing .dockerignore
. Watch for size; anything >350MB for a simple API likely indicates context issues.
5. Running the Container: Bind Ports and Inspect Logs
Test local container instance as follows:
docker run --rm -it -p 5005:80 --name apitest mywebapi:local
--rm
cleans up after exit.-it
for attached process and readable logs on startup.-p 5005:80
maps external port 5005 to container’s port 80 (use different mapping for parallel services).
Container logs are your immediate health check:
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://[::]:80
If you see:
It was not possible to find any compatible framework version
The runtime image and SDK image are mismatched or the wrong DLL is specified.
6. Refactor Configuration for Containerization
Strong recommendation: Transition settings from static files to environment variables.
- .NET Core uses layered configuration; environment variables override
appsettings.json
. Use colon-separated keys:docker run -e 'ConnectionStrings__Main=Server=db;Database=prod;User Id=sa;Password=...' ...
- When secrets are involved, inject via secret manager or Docker secrets, never plain text in Dockerfiles or images.
If persistent storage or logs are required, use Docker volumes:
docker run -v /host/logs:/app/logs ...
Note: File-based session or cache storage should be avoided in stateless workloads.
7. Use Docker Compose for Multi-Service Solutions
Legacy apps rarely exist in isolation. Include service dependencies:
version: '3.8'
services:
api:
build: .
ports:
- "5005:80"
environment:
ConnectionStrings__Main: Server=db;Database=webapi;
depends_on:
- db
db:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
SA_PASSWORD: "yourStrong(!)Password"
ACCEPT_EULA: "Y"
ports:
- "1433:1433"
Bring up entire stack:
docker-compose up --build
Known issue: depends_on
does not guarantee db readiness; always use retry logic in app startup.
Additional Notes and Practical Tips
- CI/CD integration: Push Dockerfile to mainline. Trigger builds on commit—prefer ACR/ECR/GCR as registries. Use
docker scan
to spot vulnerabilities. - Non-obvious: For .NET global tools/layered caching, group COPY/restore statements to maximize Docker’s layer caching.
- .NET app self-contained deployments can further eliminate runtime dependencies but increase image size. Evaluate trade-offs.
- Debugging containerized apps: Use
docker exec -it apitest /bin/bash
for direct container inspection.
Further Reading
Retrofitting Docker to an existing .NET Core project demands discipline, not heroics. Expect edge cases with legacy configs, test thoroughly, and iterate builds until parity with the existing environment is proven—don’t assume “works on my machine” is a solved problem without scrutiny. Ignore environmental drift at your own risk.