Containerizing a C# hello world app

I've always wanted to have my own little Kubernetes cluster somewhere and I finally decided to create one with Civo. But I had no idea what to run on it. So, this little post is about how I went about creating a hello-world application to run on my cluster!

If you only want to see the code, here you go.

The application itself is a simple C# file inspired by Jess Frazelle's much cooler party-clippy:

const string CLIPPY = @"         
        
         __                 
        /  \        _____________ 
        |  |       /             \
        @  @       | It looks    |
        || ||      | like you    |
        || ||   <--| are very    |
        |\_/|      | bored.      |
        \___/      \_____________/             
          
";

var app = WebApplication.CreateBuilder(args).Build();
app.MapFallback(() => CLIPPY);
app.Run();
main.cs

Next, I put together a Dockerfile. After some googling around, I decided to use the ubuntu chiseled docker image ubuntu/dotnet-deps. I've only ever used alpine or debian for C# applications, so this was a first!

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env
WORKDIR /app

COPY ./src ./src
RUN dotnet publish -c Release -r linux-x64 --self-contained -o /app/out ./src/clippy.csproj

FROM ubuntu/dotnet-deps:7.0_edge AS final

LABEL org.opencontainers.image.source="https://github.com/gldraphael/clippy"
LABEL org.opencontainers.image.description="A simple hello world application."

ENV \
    # Configure web servers to bind to port 80 when present
    ASPNETCORE_URLS=http://+:80      \
    # Enable detection of running in a container
    DOTNET_RUNNING_IN_CONTAINER=true \
    # Disable globalization
    DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1

EXPOSE 80

WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["./clippy"]
Dockerfile

At this point, I had something I could run in a container.

Now that I had a working docker image, I wanted to be able to publish this to GitHub packages using GitHub actions. This bit was actually the easiest. I literally just pasted this example from the documentation, and it just worked!

# pull the latest image
docker pull ghcr.io/gldraphael/clippy:main

# run it in the background
docker run -it -d -p 8080:80 --name clippy --rm ghcr.io/gldraphael/clippy:main

# curl it
curl localhost:8080

# once you're done, stop it to delete it
docker stop clippy

This also happens to be the first time I've ever used GitHub packages. Another first. Yay!

Next, I wanted a helm chart. Creating the chart itself was easy. helm create chart did it. I only had to go in and update a few things (like change the app name from chart to clippy, update the image to use, and so on).

Trouble was, I wasn't even sure if GitHub container registry supported OCI artifacts yet. Lucky for me, Niklas Metje documented this on his blog. I just had to figure out the workflow file. I ended up using Stefan Prodan's podinfo project as an example and decided to go with something like this:

name: Push helm chart

on:
  push:
    branches: ['main']

env:
  REGISTRY: ghcr.io

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Setup Helm
        uses: azure/setup-helm@v3
        with:
          version: v3.11.3

      - name: Log in to the Container registry
        uses: docker/login-action@v2
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Publish Helm chart to GHCR
        run: |
          helm package chart
          export CHART_VERSION=$(grep 'version:' ./chart/Chart.yaml | tail -n1 | awk '{ print $2 }')
          helm push clippy-$CHART_VERSION.tgz oci://${{ env.REGISTRY }}/gldraphael/charts
          rm clippy-$CHART_VERSION.tgz

The action seems to push a chart ok, but I'm yet to actually run this on a cluster. Next weekend!