left arrow

Software

2022-05-10

Deploying ASP.NET Core on a Raspberry Pi using Docker

Go to motivation

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.

Go to goal

Goal

Use CI/CD to automatically deploy and run the PUTS project on my Raspberry Pi.

Go to implementation

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-arm32v7
RUN apt-get update && apt -y install g++
WORKDIR /app
COPY published/ .
ENTRYPOINT ["dotnet", "PUTSWeb.dll"]
  • I need g++ in the container because PUTS uses g++ 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 is arm32v7 (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/putsweb
restart: always
ports:
- 8000:5001
volumes:
- keys:/app/Keys
- ~/.aspnet/https:/https:ro
depends_on:
- db
environment:
ConnectionStrings__ProblemDatabase: $ConnectionStrings__ProblemDatabase
ASPNETCORE_Kestrel__Certificates__Default__Password: $ASPNETCORE_Kestrel__Certificates__Default__Password
ASPNETCORE_Kestrel__Certificates__Default__Path: $ASPNETCORE_Kestrel__Certificates__Default__Path
ASPNETCORE_URLS: https://+:5001
db:
image: jsurf/rpi-mariadb
restart: always
ports:
- 8001:3306
volumes:
- db-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORD
adminer:
image: adminer
restart: always
ports:
- 8002:8080
volumes:
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 regular mysql 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:

  1. Build and publish Docker image
    1. Setup dotnet
    2. Build dotnet
    3. Publish dotnet
    4. Build Docker image
    5. Push Docker image to Docker Hub
  2. Deploy on Raspberry Pi
    1. Copy stack file over to the Pi
    2. Compose pull
    3. 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: PUTSWeb
on:
push:
branches: [master]
workflow_dispatch:
jobs:
docker-image:
name: Build and publish docker image
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./PUTSWeb
steps:
- name: Checkout 🛎️
uses: actions/checkout@v3
- name: Setup .NET 🌐
uses: actions/setup-dotnet@v2
with:
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: buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub 🐋
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and push ⬆️
uses: docker/build-push-action@v2
with:
context: ./PUTSWeb/
platforms: linux/arm/v7
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/putsweb:latest
deploy-rpi:
name: Deploy stack to raspberry pi
needs: docker-image
runs-on: ubuntu-latest
environment:
name: pi
url: http://benaspi.ddns.net:8000
steps:
- name: Checkout 🛎️
uses: actions/checkout@v3
- name: Copy stack file 📁
uses: appleboy/scp-action@master
with:
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@master
with:
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@master
with:
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@master
with:
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 and Buildx are needed to build the image since this runner is using ubuntu and I'm building an image for the arm32v7 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.

Go to raspberry-pi-as-a-runner

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