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.
Comments