Swimburger

How to run Umbraco 9 as a Linux Docker container

Niels Swimberghe

Niels Swimberghe - - Umbraco

Follow me on Twitter, buy me a coffee

Umbraco logo next Docker logo next to title: How to run Umbraco 9 as a Linux Docker container

Umbraco 9 has been built on top of .NET 5 (.NET Core). This means you are not limited to hosting Umbraco on Windows anymore, but you can now also host Umbraco on macOS and many popular Linux distributions. As a result, you can also containerize your Umbraco 9 websites in Linux containers using Docker.

Prerequisites #

You will need these items to follow along:

  • .NET 5 SDK (install)
  • Docker Engine and CLI
  • A Microsoft SQL Server and database
  • A connection string to the database

This tutorial will be using Ubuntu with the bash shell, but you should be able to follow along on Windows with minor modifications.

Umbraco uses Microsoft SQL server as its database. There are many ways to create a Microsoft SQL Server. You could host it on your machine, run it in a container, or use a cloud offering like Azure SQL. This tutorial will not go over how to set up a Microsoft SQL server or how to create a connection string.

Creating a new Umbraco 9 website #

Create a new directory to host your new Umbraco 9 source code in and move into the new directory:

mkdir Umbraco9Docker
cd Umbraco9Docker

Run the following command to install the Umbraco 9 templates into the .NET SDK:

dotnet new -i Umbraco.Templates::*

Run this command to create a new Umbraco 9 website:

dotnet new umbraco

If you wanted to, you could build and run this new website directly without using a container using the dotnet run command.

Building a Docker image for your Umbraco 9 site #

You have to create a Dockerfile to provide instructions to Docker on how to build the container image. Create a file called Dockerfile and paste in the following content:

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["Umbraco9Docker.csproj", "./"]
RUN dotnet restore "Umbraco9Docker.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet publish "Umbraco9Docker.csproj" -c Release -o /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
EXPOSE 80
ENV ASPNETCORE_URLS=http://+:80
COPY --from=build /app/publish .

ENTRYPOINT ["dotnet", "Umbraco9Docker.dll"]

This Dockerfile will do the following:

  • Copy the Umbraco9Docker.csproj to the src folder.
  • Restore the NuGet packages specified in Umbraco9Docker.csproj. Docker creates a layer after every step in the Dockerfile. By only copying the Umbraco9Docker.csproj file and then doing a restore, these two layers will only need to be recreated if the Umbraco9Docker.csproj changes. This speeds up the Docker build significantly if no change has been made to the Umbraco9Docker.csproj file.
  • Copy all the source code to the src folder.
  • Publish the .NET project in release mode and put the output at /app/publish.
  • Expose port 80 so the port can be mapped on the container host.
  • Set an environment variable with key 'ASPNETCORE_URLS' and value 'http://+:80'.
    This environment variable will instruct ASP.NET Core to host the application using HTTP on port 80. The environment variable can be overwritten by the container host.
  • Copy the publish output from the build image to the final image.
  • Configure dotnet Umbraco9Docker.dll as the entry point. When the image is run, the entry point will be executed inside the container.

Note: You can also expose port 443, but setting up HTTPS in Docker containers is a blog post of its own. Docker containers commonly expose port 80 and a reverse proxy/load balancer will decrypt/encrypt traffic for HTTPS.

Execute the following command to build your container image:

docker build -t umbraco9-image:latest .

This will build your container image and tag the image as 'umbraco9-image:latest'.
By tagging your image, you can run it by its tag later on.

Running the Umbraco 9 Docker image #

Execute the following command to run your image:

docker run --rm -t -p 80:80 umbraco9-image:latest

Here's what the arguments do:

  • --rm: clean up the container when the container is stopped
  • -t: Allocate a pseudo-tty. This will color code the output among things.
  • -p: Maps port 80 on the host to port 80 in the container
  • umbraco9-image:latest: specifies the container image to run

If you browse to http://localhost:80, you should see the website running and Umbraco will prompt you to install the CMS. Unfortunately, you also have to specify the connection string in the wizard. When you enter the connection string, it will be persisted in the appsettings.json file in the container, but when you destroy and recreate the container, the changes will be gone and you'll have to go through the wizard again.

Passing the connection string into the Docker container #

Instead, you should pass in the connection string some other way. ASP.NET Core's configuration system parses command-line arguments out of the box, so you can pass in the Umbraco connection string as an argument. First, store the connection string in a variable by running these commands:

echo -n Enter Umbraco database connection string: 
read -s umbracoDbDSN

This will prompt you for the connection string and put the entered value into the umbracoDbDSN variable.

Next, run this updated docker run command:

docker run --rm -t -p 80:80 \
    umbraco9-image:latest ConnectionStrings:umbracoDbDSN="$umbracoDbDSN"

Umbraco will look for a connection string named umbracoDbDSN by default.

Browse to http://localhost:80 again. If you haven't installed Umbraco into the database, it'll prompt you for your desired username and password, but not for a connection string.
After installing Umbraco on the database, you will be taken to the Umbraco back-office at http://localhost:80/umbraco

If you already had installed Umbraco into the database, you'll be greeted by a standard Umbraco splash page because you're running a blank Umbraco instance without any content or templates. Browse to http://localhost:80/umbraco to see the back-office.

Alternatively, you can also pass in the connection string using an environment variable.
Command-line arguments and environment variables get the job done, but there are more secure options to consider. Depending on which container host or container orchestrator you are using, you can pass in secrets as files. Once the secret files are in the container, you can use the Key-per-file configuration provider to parse them into the ASP.NET configuration. This is a common way to pass in secrets to a container that is used by Docker compose, Docker Swarm, Kubernetes, and many other container hosts.

Change Serilog to log to console #

