This guide will elaborate on how to automatically deploy a ASP.NET core website to a Linux host using GitLab and gitlab-ci-multirunner.
Dotnet website files located in /var/www may be different for your deployment, adjust accordingly. This guide assumes your website is already ready to be deployed, that you have a basic understanding of Linux and are comfortable with the console. You will need to substitute some variables in the templates and file names marked by <name-of-variable>.

Dependencies

  1. A Linux host with root access
  2. Dotnet version required to run the website installed on the host. (Assumes an existing symlink to dotnet in /usr/local/bin/dotnet, adjust accordingly)
  3. A GitLab repository
  4. The gitlab-ci-multi-runner package installed on the host. (With Debian stretch the runner package is part of the regular repository in systemd unit)
  5. The GitLab runner configured to the GitLab instance with the shell runner. It is adviced to make the runner specific to the website repository and give it a unique tag. A good unique tag would be the domain that the website will be reachable from.
  6. Nginx or other web server. If you wish to use another web server besides Nginx adjust the permissions in /etc/sudoers and commands in .gitlab-ci.yml accordingly.
  7. Sudo installed on the host.

Configuration

Preparing the host

First create a systemd unit to control your website on the Linux host from the template below.
If you have dotnet installed through a package manager remember to adjust the dotnet path in ExecStart

[Unit]
Description=example website

[Service]
WorkingDirectory=/var/www

# Make sure the web server user owns the files for the website
ExecStartPre=/bin/chown www-data:www-data -R /var/www/
ExecStart=/usr/local/bin/dotnet /var/www/<name.of.website>.dll

# Automatically restart the website if it crashes
Restart=always
RestartSec=10
SyslogIdentifier=dotnet-website
User=www-data
# Only run the start command as www-data
PermissionsStartOnly=true

# We want asp.net to run with the production settings
Environment=ASPNETCORE_ENVIRONMENT=Production

[Install]
WantedBy=multi-user.target

Now we need to adjust the permissions of the gitlab-runner user to allow him to control the files needed by the website and hand permission back to the web server user after the deployment. The handing back of the permissions is done in the systemd unit. For that we need to adjust /etc/sudoers to allow for no password execution of specific commands, simply add the line below to the sudoers file. With that the basic setup of the host is complete, below is an example Nginx config.

gitlab-runner ALL=NOPASSWD: /bin/systemctl stop <name-of-service>, /bin/systemctl start <name-of-service>, /bin/systemctl stop nginx, /bin/systemctl start nginx, /bin/chown gitlab-runner\\:gitlab-runner -R /var/www

An example Nginx config to proxy requests to the running dotnet instance.

location /
{
    proxy_redirect      off;
    proxy_set_header    Host                    $host;
    proxy_set_header    X-Real-IP               $remote_addr;
    proxy_set_header    X-Forwarded-For         $proxy_add_x_forwarded_for;
    proxy_set_header    X-Forwarded-Protocol    $scheme;
    proxy_set_header    X-Url-Scheme            $scheme;
    proxy_set_header    X-Forwarded-Port        $server_port;
    proxy_pass          http://[::1]:5000;
}

The GitLab CI script

After the host is configured all that is left to do is to tell the GitLab server to execute the CI build on any build server and the on the deployment server. For that we can use .gitlab-ci.yml. The script below assumes the usage of Nginx and the dotnet standard repository layout. The script will build the website on any runner (shared included) with the dotnet tag. After that the build files will be uploaded to the GitLab server, then the specific runner, our host, will pick up the next stage. But only on the master branch as we don’t want to publish the website on every commit. The runner will then stop the web server, dotnet and update the permissions of the old files, delete the old files and move the new files in place and restart all stopped services. The script need to be located at the root of the repository.

stages:
  - build
  - release

build:
  stage: build
  script:
  - 'dotnet publish --output $(pwd)/src/<Name-of-Project>/bin/Release/Publish/ -c Release'
  tags:
  - dotnet
  artifacts:
    name: "${CI_PROJECT_NAME}_${CI_BUILD_REF_NAME}"
    expire_in: 7d
    paths:
    - "./src/<Name-of-Project>/bin/*"

release:
  stage: release
  script:
  - 'sudo /bin/systemctl stop nginx'
  - 'sudo /bin/systemctl stop <name-of-service>'
  - 'sudo /bin/chown gitlab-runner:gitlab-runner -R /var/www'
  - 'rm -rf /var/www/*'
  - 'mv ./src/<Name-of-Project>/bin/Release/Publish/* /var/www/'
  - 'sudo /bin/systemctl start <name-of-service>'
  - 'sudo /bin/systemctl start nginx'
  tags:
  - <unique-tag-of-build-server>
  only:
  - master
  dependencies:
  - build

Afterword

The great thing about using the GitLab runner is that if for whatever reason one of your changes to the website breaks it, you can simply roll back to any older version by executing that pipeline again. Another thing that can be done is to make the server host multiple websites at once, which can a bit of a security risk because the build server need to be allow to control all website files. Which could be misused to disable other websites hosted on the server, which is only a concern if users are allowed to freely edit the CI script in gitlab.

Shared hosting

If you want to make the host a shared host, than you need to tell dotnet to listen on a different port per installation. That can be achieved multiple ways, in this guide we will use command line arguments. For that we need to firstly tell dotnet to use command line arguments by telling WebHost to use a IConfigurationRoot created by the command line arguments. After that the website can be configured from the command line, mainly the listen address can be specified in the service file. It is important where the argument is added, in case of dotnet it need to be after the path to the website dll. Now to change the listen address append --urls "http://[::1]:5001" to ExecStart. This causes dotnet to only listen on IPv6 localhost and port 5001, multiple listen addresses an be specified by separating them with ;. It is also required to adapt the proxy address accordingly, that is left for the reader to do as an exercise. Another thing that need to be adapted is /etc/sudoers to allow for the new commands needed to start/stop multiple services.

Adding IConfigurationRoot

public static IWebHost BuildWebHost(string[] args)
{
  IConfigurationRoot config = new ConfigurationBuilder()
    .AddCommandLine(args)
    .Build();
  return WebHost.CreateDefaultBuilder(args)
    .UseConfiguration(config)
    .UseStartup<Startup>;()
    .Build();
}