Abstract


Azure App Service expanded its support to include Linux and containers in 2017. This update brought similar functionalities to the classic App Service on Windows Server. Both versions allow you to run various types of applications, such as .NET, Java, NodeJS, PHP, and Ruby, with features like continuous integration and deployment (CI/CD) and automatic scaling. To illustrate the workings of Azure App Service, this post will focus on an ASP.NET Core application as an example.

A Simple Test 


I created a new web app using Code deployment on Linux. Notice, I am NOT choosing Docker Container here. 

Code vs Docker Container

The difference between Code and Docker Container is that Code deployment doesn't require you to have a Dockerfile and build your docker image before publishing, while Docker Container requires your image to be ready before publishing to Azure App Service, which means you need to write a Dockerfile for your website and also build the image, push it to Docker Hub or other container registries.

A typical ASP.NET Core application that uses Docker will have a Dockerfile look like this:

FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
LABEL maintainer="edi.wang@outlook.com"
LABEL repo="https://github.com/EdiWang/Elf"

WORKDIR /app
EXPOSE 80
EXPOSE 443

ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true

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

FROM build AS publish
RUN dotnet publish "Elf.Api.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Elf.Api.dll"]

And, because you need to build your own image when using Docker Container option, this means the Runtime stack is also included in your image, so Azure won't let you choose the program runtime here. When use Code deployment, you only need to build the code, in my case, I am using ASP.NET Core 6.0, Azure manages the runtime for me, so Azure will need to setup the ASP.NET Core 6.0 runtime ready to run my code.

Detecting container

This application I wrote is just to detect if the current process is running in Docker as mentioned in this Microsoft document.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var isInDocker = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") ?? false.ToString();

app.MapGet("/", () => $"DOTNET_RUNNING_IN_CONTAINER: {isInDocker}");

app.Run();

And it doesn't have a Dockerfile

When running on my local machine without docker, it shows False

Now, I publish this code to the Azure App Service Linux instance I created before from Visual Studio.

As the output indicates, this ASP.NET Core application is running in Docker.

As we can see, even if I did not choose "Docker Container" when creating Azure App Service, my code still runs in Docker. I don't have to build a Docker image for my application, I don't need to learn Docker, I don't even need to be aware of the Docker underlying, Azure manages the runtime for me. 

Explore the underlying Docker infrastructure


To future explore how it works. I use the Kudu tool that integrated in every Azure App Service resource. 

Logs

From the landing page, I can download "Current Docker logs".

In the "Environment" menu, I can also find a few information about the Docker version Microsoft is using, and even Azure App Service's SSH password.

Docker log file shows how Azure App Service is using Docker to run my application.

2023-08-23T01:47:56.969Z INFO  - Logging is not enabled for this container.
Please use https://aka.ms/linux-diagnostics to enable logging to see container logs here.
2023-08-23T01:48:04.719Z INFO  - Initiating warmup request to container linux996_3_f13b8c09 for site linux996
2023-08-23T01:48:24.563Z INFO  - Container linux996_3_f13b8c09 for site linux996 initialized successfully and is ready to serve requests.
2023-08-23T01:48:25.195Z INFO  - Starting container for site
2023-08-23T01:48:25.213Z INFO  - docker run -d --expose=8080 --name linux996_4_ed97a4fb -e WEBSITE_SITE_NAME=linux996 -e WEBSITE_AUTH_ENABLED=False -e WEBSITE_ROLE_INSTANCE_ID=0 -e WEBSITE_HOSTNAME=linux996.azurewebsites.net -e WEBSITE_INSTANCE_ID=90f7789cd4cc1d5a4fe2a8c0e9db0d4e3d0c73856fa77c2d8c30202608ce99f6 -e ASPNETCORE_HOSTINGSTARTUPASSEMBLIES=Microsoft.ApplicationInsights.StartupBootstrapper -e DOTNET_STARTUP_HOOKS=/agents/core/StartupHook/Microsoft.ApplicationInsights.StartupHook.dll -e WEBSITE_USE_DIAGNOSTIC_SERVER=True appsvc/dotnetcore:6.0_20230726.2.tuxprod dotnet DockerDetect.dll 

2023-08-23T01:48:25.214Z INFO  - Logging is not enabled for this container.
Please use https://aka.ms/linux-diagnostics to enable logging to see container logs here.
2023-08-23T01:48:33.368Z INFO  - Initiating warmup request to container linux996_4_ed97a4fb for site linux996
2023-08-23T01:48:45.009Z INFO  - Container linux996_4_ed97a4fb for site linux996 initialized successfully and is ready to serve requests.