Unfortunately, you cannot see the logs coming from the website, because out of the box, Serilog isn't configured to log to the console. For you to be able to see logs in docker, it needs to be logged to the console. Update the appsettings.json file by adding the "WriteTo" property to the "Serilog" object with the "Async" configuration below:

{
  "$schema": "./umbraco/config/appsettings-schema.json",
  "Serilog": {
    "MinimumLevel": {...},
    "WriteTo": [
      {
        "Name": "Async",
        "Args": {
          "configure": [
            {
              "Name": "Console"
            }
          ]
        }
      }
    ]
  },
  "ConnectionStrings": {...},
  "Umbraco": {...}
}

Rebuild the container image with the updated source code:

docker build -t umbraco9-image:latest .

Run the new image:

docker run --rm -t -p 80:80 \
    umbraco9-image:latest ConnectionStrings:umbracoDbDSN="$umbracoDbDSN"

You should now see all the logs from your running container.

Persisting file system using volumes and mounts #

Persisting media files #

When you upload files to Umbraco, by default, it will store the files somewhere in the wwwroot/media folder. But when a container is destroyed and recreated, the files uploaded to the wwwroot/media folder are lost. 

Note: If you are using a different file provider that stores the files outside of the container, this won't be an issue. 

You can use Docker volumes to persist these files even when a container is destroyed and recreated. The first way you could achieve this is by creating a named volume like this:

docker run --rm -t -p 80:80 \
     -v media-volume:/app/wwwroot/media \
    umbraco9-image:latest ConnectionStrings:umbracoDbDSN="$umbracoDbDSN"

The command will now also create a volume named 'media-volume' for the /app/wwwroot/media in the container. If you want to see the files inside this volume from the container host, you can use the following command to get the path to the volume on the host:

docker volume inspect media-volume
# output:
# [
#     {
#         "CreatedAt": "2021-11-23T22:44:48Z",
#         "Driver": "local",
#         "Labels": null,
#         "Mountpoint": "/var/lib/docker/volumes/media-volume/_data",
#         "Name": "media-volume",
#         "Options": null,
#         "Scope": "local"
#     }
# ]

The Mountpoint property will hold the path to your volume on your container host.

Alternatively, you can use another option to mount a source directory from the host to a destination directory in the container like this:

docker run --rm -t -p 80:80 \
    --mount type=bind,source="$(pwd)/wwwroot/media",destination=/app/wwwroot/media \
    umbraco9-image:latest ConnectionStrings:umbracoDbDSN="$umbracoDbDSN"

This will mount "$(pwd)/wwwroot/media" on the host where $(pwd) will be replaced with the current directory, and this folder will be mounted to the /app/wwwroot/media folder in the container. The advantage of this approach is that the uploaded files will also be part of your source code. If you don't want the uploaded files to be part of the source code, use the previous approach.

With both options, the files uploaded to the media folder will be persisted when the container is destroyed and recreated. 

Persisting log files #

The default log configuration in Umbraco is to log to the umbraco/Logs folder. You should turn off the default UmbracoFile sink if you have your own logging infrastructure. 
If you do use these log files, you will also need to persist them using volume mounts. You can update the Docker run command like this:

docker run --rm -t -p 80:80 \
    --mount type=bind,source="$(pwd)/umbraco/Logs",destination=/app/umbraco/Logs \
    --mount type=bind,source="$(pwd)/wwwroot/media",destination=/app/wwwroot/media \
    umbraco9-image:latest ConnectionStrings:umbracoDbDSN="$umbracoDbDSN"

Now you will be able to find the log files both in the container and in the host at umbraco/Logs.

Persisting Views, Scripts, and Styles #

Log and media files aren't the only way Umbraco interacts with the file system. In the Umbraco back-office, you can also create, update, and delete view files, script files, and style files.
You can simply ignore this if you don't use this feature when running Umbraco inside Docker containers.

Note: For the Umbraco websites I develop, I am the only one messing with the source code and I'll update the models, views, CSS, and JavaScript outside of the Docker container. So I don't really interact with these files from the Umbraco back-office and don't set up persistent volumes for them.

If you do need to support these back-office capabilities, you can add more mounts like this:

docker run --rm -t -p 80:80 \
    --mount type=bind,source="$(pwd)/umbraco/Logs",destination=/app/umbraco/Logs \
    --mount type=bind,source="$(pwd)/wwwroot/media",destination=/app/wwwroot/media \
    --mount type=bind,source="$(pwd)/wwwroot/css",destination=/app/wwwroot/css \
    --mount type=bind,source="$(pwd)/wwwroot/scripts",destination=/app/wwwroot/scripts \
    --mount type=bind,source="$(pwd)/Views",destination=/app/Views \
    umbraco9-image:latest ConnectionStrings:umbracoDbDSN="$umbracoDbDSN"

Persisting Models from ModelBuilder #

By default, Umbraco 9 will store the models generated from Document Types in-memory without storing them into files. But if you set the models mode to SourceCodeManual or SourceCodeAuto, the model C# files will be created at umbraco/models, or whichever directory you configure using the ModelsDirectory property. This means you should create another mount just like the others to persist these files.

Warning: When you're running the Docker container locally, using 'bind' mounts, the code will always be in sync, but if you're running the container elsewhere, the code in the container may drift away from your source code. Any changes to the views, CSS, JavaScript, and models should somehow find their way back into your source control system, or remove the capability to edit these source files altogether.

Next steps #

Now that your Umbraco 9 website is running in a Linux Docker container, you can run it wherever Docker containers are supported, which is pretty much everywhere. If you are an Azure cloud user like me, keep an eye out for more tutorials on how to take this container and host it using Azure App Service, Azure Container Instances, or Azure Container Apps.

Related Posts

Related Posts