0

There are two ways to run a dot .net core application on a Linux based container.

  1. Getting a base image which has .net core framework install (on top of Linux image) & then copy code using dotnet publish.

  2. Use a Linux base image and then copy the dot net runtime & code using dotnet publish --self-contained option.

    --self-contained [true|false]

    Publishes the .NET runtime with your application so the runtime doesn't need to be installed on the target machine. Default is true if a runtime identifier is specified and the project is an executable project (not a library project).

    Is there any advantage of using one option over other ?

SharpCoder
  • 18,279
  • 43
  • 153
  • 249
  • FWIW, https://learn.microsoft.com/en-us/dotnet/core/deploying/ has some pros and cons of each. – omajid Nov 23 '21 at 00:12

2 Answers2

0

The self-contained and trimmed app can be published in a container that only has runtime dependencies for dotnet without the need for the whole runtime. This will lead to much smaller images overall.

  • mcr.microsoft.com/dotnet/runtime:6.0-alpine -> 79.7MB
  • mcr.microsoft.com/dotnet/runtime-deps:6.0-alpine -> 10.1MB

For one of my apps:

  • dotnet publish -c Release -r linux-musl-x64 --self-contained false -> 30MB
  • dotnet publish -c Release -r linux-musl-x64 --self-contained true /p:PublishTrimmed=true -> 74.6MB

Final image:

  • Using runtime base: 120MB
  • Using runtime-deps base and trimming: 96.9MB

You can also cache the non-app binaries in a layer so when only app code changes only the layer with the app binaries will be pushed. Here is my setup for the whole context:

# Dockerfile-build

ARG BASE=mcr.microsoft.com/dotnet/sdk:6.0-alpine

FROM ${BASE} AS build
RUN apk add --no-cache rsync

WORKDIR /src
COPY *.sln .

COPY **/*.csproj ./
COPY **/**/*.csproj ./
COPY **/**/**/*.csproj ./
RUN dotnet sln list | grep ".csproj" | while read -r line; do mkdir -p $(dirname $line); mv $(basename $line) $(dirname $line); done;

WORKDIR /src

# The build has a memory leak if the proxy is not specified 
# https://stackoverflow.com/questions/72885244/net-6-building-solution-at-docker-conteiner-taking-a-long-time-and-consuming 
RUN export http_proxy=proxy:80
RUN export https_proxy=$http_proxy

COPY .cache* .cache
RUN --mount=type=cache,target=/root/.nuget/packages \
    test -d .cache && rsync -a .cache/ /root/.nuget/packages/ && rm -rf .cache && echo "Cache applied"; \
    dotnet restore -r linux-musl-x64

COPY . .
# We cannot use the -r flag for a global sln build https://github.com/dotnet/sdk/issues/14281#issuecomment-876510589
# The UnitTests project depends on all the other projects so this command builds the whole thing
RUN --mount=type=cache,target=/root/.nuget/packages \
    dotnet build -c Release -r linux-musl-x64 -f net6.0 --no-restore Tests/IntegrationTests

FROM build as test
RUN --mount=type=cache,target=/root/.nuget/packages \
    dotnet test -c Release -r linux-musl-x64 -f net6.0 --no-restore

FROM build as cache-prep
RUN --mount=type=cache,target=/root/.nuget/packages \
    mkdir -p /packages && cp -R /root/.nuget/packages/* /packages

FROM scratch as cache
COPY --from=cache-prep /packages .

# To not trigger the stages above
FROM build

# Dockerfile-publish

ARG BASE=build

FROM ${BASE}

ONBUILD ARG DIR
ONBUILD ARG APP_NAME

ONBUILD ARG OUT_DIR=./out
# Set to 0 to disable trimming and layered publish
ONBUILD ARG TRIM=1

ONBUILD WORKDIR /src/${DIR}/${APP_NAME}
ONBUILD RUN --mount=type=cache,target=/root/.nuget/packages \
    dotnet publish -c Release -r linux-musl-x64 --no-restore --no-dependencies \
    $(test ${TRIM} -eq 1 && echo '--self-contained -p:PublishTrimmed=true') -o ${OUT_DIR}

# Move the app binaries to a different folder so we can try to cache the dependencies in a layer. 
# That might not work for the trimmed build but if not much changed, this saves a lot of container space. 
ONBUILD RUN test ${TRIM} -eq 1 \
    && mkdir -p ./app-bin \
    && mv ${OUT_DIR}/${APP_NAME}* ./app-bin \
    && mv ${OUT_DIR}/Kernel* ./app-bin \
    || echo 0
# Dockerfile-runtime

ARG BASE=publish
ARG RUNTIME=mcr.microsoft.com/dotnet/runtime-deps:6.0-alpine

# Name the layer so it can be used in a COPY command
FROM ${BASE} as publish

# Build runtime image
FROM ${RUNTIME}

# As per https://www.abhith.net/blog/docker-sql-error-on-aspnet-core-alpine/
RUN apk add --no-cache icu-libs tzdata \
   && cp /usr/share/zoneinfo/Europe/Prague /etc/localtime

ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
ENV TZ="Europe/Prague"

RUN adduser --disabled-password --home /app --gecos '' dotnetuser && chown -R dotnetuser /app
USER dotnetuser

ONBUILD ARG DIR
ONBUILD ARG APP_NAME
ONBUILD ENV APP_NAME=${APP_NAME}

ONBUILD ARG OUT_DIR=./out

ONBUILD WORKDIR /app
# TODO: use this once https://github.com/moby/buildkit/issues/816 is resolved
# For now just add the COPY commands to the project dockerfile
#ONBUILD COPY --from=publish /src/${DIR}/${APP_NAME}/out .
#ONBUILD COPY --from=publish /src/${DIR}/${APP_NAME}/app-bin .

ONBUILD ENTRYPOINT "./${APP_NAME}"
# project Dockerfile

ARG DIR=Service
ARG APP_NAME=XXXService

ARG BUILD=build
ARG PUBLISH=publish
ARG RUNTIME=runtime


FROM ${BUILD} as build

FROM ${PUBLISH} as publish

FROM ${RUNTIME}
# TODO: use ONBUILD in `publish` once https://github.com/moby/buildkit/issues/816 is resolved
COPY --from=publish /src/${DIR}/${APP_NAME}/out .
COPY --from=publish /src/${DIR}/${APP_NAME}/app-bin .

To build and publish you simply prepare the base images by

docker build -t build -f Dockerfile-build .
docker build -t publish -f Dockerfile-publish .
docker build -t runtime -f Dockerfile-runtime .

you test the app by

docker build -f Dockerfile-build --target test .

you can cache the nuget dependencies by exporting them from the image (in a CI environment for example)

docker buildx create --name buildx || true
docker build -f Dockerfile-build --builder buildx --target cache -o type=local,dest=./.cache .

and you build the final image by

docker build -t "$TAG" Services/XXXService

Hope this helps a little

Heehaaw
  • 2,677
  • 17
  • 27
-2

Basically, you don't want self-contained as long as you don't need to. The package will stay as small as possible which is what you want. Only when you're not sure the targeting platform will be able to host your system because of missing runtime components, you can add these runtime components with the self-contained option.

You can install the dotnet runtime on several platforms including Linux. If you have control over doing so, I'd go for that option.

Eduard Keilholz
  • 820
  • 8
  • 27