Self-hosting ASP.NET Core using Coolify


Back in the late-2000s when I was first starting out in my career, everything was self-hosted. This was the early days of .NET framework and the early days of SaaS - manually configuring IIS, installing libraries to the GAC, managing SSL certificates - basically it was enough to put me off for the next 15 years! I was fortunate enough back then to be an early adopter of Azure and once I had a taste of PaaS I never went back. Over the years I’ve used both Azure and AWS extensively and just accepted the encroaching costs - even when the compute bill ends up in the thousands!

When I was building Boarded I immediately gravitated back to Azure App Services and Azure Static Sites, setup the CD up for it and didn’t think much more about it. However, when I came to launch the product, I had to switch to a $50 App Service to stop it from shutting down. That’s $50 for a server to host one API with 1 core and 1.75 GB of RAM! As somebody with aspirations to launch many such services, I don’t really want to be paying $50 for each service - when it comes out of your own pocket it definitely sharpens your mind on the aforementioned costs!

In addition to that, Twitter has been full of scare stories about DDoS attacks causing HUGE bills because of ingress overage, and nobody wants that risk hanging over them. Besides, a lot has changed in the past 15 years and after hearing about Coolify I thought it could be a good time to revisit self-hosting. Coolify markets itself as an open source and self-hosted Heroku/Netlify/Vercel alternative but it’s actually quite a bit more than that. Coolify can essentially host anything via Docker, it provides a Traefik load balancer which provisions free SSL certificates, it monitors your services and it integrates directly with GitHub - meaning all you need to bring is your own server with a SSH connection.

I won’t go into the ins and outs of connecting a server as Coolify already documents all of that, and setting up static site hosting is a breeze, but I did hit a couple of stumbling blocks when trying to host my ASP.NET Core Web Api so I will share what is required to get that up and running. Firstly, I’ve created a GitHub App within Coolify and connected that to my GitHub account - this means that I can simply select the project from the list of GitHub repos.

Create resource in Coolify

Build and Run

Everything so far is identical for any resource you want to create from a private GitHub repository - where it gets interesting is on the next step where we setup our Nixpacks commands. Nixpacks creates an OCI compliant image directly from source and it will even identify the project type. However, I found that for Web Api the default doesn’t quite work - fortunately after much trial and error I’ve found some commands which do work.

Nixpacks build in Coolify

The default command is just fine for Install, but for Build and Start I used the following.

Build

dotnet publish src/Api --no-restore -c Release -o out

This example assumes you have your Web Api in a /src/Api folder, but you can adapt this to match your project.

Run

cd out && dotnet Api.dll --urls=http://0.0.0.0:3000

Here we’re moving into the output folder before running the published DLL on the port configured in Coolify.

Coolify port configuration

By default Nixpacks will run in .NET 6, so if you’re using a newer version you will need to set an environment variable to specify the .NET version to run. Fortunately Coolify has a built in mechanism for setting environment variables that you can use for this. For example, for .NET 8 you would use:

NIXPACKS_CSHARP_SDK_VERSION=8.0

Coolify environment variables

Environment Variables

And that’s all it needs to get your API up and running! However, chances are you will also need to transform your appsettings.json with your production config. At this point you could connect to a key vault, but as we’ve already seen Coolify already supports a way of setting environment variables as secrets so we’ll use that instead. ASP.NET Core loads configuration in the following order of precedence.

  • appsettings.json
  • appsettings.{Environment}.json
  • User secrets
  • Environment variables
  • Command-line arguments

.NET will need environment variables names to match the structure expected by the IConfigurationRoot. Say for example you have the following configuration.

{
    "MongoDb": {
        "ConnectionString": "PROVIDED_BY_ENVIRONMENT_VARIABLE"
    },
    "SendGrid": {
        "ApiKey": "PROVIDED_AT_BUILD_TIME"
    }
}

You will need to name your environment variables MongoDb__ConnectionString and SendGrid__ApiKey where each level of the object is separated by a double underscore (__).

Automated Builds

By default, Coolify will automate a build whenever you push to GitHub. However, in my case I wanted to run my tests in GitHub Actions before a build was triggered. The first step here was to disable auto deploy in Coolify, by going to advanced settings.

Coolify auto-deploy setting

Coolify also supports a webhook for deployment which you can get from the Webhooks setting on your resource. The format of the webhook is always: https://app.coolify.io/api/v1/deploy?uuid=:resourceId&force=false. You will also need to authenticate using an API token which you can create and manage in the Security section of Coolify.

Coolify API tokens

Once you have grabbed the webhook URL and the API token, you can simply use cURL to trigger the webhook and start the deployment. Here’s the full GHA workflow.

name: Test and Deploy

on:
  push:
    branches: [main]

env:
  DOTNET_VERSION: '7.0.x'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Setup .NET
        uses: actions/setup-dotnet@v2
        with:
          dotnet-version: 7.0.x

      - name: Restore dependencies
        run: dotnet restore
      
      - name: Build
        run: dotnet build --no-restore
      
      - name: Test
        run: dotnet test --no-build --verbosity normal
  
  deploy:
    name: deploy
    runs-on: ubuntu-latest
    env:
      CI: false
    needs: test

    steps:
      - name: trigger deployment
        run: |
          curl -X GET "${{ secrets.COOLIFY_WEBHOOK }}" -H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}"

Custom Domain

We’re almost there now! One final step you probably want to complete is to setup your custom domain. As always with Coolify, this is really easy - simply setup an A record in your DNS which points to your connected server and then set the URL you wish to use against your Coolify resource. And that’s it - Coolify will setup the reverse proxy to connect on that domain and even issue free SSL certificates.

Coolify custom domains

Conclusion

Hopefully the above helps you get started with self-hosting ASP.NET Core using Coolify and you can start saving some money 🤑 I hasten to add that some of the above has been discovered using trial and error, so if you’ve found a better or alternative way then don’t hesitate to let me know. Any questions you might have I’d be more than happy to help to the best of my knowledge.

Get in touch