Cowboy in the desert.

Deploying ASP.NET Core to Linux using Octopus

Michael Richardson

Michael Richardson

July 11, 2016

You may have seen the release of .NET Core and ASP.NET Core 1.0 was announced at DevNation recently.

During the KeyNote presentation Scott Hanselman demonstrated deploying a .NET Core application to a Red Hat Linux server using Octopus Deploy (Scott takes the stage at 48:15 and demonstrates Octopus Deploy integration at 1:02:00).

We were honored to get a mention during Scott's presentation. And we're excited about the possibilities for .NET Core.

Scott understandably skipped the gory details. For those interested, this post will dive deeper into what a real-world-ish deployment of an ASP.NET Core application to a Linux server might look like.

Disclaimer: IANALG (I Am Not A Linux Guy). But I think that's the point. Most .NET developers are (at least currently) most familiar with Windows. For many of us, Linux is a strange (and if we're honest, scary) new world. Come on, let's hold hands...

Best Before

The problem with writing a technical procedure post is that it's often obsolete by the time you finish writing it (a problem very apparent when following posts regarding .NET Core).

Wherever possible I have referenced official documentation, which is far more likely to be maintained.


  • Obviously we will require an Octopus Deploy Server. If you don't have one handy, download a trial instance or spin one up from the Azure Marketplace.

  • A Linux server to deploy to. Following Mr Hanselman's lead, we will use a server running Red Hat Enterprise Linux 7.2. You can easily create a RHEL VM in Azure.

  • An ASP.NET Core application to deploy. We will use a demonstration project created for this purpose:
    It is trivial, but contains two relevant features:

    • It uses configuration settings from appsettings.json (into which we will substitute Octopus variables)
    • It contains some configuration files under the \conf directory which we will use to configure our Linux server.

Create a Package

If you want to skip creating the package, you can download the zip file from the Releases page on the GitHub repository.

If you haven't already, clone the sample application repo:

git clone

Move to the directory you cloned the project into. The following commands will be run from there.

Note: We have a saying at Octopus HQ: "Friends don't let friends right-click-publish". In a .NET Core world we may have to update it to: "Friends don't let friends dotnet publish". Creating your package and pushing it to Octopus should be performed by your Build Server. Plugins are available for most popular Build Servers (e.g. Team City, Jenkins, TFS).

See here for our official documentation on publishing ASP.NET Core applications to Octopus.

Restore the NuGet packages:

dotnet restore src

Publish the application to a directory:

dotnet publish src --output published

The contents of the \published directory will be the contents of our package. You can use your favorite archiving tool (I recommend 7-Zip) to archive the contents of \published (not the directory) into a file named

Now upload the package to Octopus Deploy.

Upload Package

You should now see your published package:

Published Package

Create an SSH Target in Octopus

Octopus Requirements

There are a couple of requirements the Red Hat Linux server must satisfy in order to be added as an SSH Target in Octopus:


Mono must be installed. The most up-to-date instructions can be found in the Mono docs. For a RHEL server, follow the 'CentOS and derivatives' section.

In a root shell, execute:

yum install yum-utils
rpm --import ""
yum-config-manager --add-repo
yum intall mono-complete


Octopus will authenticate with the RHEL server using an SSH Key Pair. A guide for creating a key-pair can be found here.

In your Linux shell, generate an SSH key-pair:

ssh-keygen -t rsa

Add the public key to the authorized keys:

cat ~/.ssh/ >> ~/.ssh/authorized_keys

Ensure correct ownership of the SSH directories:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

Other Requirements

.NET Core

Obviously we need .NET Core. Instructions for installing .NET Core on RHEL can be found here. For full disclosure, this author followed the CentOS instructions rather than using subscription-manager.


For our little demonstration application, there is no reason we couldn't have Kestrel directly serving requests. But the consensus seems to be that best-practice is to use a production-grade web server as a reverse-proxy in front of your ASP.NET Core application. On Windows this would be IIS.

We'll use NGINX. Following the instructions found at I create a file /etc/yum.repos.d/nginx.repo, and edited it to contain:

name=nginx repo

I also modified /etc/nginx/nginx.conf to include the line:

