How to Deploy .NET Applications Continuously in Docker Without Pipelines

·

3 min read

Due to requirements in the workplace for a specific application where the staging/testing server wasn’t able to be added into the deployment group of a pipeline (security risk for on-premise server access) I had to find a way to have a container automatically update itself from a GIT repository.

I had tried a few different methods of achieving this, most ended up unnecessarily bringing the service down to rebuild or adding complications, even if there was no changes to the code:

  • Automatically pulling and rebuilding the docker-compose every x minutes (this lead to cached GIT code and not a fresh build)

  • Using a separate container as a cron job, to pull the code into the main one and rebuild as needed (this was difficult to coordinate and would sometimes leave the main image in a crashed state during build)

The solution that I ended up with was to insert the exact hash of the latest GIT commit into the Dockerfile, to ensure that it would not use the image cache and pull the latest code correctly. Since it was also a docker-compose file it wouldn’t affect the main image if the build process failed (this will need extra monitoring though as you might get a drift from the latest working code)

The process is as follows:

  1. Create a separate Dockerfile to use as a template, with a placeholder variable for the GIT hash

    1. As part of the build process, place the hash of the GIT repo into /app/revision.txt to compare against later
  2. Make a Bash script to:

    1. Fetch the latest hash of the GIT Head (or specific branch)

    2. Compare against the last used hash (stored in a text file)

    3. If they differ, copy the Dockerfile, replace the hash and rebuild the container

    4. Prune old images

  3. Set the script to run on a schedule (using Cron in my case)

Below is the code used, the Dockerfile will be specific to a .NET 8 web application for the build and run process

DockerfileHead

#-----------------------------------------------------------------------
# https://github.com/dotnet/dotnet-docker/blob/main/samples/README.md
#-----------------------------------------------------------------------

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /source

# Install dependencies
RUN apt-get -y update && apt-get install -y \
git

# Clone Repo
RUN git clone --branch master "https://user:password@github.com/REPO/PROJECT.git" /source && git reset --hard a9828ea43d862f94a518d3eded4aae31e285b320
# Add manual files
COPY appsettings.json /source
RUN dotnet restore PROJECT/PROJECT.csproj

# copy and publish app and libraries
RUN dotnet publish PROJECT/PROJECT.csproj --no-restore -o /app

# Output hash of commit to text file to check for updates
RUN git rev-parse HEAD > /app/revision.txt

# Update database
COPY appsettings.json /source/PROJECT
ENV PATH="{$PATH}:/root/.dotnet/tools"
RUN dotnet tool install --global dotnet-ef --version 8.*
RUN dotnet ef database update --project PROJECT

# Final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:8.0
EXPOSE 8080
WORKDIR /app
COPY --from=build /app .
COPY appsettings.json /app
ENTRYPOINT ["./PROJECT"]

Update.sh

#-----------------------------------------------------------------------
# To setup the file
# crontab -e
# Minute Hour Day Month DayOfWeek Command
#
# */10 * * * * /Docker/PROJECT/update.sh >> /var/log/PROJECT.log 2>&1
#-----------------------------------------------------------------------

# Make sure we're in the right folder
cd /Docker/PROJECT

# Fetch the hash used in the latest Docker image
docker cp $(docker ps --filter "name=$(basename "$PWD")" -lq):/app/revision.txt .

# Fetch hash of latest revision
remote=$(git ls-remote "https://user:password@github.com/REPO/PROJECT.git" --abbrev-ref HEAD | grep -o -P "^[a-zA-Z0-9]{0,}")

if [ -e revision.txt ]; then
    local=$(cat revision.txt)
else
    local="a"
fi
echo "local |"$local"|"
echo "remote|"$remote"|"

if [ $local != $remote ]; then
    # Force rebuild, replace "HASH" with the latest GIT hash
    cp -rf DockerfileHead Dockerfile
    sed -i -e 's/HASH/'$remote'/g' Dockerfile
    docker compose up --build -d
    # Clean up stale images (WARNING, this will remove all non-running images)
    docker image prune -f -a
fi

Once that is all tested and running, you can add it into the Crontab file as below to automatically run every x minutes or as needed: (use “crontab -e” to edit this file)

*/10 * * * * /Docker/PROJECT/update.sh >> /var/log/PROJECT.log 2>&1