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