include /etc/nginx/sites-enabled/*.conf;

and we should also create that directory:

mkdir /etc/nginx/sites-enabled

This will be important when we deploy our application.

So the http section appears as:

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/sites-enabled/*.conf;
    include /etc/nginx/conf.d/*.conf;


An ASP.NET Core application is run by executing the dotnet utility. Executing it directly via the terminal is fine when you are testing locally, but for deploying to a server we need:

  • To be able to start and stop the service
  • For the service to start automatically on server restarts

So we'll use Supervisor.

Following the install instructions:

yum install python-setuptools
easy_install supervisor

And we will create a Supervisor configuration file using default settings, as per here:

echo_supervisord_conf > /etc/supervisor/supervisord.conf

Now we will create a directory to hold our application-specific supervisor config:

mkdir -p /etc/supervisor/conf.d

And edit /etc/supervisor/supervisord.conf and add at the end:

files = /etc/supervisor/conf.d/*.conf

Note: Installing Supervisor using easy-install does not appear to register Supervisor with SYSTEMD. This seems like something you would certainly want to do. A nice walk-through of the complete supervisor setup can be found here.


By default your RHEL server will likely be rather locked down (as it should be). Since we are going to be using it as a web server, we need to loosen the chains.

I can't state strongly enough that I am not qualified to provide Linux security advice. Please consult your local sysadmin. And apologize in advance.

We need to open port 80:

firewall-cmd --zone=public --add-port=80/tcp --permanent
firewall-cmd --reload

By default, SELinux was preventing NGINX from proxying HTTP requests to Kestrel. Information regarding this can be found here.

Running the following should allow connections to be made:

setsebool httpd_can_network_connect on -P

Create an Environment

Since all Targets need to belong to an Environment, let's create an Environment first.

Create Environment

Create the SSH KeyPair Account

Octopus documentation.

Create SSH KeyPair Account

The private key can be obtained by:

cat ~/.ssh/id_rsa

You will need to save the text into a file, which will then be supplied for the 'Private key' field as shown below.

SSH KeyPair Account Details

Create the SSH Target

Create SSH Target

SSH Target Details

Once you have created the target you can run a health-check to ensure connectivity.

Create a Project in Octopus

Now we will create a project to deploy our ASP.NET Core Demo application.

Create Project

Add Deploy a Package Step

Add a Deploy a Package step to your Project.

Add Package Step

It will reference the package we previously uploaded.

Package Step Details

We will enable two features for this step:

Package Step Features

JSON Configuration Variables

We will use the JSON Configuration Variables feature to transform our appsettings.json file.

In this case we are simply changing the rendered message, but this demonstrates how Octopus can transform hierarchical variables in your JSON configuration files.

Substitute Variables in Files

We will also use the Substitute Variables in Files feature to supply variables to our NGINX and Supervisor configuration files.

If you view these files (located in src\aspnetcoredemo\deployment), you will see they contain placeholders for Octopus variables. For example, the nginx.conf file contains the #{IPAddress} variable:

server {
 listen 80;
 server_name #{IPAddress};
 location / {
     proxy_pass http://localhost:5000;
     proxy_http_version 1.1;
     proxy_set_header Upgrade $http_upgrade;
     proxy_set_header Connection keep-alive;
     proxy_set_header Host $host;
     proxy_cache_bypass $http_upgrade;

Configure NGINX

Next we will add a Run a Script Step to move our NGINX configuration file to /etc/nginx/sites-enabled and tell NGINX to reload.

NGINX Script Step Details

The script source is:

installed=$(get_octopusvariable 'Octopus.Action[Deploy Pkg].Output.Package.InstallationDirectoryPath')
echo "Moving $installed$nginxConf to $dest"
sudo mv -f $installed$nginxConf $dest

echo 'Reloading NGINX'
sudo nginx -s reload

The first line gets the path to the extracted package.

Being able to drop files into the sites-enabled directory saves us from having to modify the existing nginx.conf.

Apparently a common approach is to have both a sites-available and sites-enabled directories. The actual configuration files are deployed to sites-available and symlinks are added to sites-enabled. It is then sites-enabled which is included in nginx.conf. I'll leave this for homework.

Run Supervisor

Now we will add another Run a Script Step to move our Supervisor configuration file to /etc/supervisor/conf.d/aspnetcoredemo.conf and tell Supervisor to reload.

Supervisor Script Step

The script source is:

installed=$(get_octopusvariable "Octopus.Action[Deploy Pkg].Output.Package.InstallationDirectoryPath")
echo "Moving $installed$supervisorConf to $dest"
sudo mv -f $installed$supervisorConf $dest

echo 'Reloading supervisor'
sudo supervisorctl reload


And finally we need to add the variables which will be substituted into our configuration files.



At this point your deployment process should appear similar to:

Deployment Process

What are you waiting for? Create a Release, and Deploy!

Deployment Summary

If all has gone well, you have just deployed an ASP.NET Core application to a Red Hat Enterprise Linux server.

Browser serving application

Note to self: Add balloons.png to sample app home-page ;)


Although it's beyond the scope of this post, it's worth mentioning that with very little effort, we can take that same package and deploy it to also both a Windows 2012 R2 Server and an Azure Web App.

Windows, Azure and Linux Deployment Log

The Future

.NET Core provides a great opportunity for us to improve our story for deploying to Linux targets.

Sans Mono

The fact that you currently have to install the Mono framework on your Linux server in order for it to be an Octopus Deployment Target is awkward in many cases. We are moving towards making our deployment tasks execute using .NET Core, removing the Mono dependency completely.

Sans Script

A major focus for us is to provide the capability to deploy what you want, to where you want, without having to script it yourself. Octopus has historically been targeted at .NET, and .NET has historically been targeted at Windows. .NET Core has breached the Linux wall. We should be able to march through and implement some more Linux-focussed deployment steps.


If you are using Octopus (or even thinking of using it) to deploy .NET Core applications to Linux, we would love to hear from you.

Please tell us what works, what doesn't, and how we can make your deployments easier and more reliable.

Happy (cross-platform) deployments!