Introduction

Welcome to the 5th part of the blog series about setting up Azurite with a self-signed certificate in Docker.

In the previous parts we’ve assembled all the puzzle pieces necessary for communicating with Azurite over HTTPS through our containerized application.

In this part we will optimize our Dockerfile and code so this only becomes relevant for our development cycle, not our other environments.

You can read the previous parts here:

Using environment variables

Let’s start with moving our hard-coded service URL in our ~/azurite-demo/demo-app/Program.cs file to the appsettings.json file.

I personally prefer to enter an empty string in my production appsettings and the actual value in my development settings where applicable so I get a reminder in case I forget a setting. Obviously this is completely up to you if you want to follow along.

Let’s open up our appsettings.json file and add the following JSON object:

"Storage": {
  "ServiceUri": ""
}

Then we’ll head over to our appsettings.Development.json file and add the same JSON but with the actual value for the Azurite URL now:

"Storage": {
  "ServiceUri": "https://127.0.0.1:10000/devstoreaccount1"
}

Yes, we’re referring to 127.0.0.1 here, we’ll cover the Docker container name in a different environment variable later on.

We’re not picking the Storage object with the ServiceUri randomly. These settings correspond with the BlobServiceClient constructor values as can be read in Microsoft’s documentation.

Note that if you do pick different values for your appsettings, the way we set up our BlobServiceClient next won’t work for you.

Go to the Program.cs file and update the AddBlobServiceClient extension method:

clientBuilder.AddBlobServiceClient(builder.Configuration.GetSection("Storage"));

Since we’re using the JSON object of Storage with the ServiceUri property, we can simply tell the AddBlobServiceClient method to take this Configuration section. The Azure SDK will wire it up for us.

Run the application (dotnet run) and verify you still get a response from your /blob endpoint.

Run this on your machine, not through a Docker container.

Setting up environment variables for our Docker container

Now that we’ve got our application ready to support different URLs based on appsettings, we can also add this to our containerized version of the application.

We have already done a similar thing for our Azure credentials using the azure.env file in part 4 of this series.

Now let’s create a app.env file in the root directory of your project (~/azurite-demo) and add the storage URL appsetting.

Remember that if you’re on WSL2/Linux you should use __ (double underscore) as a separater for nested settings as opposed to : on Windows machines.

Storage__ServiceUri=https://azurite:10000/devstoreaccount1

After this we will update our Compose file (~/azurite-demo/compose.yaml) to read from this new environment file:

demo_app:
  container_name: demo-app
  build:
    context: .
    dockerfile: Dockerfile
  env_file:
    - azure.env
    - app.env
  ports:
    - 8080

Let’s run our applications: docker compose up -d --build and let’s navigate to the /blob endpoint of container’s URL. You should see your blob item.

Optimizing our Dockerfile

Currently when we build our .NET application with our Dockerfile, it will always trust the certificate we’ve generated and self-signed for development purposes. Whilst this won’t hurt, it’s not optimal to execute this in any other environment than development. Especially when you’re using the same Dockerfile for automated builds to other environments, for instance using automated deployment pipelines (e.g. GitHub Actions).

We can use more multi-stage magic to make this happen only in situations where we want it. More specifically, by using targets.

Let’s open up our Dockerfile (~/azurite-demo/Dockerfile).

We’re going to change the stages a bit. First of all we’re going to add an alias to our second stage called runtime. To do so, add AS runtime after the second FROM statement:

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime

Next, we’re going to make the runtime stage copy the files from the build-env stage to the /app directory. In other words, we’ll grab the two lines we have below the RUN update-ca-certificates statement and paste them below the runtime stage:

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime

WORKDIR /app
COPY --from=build-env /publish .

Now we have a runtime stage available which has the slimmed down version of the .NET runtime, rather than the entire SDK and our .NET application’s published files available for reference.

Below these lines, we’ll create a new stage called development, based on the runtime stage like so:

FROM runtime AS development

This stage will do the certificate work we’ve set-up previously, as well as switching to the /app directory later on and setting the ENTRYPOINT statement. It no longer needs to copy the files from the /publish directory from the other stage as we’re basing it off our runtime stage. Remove that code

Your development stage should then look like this:

FROM runtime AS development

WORKDIR /certs
COPY ./certs/cert.pem .
RUN cp cert.pem /usr/local/share/ca-certificates/cert.crt
RUN update-ca-certificates

WORKDIR /app
ENTRYPOINT ["dotnet", "demo-app.dll"]

Finally, below the ENTRYPOINT of the development stage, we’ll create a new unnamed stage also based off the runtime stage. This simply points to the /app directory and executes (the same) ENTRYPOINT statement as our development stage.

FROM runtime

WORKDIR /app
ENTRYPOINT ["dotnet", "demo-app.dll"]

Your entire Dockerfile now looks like:

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
WORKDIR /app

COPY ./demo-app ./
RUN dotnet restore
RUN dotnet publish --no-restore -c Release -o /publish

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime

WORKDIR /app
COPY --from=build-env /publish .

FROM runtime AS development

WORKDIR /certs
COPY ./certs/cert.pem .
RUN cp cert.pem /usr/local/share/ca-certificates/cert.crt
RUN update-ca-certificates

WORKDIR /app
ENTRYPOINT ["dotnet", "demo-app.dll"]

FROM runtime

WORKDIR /app
ENTRYPOINT ["dotnet", "demo-app.dll"]

If we would now run our Compose services (docker compose up -d --build), you’ll see it will no longer import and trust the self-signed certificate (causing an HTTP 500 error when navigating to the /blob endpoint, by the way).

We can remedy this by updating our Compose file (~/azurite-demo/compose.yaml).

At our demo_app service, in the build property we can add the target property and set this to development like so:

build:
  context: .
  dockerfile: Dockerfile
  target: development

If you intend to use the same Compose file for deployments or for other environments, it might be a good idea to move this target to an environment variable.

If we run our Compose services now (docker compose up -d --build), everything works like it used to do.

Next

Great! We now have an optimized Dockerfile with support from Compose to have the option to import and trust the self-signed certificate or not.

We also have updated our code to let the Blob Service Client be registered based on an app setting rather than a hardcoded URL.

In the next part we’ll deploy a real storage account in Azure, upload a blob to it and deploy our .NET application to Azure and allow it to read the file using managed identities and the same code as we’ve written all the way back in part 3 - excluding the environment variables, but that was a minor change 😉!

If you want to clean up your local Docker files you can run docker compose down --rmi all. If you want to clean your entire Docker environment afterwards, you can run: docker system prune -af && docker volume prune -af && docker builder prune -af.

Continue to part 6 here.