Add Docker To Existing Net Core Project

Add Docker To Existing Net Core Project

Reading time1 min
#Docker#DevOps#Cloud#DotNet#Containerization#Microservices

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.