The Docker image

From the log, I can see it is using a Docker image named appsvc/dotnetcore

This used to be published in Docker Hub (https://hub.docker.com/r/appsvc/dotnetcore/tags), but it hasn't been updated for 2 years. So is its code on GitHub (https://github.com/Azure-App-Service/dotnetcore). The tag from the log is 6.0_20230726.2.tuxprod, it isn't on the outdated Docker Hub image. After some digging, it is moved to Microsoft Artifact Registry (mcr.microsoft.com), which can be pulled from anywhere:

docker pull mcr.microsoft.com/appsvc/dotnetcore:6.0_20230726.2.tuxprod

How does it run my code

This appsvc/dotnetcore image is only the ASP.NET Core 6.0 runtime, it doesn't include the application files used by Azure customers. My code isn't in the image, how does it run my application?

From the log above, I can see a parameter that starts my application

dotnet DockerDetect.dll 

This is the exact same command used by .NET runtime to run any ASP.NET Core application.

Now, go deeper into the image itself to find it's Entrypoint:

docker inspect mcr.microsoft.com/appsvc/dotnetcore:6.0_20230726.2.tuxprod

Gives 

"WorkingDir": "/home/site/wwwroot",
"Entrypoint": [
    "/bin/init_container.sh"
],

It also shows how "DOTNET_RUNNING_IN_CONTAINER" environment variable is set to true, which is what I used to determine a Docker environment in the previous section.

"Env": [
    "PATH=/opt/dotnetcore-tools:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/site/wwwroot",
    "ASPNETCORE_URLS=",
    "DOTNET_RUNNING_IN_CONTAINER=true",
    "USER_DOTNET_AI_VERSION=2.8.42",
    "ORYX_AI_CONNECTION_STRING=InstrumentationKey=4aadba6b-30c8-42db-9b93-024d5c62b887",
    "DOTNET_VERSION=6.0",
    "ASPNETCORE_LOGGING__CONSOLE__DISABLECOLORS=true",
    "PATH_CA_CERTIFICATE=/etc/ssl/certs/ca-certificate.crt",
    "LANG=C.UTF-8",
    "LANGUAGE=C.UTF-8",
    "LC_ALL=C.UTF-8",
    "CNB_STACK_ID=oryx.stacks.skeleton",
    "PORT=8080",
    "SSH_PORT=2222",
    "WEBSITE_ROLE_INSTANCE_ID=localRoleInstance",
    "WEBSITE_INSTANCE_ID=localInstance",
    "ASPNETCORE_FORWARDEDHEADERS_ENABLED=true",
    "HOME=/home"
]

Run the image on my local machine to see the content of init_container.sh

docker run -it --rm --entrypoint "bash" mcr.microsoft.com/appsvc/dotnetcore:6.0_20230726.2.tuxprod
cat /bin/init_container.sh

From this file, we can see the startup command location

startupCommandPath="/opt/startup/startup.sh"

But this file is created on the fly, so we can't cat this currently. It is generated by generate_and_execute_startup_script.sh

# Dockerfile copies generate_and_execute_startup_script.sh from /templates/startup_scripts directory based on assetDirs in tuxib.yml
source /bin/generate_and_execute_startup_script.sh

This file is simple. We can see it is calling Oryx to generate startup command. 

Oryx is a build system which automatically compiles source code repos into runnable artifacts. It is used to build web apps for Azure App Service and other platforms.

#!/bin/bash

oryxArgs="create-script -appPath $appPath -output $startupCommandPath -defaultAppFilePath $defaultAppPath \
    -bindPort $PORT -bindPort2 '$HTTP20_ONLY_PORT' -userStartupCommand '$userStartupCommand' $runFromPathArg"

echo "Running oryx $oryxArgs"
eval oryx $oryxArgs
exec $startupCommandPath

I don't want to work 996 to test the script generation on my local environment. Instead, the generated script can be found from the web SSH in Kudu.

As you can see, the script is finally calling dotnet DockerDetect.dll, which is from the docker run parameter that Azure provided.

What else is in the image

Labels that show its build information.

"Labels": {
    "com.microsoft.oryx.build-number": "20230721.1",
    "com.microsoft.oryx.git-commit": "e01b34550f75a38df92af2041687cb2b0ad41ec0",
    "com.microsoft.oryx.release-tag-name": "20230721.1",
    "io.buildpacks.stack.id": "oryx.stacks.skeleton",
    "maintainer": "Azure App Services Container Images <appsvc-images@microsoft.com>"
}

A bunch of .NET tools are also available inside the image. App Service can use these tools to monitor and trouble shoot the application.

root@55a2f2c9e303:/opt# ls
dotnetcore-tools  startup  startupcmdgen
root@55a2f2c9e303:/opt# cd dotnetcore-tools/
root@55a2f2c9e303:/opt/dotnetcore-tools# ls -la
total 856
drwxr-xr-x 3 root root   4096 Jul 21 01:34 .
drwxr-xr-x 1 root root   4096 Jul 26 11:53 ..
drwxr-xr-x 9 root root   4096 Jul 21 01:34 .store
-rwxr-xr-x 1 root root 142840 Jul 21 01:34 dotnet-counters
-rwxr-xr-x 1 root root 142840 Jul 21 01:33 dotnet-dump
-rwxr-xr-x 1 root root 142840 Jul 21 01:34 dotnet-gcdump
-rwxr--r-- 1 root root 142840 Jul  6 20:17 dotnet-monitor
-rwxr-xr-x 1 root root 142840 Jul 21 01:33 dotnet-sos
-rwxr-xr-x 1 root root 142840 Jul 21 01:33 dotnet-trace

.NET runtime is located in /usr/share/dotnet

root@55a2f2c9e303:/usr/share/dotnet# ls -la
total 244
drwxrwxr-x 4 root root   4096 Jun 20 19:39 .
drwxr-xr-x 1 root root   4096 Jul 26 11:53 ..
-rw-rw-r-- 1 root root   1116 Jun 20 19:31 LICENSE.txt
-rw-rw-r-- 1 root root  78479 Jun 20 19:31 ThirdPartyNotices.txt
-rwxr-xr-x 1 root root 141760 Jun 20 19:33 dotnet
drwxrwxr-x 3 root root   4096 Jun 20 19:39 host
drwxrwxr-x 4 root root   4096 Jul 21 01:34 shared

Default application that shows a welcome page of a new created App Service instance is located in /defaulthome/hostingstart

root@55a2f2c9e303:/defaulthome/hostingstart# ls -la
total 296
drwxr-xr-x 1 root root   4096 Jul 26 11:53 .
drwxr-xr-x 1 root root   4096 Jul 26 11:53 ..
-rwxr-xr-x 1 root root 142760 May 10  2021 hostingstart
-rw-r--r-- 1 root root 103261 May 10  2021 hostingstart.deps.json
-rw-r--r-- 1 root root   5632 May 10  2021 hostingstart.dll
-rw-r--r-- 1 root root  18756 May 10  2021 hostingstart.pdb
-rw-r--r-- 1 root root    311 May 10  2021 hostingstart.runtimeconfig.json
-rw-r--r-- 1 root root    488 May 10  2021 web.config
drwxr-xr-x 2 root root   4096 Jul 26 11:53 wwwroot
root@55a2f2c9e303:/defaulthome/hostingstart# cd wwwroot/
root@55a2f2c9e303:/defaulthome/hostingstart/wwwroot# ls
hostingstart.html

A part of the HTML code

<div class="pl-5 ml-5 col-xl-5 col-lg-5 col-md-10 col-sm-11 col-xs-11">
    <div class="container-fluid">
        <div class="row">
            <h2 id="upRunning">Your web app is running and waiting for your content</h2>
        </div>
        <div class="row mt-4 pt-4">
            <div id="appIsLive" style="font-size:16px;width: 516px;">Your web app is live, but we don’t have
                your content yet. If you’ve already deployed, it could take up to 5 minutes for your content
                to show up, so come back soon.</div>
        </div>
        <div class="row mt-4">
            <h5 id="supporting" class="mt-5"><img height="50" width="50"
                    src="https://appservice.azureedge.net/images/linux-landing-page/v4/built-dotnet.svg" /> Built with .NET</h5>
        </div>
    </div>
</div>

Application files

The final puzzle is how it find my DockerDetect.dll, it took some tricks to find my application file. Just ls in the default SSH landing directory won't show anything. 

Summary


In this post, I created an ASP.NET Core 6.0 web application and deployed it to Azure App Service for Linux. Azure is using a Docker image to run my code. The image contains ASP.NET Core runtime and uses Oryx to generate a startup script that finally calls dotnet to run my application files.