Packaging a .NET Core Service for Ubuntu

This is a step-by-step guide on how to package a .NET Core 2.1 service for Ubuntu 18.04 LTS, but most of the concepts explored here should work for other versions of .NET Core and/or any Debian-based Linux distribution.

Photo by Kira auf der Heide on Unsplash

Building a demo API on Linux

We will start the tutorial by building a very simple API on Linux and the first step is to install the .NET Core SDK on our working environment following the instructions from the official website:

$ wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb
$ sudo dpkg -i packages-microsoft-prod.deb
$ sudo apt-get install apt-transport-https
$ sudo apt-get update
$ sudo apt-get install dotnet-sdk-2.1

Next, we will create a very simple application using the provided webapi template:

$ dotnet new webapi -o demoapi
$ cd demoapi
$ dotnet run

That was easy! Now the sample application should be up and running on your system.

Before we complete this task, we are going to make a couple of modifications to the source code to make the packaging process less complicated. First, we are going to make the application self-contained so all the dependencies of our service are included in the package and are isolated from other applications installed in the system. This saves us from the trouble of distributing the .NET Core libraries and the .NET Core runtime.

For this purpose we create a <RuntimeIdentifiers> tag in the <PropertyGroup> section of your demoapi.csproj file that defines the runtime identifier of our service targets, that is: ubuntu.18.04-x64:

<PropertyGroup>
<RuntimeIdentifiers>ubuntu.18.04-x64</RuntimeIdentifiers>
</PropertyGroup>

Also, to further reduce the size of the deployment we are going to the use globalization invariant mode for NET Core, which also has the advantage to remove the dependency from libicu. To activate it, we also add the following section to the csproj:

<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Globalization.Invariant" Value="true" />
</ItemGroup>

The final version of the demoapi.csproj should look like this:

Now we can run the following command to build the self-contained service:

$ dotnet publish -c Release --self-contained -r ubuntu.18.04-x64

Once the project is built, the generated binaries are located in: bin/Release/netcoreapp2.1/ubuntu.18.04-x64/publish/

The PDBs for the service code are already generated in the publish directory mentioned above, but we might want to debug the service in production or analyse performance issues, in this case is good have the symbols for the .NET Framework and the native runtime DLLs (typically libcoreclr.so).

Fortunately, .NET Core 2.1 comes with an excellent symbols download tool named dotnet-symbols that makes our lives very easy (note: you may need to restart the shell after the first command):

$ dotnet tool install -g dotnet-symbol
$ dotnet-symbol --symbols bin/Release/netcoreapp2.1/ubuntu.18.04-x64/publish/

Packaging the demo API

The first step to package our application will be to install and configure the Ubuntu packaging tools, additionally we will set two environment variables that the tools will use later on to customise it:

$ sudo apt install gnupg pbuilder ubuntu-dev-tools apt-file dh-make bzr-builddeb
$ export DEBEMAIL="service@bluekiri.com"
$ export DEBFULLNAME="Bluekiri Demo"

Now we have to tidy up the source code in order to create a clean source package:

$ cd demoapi
$ dotnet clean
$ rm -rf bin obj
$ cd ..

For the tools to work, you should make sure the source directory is in the format <name>-<version>. This is only necessary the first time we run the packaging tools and we can revert to the original name afterwards (yes, it’s possible to override the requirement, but we’ll follow the best practises here). Then we can create the compressed package with the source code of the application:

$ mv demoapi demoapi-1.0
$ tar cvzf demoapi-1.0.tar.gz demoapi-1.0

At this point, we have everything ready to run the helper to prepare a Debian package for our original source archive. The dh_make tool has many options, here we are going to specify that we have already created our own source file, we want to create a single binary native package and that we are going to use the MIT license for it:

$ cd demoapi-1.0
$ dh_make -f ../demoapi-1.0.tar.gz -s -c mit -n

The main things to notice is that the command has generated a new folder named debian on the top level directory of your application. We are going to spend the rest of this section customising its contents.

We’ll start by deleting some of the optional example files and documentation that we are not going to use in our project:

$ cd debian
$ rm *ex *EX
$ rm README README.Debian README.source demoapi-docs.docs

The remaining files are all important.

