Table of contents
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
- A Linux host with root access
- Dotnet version required to run the website installed on the host. (Assumes an existing symlink to dotnet in /usr/local/bin/dotnet, adjust accordingly)
- A GitLab repository
- 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) - 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.
- 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. - 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();
}