Software
2022-05-10
Deploying ASP.NET Core on a Raspberry Pi using Docker
Motivation
I had a Raspberry Pi 3B and figured it would be a good machine to run my personal projects. I don't need a lot of computing power so it fits my needs nicely. I also had a project that I made a while ago called PUTS where the main part is an ASP.NET Core MVC project called PUTSWeb (I'll be using them interchangeably). Combining these two would certainly be an interesting learning experience.
Goal
Use CI/CD to automatically deploy and run the PUTS project on my Raspberry Pi.
Implementation
PUTS depends on a MySQL database to store information about users, problems and solutions meaning that I'd need to also deploy a database alongside PUTS. For that purpose, I chose to use the last missing part of this article – Docker. There's already an image for MySQL meaning that I could deploy the database using Docker and deploy PUTS regularly using dotnet, however, I decided to containerize PUTS and use Docker Compose as this is probably the most convenient approach for me.
Firstly I had to containerize PUTS. I decided to do all of the actual .NET building outside the Dockerfile in the CI/CD runner so that the image size is kept as small as possible. Because of this, the Dockerfile came out very small and simple:
FROM mcr.microsoft.com/dotnet/aspnet:2.1-bionic-arm32v7RUN apt-get update && apt -y install g++WORKDIR /appCOPY published/ .ENTRYPOINT ["dotnet", "PUTSWeb.dll"]
- I need
g++
in the container because PUTS usesg++
to compile the user submitted programs. - I'm using the
-bionic-arm32v7
version of the dotnet image because I'm making an image for a Raspberry Pi 3B and its architecture isarm32v7
(keep in mind that newer Raspberry Pis have a different one).
In theory, I could now build a docker image for PUTS (in practice I couldn't directly build an image for a different architecture) that I'll eventually push to Docker Hub. The next implementation step was to create a docker-compose
file which I refer to as rpi-stack.yml:
version: '3.1'services:puts:image: benasbudrys/putswebrestart: alwaysports:- 8000:5001volumes:- keys:/app/Keys- ~/.aspnet/https:/https:rodepends_on:- dbenvironment:ConnectionStrings__ProblemDatabase: $ConnectionStrings__ProblemDatabaseASPNETCORE_Kestrel__Certificates__Default__Password: $ASPNETCORE_Kestrel__Certificates__Default__PasswordASPNETCORE_Kestrel__Certificates__Default__Path: $ASPNETCORE_Kestrel__Certificates__Default__PathASPNETCORE_URLS: https://+:5001db:image: jsurf/rpi-mariadbrestart: alwaysports:- 8001:3306volumes:- db-data:/var/lib/mysqlenvironment:MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORDadminer:image: adminerrestart: alwaysports:- 8002:8080volumes:db-data:keys:
There are a couple of things worth noting about this compose file:
- The service
adminer
is completely optional, I just wanted something to conveniently check my database. - I'm using
jsurf/rpi-mariadb
instead of a regularmysql
image for the database because this one is suited for a Raspberry Pi. - Ports 8000, 8001 and 8002 are completely arbitrary
- There are several environment variables that will be passed in the compose file via GitHub environment variables.
- There are project specific volumes for ASP.NET Core Identity and HTTPS certificate.
I now had a Dockerfile and a compose/stack file for my project, all I needed now is to somehow get everything onto the Raspberry Pi and compose up the stack. It's finally time for the pipeline which consists of these logical steps:
- Build and publish Docker image
- Setup dotnet
- Build dotnet
- Publish dotnet
- Build Docker image
- Push Docker image to Docker Hub
- Deploy on Raspberry Pi
- Copy stack file over to the Pi
- Compose pull
- Compose up
I store all of my personal projects on my GitHub so I'll be using Github Actions to create a CI/CD pipeline. The pipeline I'm currently using came out like this (don't be discouraged by quite a few steps):
name: PUTSWebon:push:branches: [master]workflow_dispatch:jobs:docker-image:name: Build and publish docker imageruns-on: ubuntu-latestdefaults:run:working-directory: ./PUTSWebsteps:- name: Checkout 🛎️uses: actions/checkout@v3- name: Setup .NET 🌐uses: actions/setup-dotnet@v2with:dotnet-version: 2.1- name: Build dotnet 🔧run: dotnet build- name: Publish 📦run: dotnet publish -c Release -o published- name: Set up QEMU 🔨uses: docker/setup-qemu-action@v1- name: Set up Docker Buildx 🪛id: buildxuses: docker/setup-buildx-action@v1- name: Login to Docker Hub 🐋uses: docker/login-action@v1with:username: ${{ secrets.DOCKER_HUB_USERNAME }}password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}- name: Build and push ⬆️uses: docker/build-push-action@v2with:context: ./PUTSWeb/platforms: linux/arm/v7push: truetags: ${{ secrets.DOCKER_HUB_USERNAME }}/putsweb:latestdeploy-rpi:name: Deploy stack to raspberry pineeds: docker-imageruns-on: ubuntu-latestenvironment:name: piurl: http://benaspi.ddns.net:8000steps:- name: Checkout 🛎️uses: actions/checkout@v3- name: Copy stack file 📁uses: appleboy/scp-action@masterwith:host: ${{ secrets.RPI_HOST }}username: ${{ secrets.RPI_USERNAME }}password: ${{ secrets.RPI_PASSWORD }}source: 'Infrastructure/rpi-stack.yml'target: '~/stacks/putsweb/'strip_components: 1- name: Compose pull ⬇️uses: appleboy/ssh-action@masterwith:host: ${{ secrets.RPI_HOST }}username: ${{ secrets.RPI_USERNAME }}password: ${{ secrets.RPI_PASSWORD }}script: docker-compose -f ~/stacks/putsweb/rpi-stack.yml pull- name: Compose up 🚀uses: appleboy/ssh-action@masterwith:host: ${{ secrets.RPI_HOST }}username: ${{ secrets.RPI_USERNAME }}password: ${{ secrets.RPI_PASSWORD }}script: |export ConnectionStrings__ProblemDatabase="${{ secrets.CONNECTIONSTRINGS__PROBLEMDATABASE }}"export MYSQL_ROOT_PASSWORD="${{ secrets.MYSQL_ROOT_PASSWORD }}"export ASPNETCORE_Kestrel__Certificates__Default__Password="${{ secrets.ASPNETCORE_KESTREL__CERTIFICATES__DEFAULT__PASSWORD }}"export ASPNETCORE_Kestrel__Certificates__Default__Path="${{ secrets.ASPNETCORE_KESTREL__CERTIFICATES__DEFAULT__PATH }}"docker-compose -f ~/stacks/putsweb/rpi-stack.yml up -d- name: Check running containers 🔍uses: appleboy/ssh-action@masterwith:host: ${{ secrets.RPI_HOST }}username: ${{ secrets.RPI_USERNAME }}password: ${{ secrets.RPI_PASSWORD }}script: docker ps -a
Some comments about the pipeline:
- It makes use of other pre-built GitHub Actions (as it should) to for example copy over the stack file to the Raspberry Pi.
QEMU
andBuildx
are needed to build the image since this runner is using ubuntu and I'm building an image for thearm32v7
architecture.- All sensitive data is handled by GitHub repository secrets and environment secrets.
With everything in place, I was able to achieve what I wanted – upon PUTS code changes, a pipeline starts automatically, builds a Docker image for the PUTS project, pushes it to the Docker Hub and deploys the stack on the Raspberry Pi. What I like is that most of the things related to this project live in the repository together with PUTS code – I can see and change the stack file, update the pipeline etc.
Raspberry Pi as a runner
I also experimented a bit with the idea of having the Raspberry Pi act as a GitHub Actions runner. However, I scrapped this idea because I didn't want to put additional load on the Pi itself and the runners provided by GitHub have more resources/are faster.
Just a heads up that the actual progress wasn't as straightforward as I've described in this article and I've discovered a lot of things with trial and error