Our first task will be to modify the changelog file. We will edit it to change the version number to an Ubuntu version: 1.0–0ubuntu1 (upstream version 1.0, Debian version 0, Ubuntu version 1). Also change unstable to the current development Ubuntu release to bionic. The final result should look very similar to this:

One of your future tasks is to mantain this changelog file up-to-date for any new version of the package. We can edit the file manually or use debchange this purpose.

The very important control file contains all the metadata of the package. The first paragraph describes the source package. The second and following paragraphs describe the binary packages to be built.

In our version of this file we are going to define the metadata for our main package and the debug package. Please note that the debug package depends on the main package and that the main package has all the binary dependencies needed to run .NET Core in Linux, as the runtime is embedded in your own application. Also notice that one of the build dependencies of the source package is dh-systemd, a helper to generate a systemd service.

The last file, rules, is the most complex of all of them. This one is basically a Makefile which compiles the code and turns it into a binary package.

In our version, we override some of the build helpers processes to build and install the application. We have seen the build process in the previous section, so in this file we just launch the same command.

To install the the application, we copy both the application files and the symbols to the /opt/bluekiri/demoapi fakeroot of both packages.

The remaining overrides are for tools that are intended to run for other programming languages and are not compatible with our .NET Core application.

Finally, we have added a very simple demoapi.service file that systemd will use to control our application. You will need a better version for a production ready service (specially from a security point of view), but this one will do the trick.

With all these files and modifications in place, we can now build the package with the dpkg-buildpackage command. Use use the options to generate only the binary files and skip the signing of the package:

$ dpkg-buildpackage -b --no-sign

The result packages will be generated in the parent directory of the current location. You can install the generated package in the following way:

$ cd ..
$ sudo dpkg -i demoapi_1.0-0ubuntu1_amd64.deb

You can find the full source code of this demo api in the following location: https://github.com/bluekiri/demoapi

Creating a private Debian package repository

If you followed all the steps in the previous section, you should now have a debian package with your application. Now we are going to take a look on how to distribute the application using a private repository. In this tutorial, we are going to first create a local repository and then upload it to the well know github service. The later is just for convenience.

First we are going to install the prerequisties and then generate a private key using GnuPG that users will later use to verify the integrity of the packages.

$ sudo apt-get install gnupg rng-tools
$ gpg --gen-key

For this tutorial we are just going to use default settings, so basically you only have to type your name and email. Also we are going to use a blank password so it doesn’t ask for it each time.

Next we will install and configure reprepro, a very useful tool produce and manage local repositories:

$ sudo apt-get install reprepro
$ mkdir repo && cd repo
$ mkdir conf && cd conf
$ touch distributions

Now we need to create a new distributions file, which is where we store all the required configuration of the repository. The SignWith field should match the key your created in the previous step. This is a sample configuration, with the minimum required information:

Now we can import the generated packages to the local repository and check that are actually there:

$ cd ~
$ reprepro --basedir ~/repo includedeb bionic demoapi*.deb
$ reprepro --basedir ~/repo list bionic

In this point, the local repository is ready to go. It’s a good idea to add the local repository to /etc/apt/sources.list to test it before we upload the repo.

Let’s upload the repository to github, so anyone can test. For that purpose we will first export the public key to the repository directory root:

$ gpg --output PUBLIC.KEY --armor --export service@bluekiri.com
$ mv PUBLIC.KEY ~/repo

Then we upload everything to a previously created github repo:

$ cd ~/repo
$ git init
$ git add --all
$ git commit -m "Package repository"
$ git remote set-url origin git@github.com:bluekiri/demorepo.git
$ git push origin master

And done! At this point we have a public private repo in github that contains our application. You can download and install the service in this tutorial following these instructions:

$ wget -qO - https://raw.githubusercontent.com/bluekiri/demorepo/master/PUBLIC.KEY | sudo apt-key add -
$ echo "deb https://raw.githubusercontent.com/bluekiri/demorepo/master/ bionic main" | sudo tee /etc/apt/sources.list.d/bluekiridemo.list
$ sudo apt-get update
$sudo apt-get install demoapi

Thank you very much for you attention, I hope you enjoyed this article and you learnt something new. Stay tuned for more content related to .NET Core on Linux.