Transitioning an Application from On-Premises to Azure Docker Containers
In a previous blog post, I discussed the process of migrating an on-premises application to an Azure Scale Set. Recently, I had again the opportunity to transition an existing .NET background service to Azure using docker. A key distinction between the previously migrated application and the one targeted for this migration was its lack of Windows dependencies. This meant that the application could be feasibly migrated to a Docker container.
Starting with docker containers
Despite my limited experience with Docker containers, I was eager to delve deeper and expand my knowledge. I immersed myself in numerous documentation resources to understand how to create a Docker container using a Dockerfile. Most examples demonstrated the use of a Linux base image and the building of the application using the .NET CLI like the following:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
# Copy everything
COPY . ./
# Restore as distinct layers
RUN dotnet restore
# Build and publish a release
RUN dotnet publish -c Release -o out
# Build runtime image
COPY --from=build-env /App/out .
ENTRYPOINT ["dotnet", "DotNet.Docker.dll"]
The private nuget feed problem
The above Dockerfile works great for applications that do not have dependencies on private nuget feeds. However, as you might have anticipated, the application targeted for migration did rely on a private NuGet feed. One potential workaround for this predicament would be to incorporate the credentials for the private NuGet feed directly into the Dockerfile. There are diffrent ways to accomplish this, one of them can be read here Consuming private NuGet feeds from a Dockerfile in a secure and DevOps friendly manner.
While this approach might seem straightforward, it’s not without its drawbacks. Apart from the glaring security implications, the process of making these adjustments can be quite cumbersome.
Fortunately, the .NET CLI comes to the rescue, offering support for
building Docker images with the command
dotnet publish --os linux --arch x64 /t:PublishContainer -c Release.
The Docker image was constructed within a CI/CD pipeline, an environment
already authenticated and granted access to the private NuGet feed. As a
result there was no need for additional configuration.
- task: DotNetCoreCLI@2
displayName: 'dotnet restore'
- task: DotNetCoreCLI@2
displayName: 'dotnet build'
- task: DotNetCoreCLI@2
displayName: 'dotnet publish'
arguments: '-c Release --os linux --arch x64 /t:PublishContainer /p:ContainerImageTag=latest /p:ContainerRepository=DotNetDockerTemp'
- task: PowerShell@2
docker save DotNetDockerTemp:latest -o $(Build.ArtifactStagingDirectory)/DotNetDockerTemp.tar
- task: PublishBuildArtifacts@1
(1. Code snippet from the build pipeline)
Please notice if you want to create a docker image based on the alpine image, you need to specify the correct runtime identifier (RID) for the alpine image, otherwise you will not be able to start the application. You can find the RIDs here: RID catalog and more about the issue here.
dotnet publish -c Release --runtime=linux-musl-x64 /t:PublishContainer /p:ContainerImageTag=latest /p:ContainerRepository=DotNetDockerTemp /p:ContainerBaseImage=mcr.microsoft.com/dotnet/runtime:8.0-alpine'
Multiple Enviroments and the appsettings.json
You may have noticed in the above build pipeline that we are using
dotnet publish /t:PublishContainer command. This
command builds and publishes the Docker image to the local Docker
repository. While this is excellent for local development, it raises
questions about handling multiple environments and the appsettings.json
To address this, we save the local published Docker image into the
DotNetDockerTemp.tar file and add it to the artifacts of
the build pipeline (see 1. Code snippet from the build pipeline). During
the release pipeline, we download the artifact and load the Docker image
into the local Docker repository using the
docker load -i DotNetDockerTemp.tar command.
Next, we tackle the
appsettings.json file. For this, we
FileTransform@1 task, which transforms the
appsettings.json file based on the environment. While we
can’t inject the
appsettings.json file into the existing
DotNetDockerTemp:latest, we can create a new
Docker image based on the existing one.
- task: PowerShell@2
displayName: "Inject" appsettings.json into docker image
docker create --name dockertemp DotNetDockerTemp:latest
docker cp appsettings.json dockertemp:/app/appsettings.json
docker commit dockertemp DotNetDockerFinal:latest
- task: AzureCLI@2
displayName: Azure Container Registry Login
az acr login --name $(AzureContainerRegistryLoginName) docker push [...]
- Code snippet from the release pipeline
Finally, as a last step, we could push the docker image to the Azure Container Registry.
Custom DNS and SSL Certificates
Upon starting the Azure Container Instance (ACI), I discovered that
the service was unable to access certain endpoints. After conducting
some investigations, connecting to the container instance with
az container exec (or using the azure portal) and probing
nslookup and related commands, I deduced that the
Docker container instance was incapable of resolving our custom domain
names. This is understandable, as the Docker container instance does not
know our custom DNS server. This issue can be resolved by configuring
dnsConfig for your ACI.
With the updated settings, the app was able to resolve the endpoint but encountered an exception due to an invalid SSL certificate. Once again, this is logical as the Docker container instance is unaware of our custom SSL certificate. To enable the app or the operating system to accept the custom certificate of our endpoint, we need to add the public key of the certificate to the trusted root certificates of the Docker container instance. I was unable to find a method to accomplish this within the pipeline using the Docker command line. Consequently, I opted to create a custom Docker image based on the existing one and add the public key of the certificate to the trusted root certificates of the Docker container instance.
FROM DotNetDockerTemp:latest AS base
# Second stage: Use the Alpine image
FROM mcr.microsoft.com/dotnet/runtime:8.0-alpine AS final
# Copy files from the first stage
COPY --from=base /app /app
COPY ["certificate.crt", "/usr/local/share/ca-certificates/"]
RUN apk add --no-cache ca-certificates
This dockerfile use the docker image which was previously created
dotnet publish /t:PublishContainer [...] command
and add the public key of the certificate to the trusted root
certificates of the docker container instance. Since the app
DotNetDocker needs to be started with
DotNetDocker -s we
need to add the
-s to the CMD command. To build the docker
image using the dockerfile we can use the following command:
docker buildx build -f src/Dockerfile . -t localhost:5000/DotNetDockerFinal:latest --no-cache --progress=plain
- -f: Path to the dockerfile
- .: Path to the context; current directory where docker buildx build is executed (important for the COPY command)
- -t: Tag of the docker image
- –no-cache: Do not use cache when building the image
- –progress=plain: Show the progress of the build
Transitioning the application into a Docker container proved to be a swift and straightforward process. However, it’s important to note that this journey can entail numerous considerations that may demand a significant investment of time, particularly when the source code of the application is not open to modifications.
I’m eager to hear your thoughts on this process. Would you have approached it differently? Do you have any queries or insights to share? Your feedback is invaluable, and I look forward to our continued discussions on this topic.Propose a change