Lets asume we are building a .Net application with docker. We will be using the SDK image for building and the runtime image as base or our application container.

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS BUILD-WEB-APP
ENV DEBIAN_FRONTEND noninteractive
ENV TZ=Europe/Amsterdam
COPY my-build-script /app/
RUN ./my-build-script

FROM mcr.microsoft.com/dotnet/aspnet:8.0-bookworm-slim-amd64
ENV DEBIAN_FRONTEND noninteractive
ENV TZ=Europe/Amsterdam
# Its easy to install additional packages use apt-get, cause its debian based
#RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \
        apt-get update && apt-get -y install imagemagick file libc6-dev libtiff-tools
COPY appsettings.json /app/appsettings.json
COPY --from=BUILD-WEB-APP /opt/data/bin /app
ENTRYPOINT ["dotnet", "my-app.dll"]

We will build the image by calling docker buildx build. This should make sure that image isn't just for x86, but also Arm. I'm using a M1 machine. But in some cases, running the container on x86 didn't work, so for now the --platform argument is added to force the correct processor. This isn't required in most cases! I just added this for when someone runs into the same issues.

docker buildx build --platform "${PLATFORM}" -f "${DOCKERFILE}" -t "${IMAGE}" .

I use :latest as tag. Latest isn't some magic tool in Docker. Its just a tag like any other version number, or what ever you use it for. Latest for me is just informative to the latest build. If I would use a version number, I would have to update my docker-compose.yml on every build. This is not what I want.

So, how do we transfer the image to the correct server? Its easy to save the image to a file. Using gzip it gets compressed.

docker save "${IMAGE}" | gzip -c > "${OUTPUT}"

Then copy the image to the correct server. I use gzipcat to extra the data and pipe it to the other server. This way the image doesn't need to be copied to the remote disk, before it can be loaded. If this is the best approach, is debateble.

gzipcat "${OUTPUT}" | ssh "${REMOTEHOST}" "${DOCKER} load"
ssh "${REMOTEHOST}" "cd ${DOCKER-COMPOSE-PATH}; docker compose up -d"

In this case, the docker-compose.yml should reference the same image name as the build used. On calling docker load, the tag of the current running image will be reset, so the new image can take the name with the latest tag.