In this article, we are going to learn how to deploy ASP.NET Core applications to an Ubuntu Linux server with Apache.

To download the source code for this article, you can visit our GitHub repository.

Let’s get started.

Create a New ASP.NET Core Web API Project

Let’s create a new solution using the dotnet command line interface. We’ll open a command prompt window in the location where we want to create the solution and use these commands:

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

mkdir DeployingToLinuxWithApache
cd DeployingToLinuxWithApache
dotnet new sln --name DeployingToLinuxWithApache
dotnet new webapi --name DeployingToLinuxWithApache
dotnet sln add DeployingToLinuxWithApache/DeployingToLinuxWithApache.csproj
mkdir DeployingToLinuxWithApache cd DeployingToLinuxWithApache dotnet new sln --name DeployingToLinuxWithApache dotnet new webapi --name DeployingToLinuxWithApache dotnet sln add DeployingToLinuxWithApache/DeployingToLinuxWithApache.csproj
mkdir DeployingToLinuxWithApache
cd DeployingToLinuxWithApache
dotnet new sln --name DeployingToLinuxWithApache
dotnet new webapi --name DeployingToLinuxWithApache
dotnet sln add DeployingToLinuxWithApache/DeployingToLinuxWithApache.csproj

This creates a new solution and then adds a new Web API project to the solution.

Install the ASP.NET Core Runtime

Before we can run the ASP.NET Core applications on Linux using Apache, we need to first install the ASP.NET Core runtime. The runtime provides the necessary components and libraries required to run ASP.NET Core applications on Linux:

sudo apt-get update && sudo apt-get install -y aspnetcore-runtime-7.0
sudo apt-get update && sudo apt-get install -y aspnetcore-runtime-7.0

When this is completed, let’s run dotnet --info in the terminal to confirm that it’s been installed successfully:

dotnet --info
dotnet --info

After we execute the command, we can inspect the output:

Host:
Version: 7.0.8
Architecture: x64
Commit: 4b0550942d
.NET SDKs installed:
No SDKs were found.
.NET runtimes installed:
Microsoft.AspNetCore.App 7.0.8 [/usr/lib/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 7.0.8 [/usr/lib/dotnet/shared/Microsoft.NETCore.App]
Other architectures found:
None
Environment variables:
DOTNET_ROOT [/usr/lib/dotnet]
global.json file:
Not found
Learn more:
https://aka.ms/dotnet/info
Download .NET:
https://aka.ms/dotnet/download
Host: Version: 7.0.8 Architecture: x64 Commit: 4b0550942d .NET SDKs installed: No SDKs were found. .NET runtimes installed: Microsoft.AspNetCore.App 7.0.8 [/usr/lib/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.NETCore.App 7.0.8 [/usr/lib/dotnet/shared/Microsoft.NETCore.App] Other architectures found: None Environment variables: DOTNET_ROOT [/usr/lib/dotnet] global.json file: Not found Learn more: https://aka.ms/dotnet/info Download .NET: https://aka.ms/dotnet/download
Host:
  Version:      7.0.8
  Architecture: x64
  Commit:       4b0550942d

.NET SDKs installed:
  No SDKs were found.

.NET runtimes installed:
  Microsoft.AspNetCore.App 7.0.8 [/usr/lib/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 7.0.8 [/usr/lib/dotnet/shared/Microsoft.NETCore.App]

Other architectures found:
  None

Environment variables:
  DOTNET_ROOT       [/usr/lib/dotnet]

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download

The output shows that the runtime is installed, specifically version 7.0.8.

Deploy the App and Run It in Kestrel

Before we deploy to the Linux server, we need to create the deployment folder and make sure it’s set to the right permissions:

mkdir -p /var/www/app
chmod 666 /var/www/app
mkdir -p /var/www/app chmod 666 /var/www/app
mkdir -p /var/www/app
chmod 666 /var/www/app

Next, let’s publish the app to a folder on the development machine. In Visual Studio, create a new publish profile with Folder as the target and then select the folder location. After the profile has been created, let’s click Publish to create the deployment files in the specified location.

Now that we have the files on the development machine, let’s copy them to the Linux server using WinSCP:

Transfer asp.net core application to linux using WinSCP

When we install the dotnet runtime on the Linux server, it comes with Kestrel which is a lightweight web server for ASP.NET Core. We’ll use Kestrel to confirm the app runs. Let’s navigate to the deployment path and run it:

dotnet DeployingToLinuxWithApache.dll
dotnet DeployingToLinuxWithApache.dll

This starts up the application and produces the output:

info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /var/www/app
info: Microsoft.Hosting.Lifetime[14] Now listening on: http://localhost:5000 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. info: Microsoft.Hosting.Lifetime[0] Hosting environment: Production info: Microsoft.Hosting.Lifetime[0] Content root path: /var/www/app
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /var/www/app

The output shows that Kestrel has started successfully and is listening on port 5000. We can confirm that the app is running using the curl command:

curl -X 'GET' 'http://localhost:5000/WeatherForecast' -H 'accept: text/plain'
curl -X 'GET' 'http://localhost:5000/WeatherForecast' -H 'accept: text/plain'

This makes a GET request to the /WeatherForecast endpoint and produces the output:

[
{"date":"2023-07-13","temperatureC":24,"temperatureF":75,"summary":"Warm"},
{"date":"2023-07-14","temperatureC":21,"temperatureF":69,"summary":"Sweltering"},
{"date":"2023-07-15","temperatureC":2,"temperatureF":35,"summary":"Chilly"},
{"date":"2023-07-16","temperatureC":46,"temperatureF":114,"summary":"Chilly"},
{"date":"2023-07-17","temperatureC":-9,"temperatureF":16,"summary":"Warm"}
]
[ {"date":"2023-07-13","temperatureC":24,"temperatureF":75,"summary":"Warm"}, {"date":"2023-07-14","temperatureC":21,"temperatureF":69,"summary":"Sweltering"}, {"date":"2023-07-15","temperatureC":2,"temperatureF":35,"summary":"Chilly"}, {"date":"2023-07-16","temperatureC":46,"temperatureF":114,"summary":"Chilly"}, {"date":"2023-07-17","temperatureC":-9,"temperatureF":16,"summary":"Warm"} ]
[
  {"date":"2023-07-13","temperatureC":24,"temperatureF":75,"summary":"Warm"},
  {"date":"2023-07-14","temperatureC":21,"temperatureF":69,"summary":"Sweltering"},
  {"date":"2023-07-15","temperatureC":2,"temperatureF":35,"summary":"Chilly"},
  {"date":"2023-07-16","temperatureC":46,"temperatureF":114,"summary":"Chilly"},
  {"date":"2023-07-17","temperatureC":-9,"temperatureF":16,"summary":"Warm"}
]

Now that we have it running in Kestrel, let’s set up Apache as a reverse proxy server. 

Install and Configure Apache on the Server

A reverse proxy server like Apache excels more than Kestrel at handling some web-serving capabilities such as caching and compressing requests, as well as serving static content. Let’s install it using apt:

sudo apt-get update && sudo apt-get install -y apache2
sudo apt-get update && sudo apt-get install -y apache2

Now that it’s installed, let’s configure it to function as a reverse proxy. First, we need to enable the proxy and proxy_http modules:

sudo a2enmod proxy proxy_http
sudo a2enmod proxy proxy_http

The a2enmod command enables the modules and produces the output:

Enabling module proxy.
Considering dependency proxy for proxy_http:
Module proxy already enabled
Enabling module proxy_http.
To activate the new configuration, you need to run:
systemctl restart apache2
Enabling module proxy. Considering dependency proxy for proxy_http: Module proxy already enabled Enabling module proxy_http. To activate the new configuration, you need to run: systemctl restart apache2
Enabling module proxy.
Considering dependency proxy for proxy_http:
Module proxy already enabled
Enabling module proxy_http.
To activate the new configuration, you need to run:
  systemctl restart apache2

Next, let’s create a conf file in /etc/apache2/sites-available/ named app.conf:

nano /etc/apache2/sites-available/app.conf
nano /etc/apache2/sites-available/app.conf

The nano command creates the file if it does not exist and opens it for editing.

Then, we are going to modify the created file:

<VirtualHost *:80>
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:5000/
ProxyPassReverse / http://127.0.0.1:5000/
ErrorLog ${APACHE_LOG_DIR}/app-error.log
CustomLog ${APACHE_LOG_DIR}/app-access.log common
</VirtualHost>
<VirtualHost *:80> ProxyPreserveHost On ProxyPass / http://127.0.0.1:5000/ ProxyPassReverse / http://127.0.0.1:5000/ ErrorLog ${APACHE_LOG_DIR}/app-error.log CustomLog ${APACHE_LOG_DIR}/app-access.log common </VirtualHost>
<VirtualHost *:80>
    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:5000/
    ProxyPassReverse / http://127.0.0.1:5000/
    ErrorLog ${APACHE_LOG_DIR}/app-error.log
    CustomLog ${APACHE_LOG_DIR}/app-access.log common
</VirtualHost>

The directives in the file instruct Apache to listen for requests on port 80 and forward them for handling on port 5000 of the same server, which is where our app is running in Kestrel.

After this let’s disable the default site, enable the one we just created, and then restart Apache:

sudo a2dissite 000-default
sudo a2ensite app
sudo systemctl restart apache2
sudo a2dissite 000-default sudo a2ensite app sudo systemctl restart apache2
sudo a2dissite 000-default
sudo a2ensite app
sudo systemctl restart apache2

When we test this using curl:

curl -X 'GET' 'http://localhost/WeatherForecast' -H 'accept: text/plain'
curl -X 'GET' 'http://localhost/WeatherForecast' -H 'accept: text/plain'

We can see it’s still running as expected:

[
{"date":"2023-07-13","temperatureC":24,"temperatureF":75,"summary":"Warm"},
{"date":"2023-07- 14","temperatureC":21,"temperatureF":69,"summary":"Sweltering"},
{"date":"2023-07-15","temperatureC":2,"temperatureF":35,"summary":"Chilly"},
{"date":"2023-07-16","temperatureC":46,"temperatureF":114,"summary":"Chilly"},
{"date":"2023-07-17","temperatureC":-9,"temperatureF":16,"summary":"Warm"}
]
[ {"date":"2023-07-13","temperatureC":24,"temperatureF":75,"summary":"Warm"}, {"date":"2023-07- 14","temperatureC":21,"temperatureF":69,"summary":"Sweltering"}, {"date":"2023-07-15","temperatureC":2,"temperatureF":35,"summary":"Chilly"}, {"date":"2023-07-16","temperatureC":46,"temperatureF":114,"summary":"Chilly"}, {"date":"2023-07-17","temperatureC":-9,"temperatureF":16,"summary":"Warm"} ]
[ 
  {"date":"2023-07-13","temperatureC":24,"temperatureF":75,"summary":"Warm"},
  {"date":"2023-07- 14","temperatureC":21,"temperatureF":69,"summary":"Sweltering"},
  {"date":"2023-07-15","temperatureC":2,"temperatureF":35,"summary":"Chilly"},
  {"date":"2023-07-16","temperatureC":46,"temperatureF":114,"summary":"Chilly"},
  {"date":"2023-07-17","temperatureC":-9,"temperatureF":16,"summary":"Warm"}
]

To wrap this up, let’s configure Kestrel to run as a Linux service so it will be easier to monitor and manage.

Configure the App to Run as a Service

Let’s create a systemd unit file named kestrel-app.service in the /etc/systemd/system directory:

nano /etc/systemd/system/kestrel-app.service
nano /etc/systemd/system/kestrel-app.service

Then we are going to modify the created file:

[Unit]
Description=ASP.NET Core Web API running on Ubuntu
[Service]
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/dotnet /var/www/app/DeployingToLinuxWithApache.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-web-api
# This user should exist on the server and have ownership of the deployment directory
User=deploy
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target
[Unit] Description=ASP.NET Core Web API running on Ubuntu [Service] WorkingDirectory=/var/www/app ExecStart=/usr/bin/dotnet /var/www/app/DeployingToLinuxWithApache.dll Restart=always # Restart service after 10 seconds if the dotnet service crashes: RestartSec=10 KillSignal=SIGINT SyslogIdentifier=dotnet-web-api # This user should exist on the server and have ownership of the deployment directory User=deploy Environment=ASPNETCORE_ENVIRONMENT=Production [Install] WantedBy=multi-user.target
[Unit]
Description=ASP.NET Core Web API running on Ubuntu

[Service]
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/dotnet /var/www/app/DeployingToLinuxWithApache.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-web-api
# This user should exist on the server and have ownership of the deployment directory
User=deploy
Environment=ASPNETCORE_ENVIRONMENT=Production

[Install]
WantedBy=multi-user.target

Let’s save the file after adding the content, and then enable and start up the service:

sudo systemctl enable kestrel-app.service
sudo systemctl start kestrel-app.service
sudo systemctl status kestrel-app.service
sudo systemctl enable kestrel-app.service sudo systemctl start kestrel-app.service sudo systemctl status kestrel-app.service
sudo systemctl enable kestrel-app.service
sudo systemctl start kestrel-app.service
sudo systemctl status kestrel-app.service

After enabling and starting the service, the last command checks the status of the service and produces the output:

● kestrel-app.service - ASP.NET Core Web App running on Ubuntu
Loaded: loaded (/etc/systemd/system/kestrel-app.service; enabled; preset: enabled)
Active: active (running) since Thu 2023-07-13 05:37:45 UTC; 1s ago
Main PID: 2889 (dotnet)
Tasks: 22 (limit: 9379)
Memory: 18.3M
CPU: 1.615s
CGroup: /system.slice/kestrel-app.service
└─2889 /usr/bin/dotnet /var/www/app/DeployingToLinuxWithApache.dll
Jul 13 05:37:45 ubuntu systemd[1]: Started ASP.NET Core Web App running on Ubuntu.
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: info: Microsoft.Hosting.Lifetime[14]
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: Now listening on: http://localhost:5000
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: info: Microsoft.Hosting.Lifetime[0]
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: Application started. Press Ctrl+C to shut down.
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: info: Microsoft.Hosting.Lifetime[0]
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: Hosting environment: Production
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: info: Microsoft.Hosting.Lifetime[0]
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: Content root path: /var/www/app
● kestrel-app.service - ASP.NET Core Web App running on Ubuntu Loaded: loaded (/etc/systemd/system/kestrel-app.service; enabled; preset: enabled) Active: active (running) since Thu 2023-07-13 05:37:45 UTC; 1s ago Main PID: 2889 (dotnet) Tasks: 22 (limit: 9379) Memory: 18.3M CPU: 1.615s CGroup: /system.slice/kestrel-app.service └─2889 /usr/bin/dotnet /var/www/app/DeployingToLinuxWithApache.dll Jul 13 05:37:45 ubuntu systemd[1]: Started ASP.NET Core Web App running on Ubuntu. Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: info: Microsoft.Hosting.Lifetime[14] Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: Now listening on: http://localhost:5000 Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: info: Microsoft.Hosting.Lifetime[0] Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: Application started. Press Ctrl+C to shut down. Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: info: Microsoft.Hosting.Lifetime[0] Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: Hosting environment: Production Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: info: Microsoft.Hosting.Lifetime[0] Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: Content root path: /var/www/app
● kestrel-app.service - ASP.NET Core Web App running on Ubuntu
     Loaded: loaded (/etc/systemd/system/kestrel-app.service; enabled; preset: enabled)
     Active: active (running) since Thu 2023-07-13 05:37:45 UTC; 1s ago
   Main PID: 2889 (dotnet)
      Tasks: 22 (limit: 9379)
     Memory: 18.3M
        CPU: 1.615s
     CGroup: /system.slice/kestrel-app.service
             └─2889 /usr/bin/dotnet /var/www/app/DeployingToLinuxWithApache.dll

Jul 13 05:37:45 ubuntu systemd[1]: Started ASP.NET Core Web App running on Ubuntu.
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: info: Microsoft.Hosting.Lifetime[14]
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]:       Now listening on: http://localhost:5000
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: info: Microsoft.Hosting.Lifetime[0]
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]:       Application started. Press Ctrl+C to shut down.
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: info: Microsoft.Hosting.Lifetime[0]
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]:       Hosting environment: Production
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]: info: Microsoft.Hosting.Lifetime[0]
Jul 13 05:37:46 ubuntu dotnet-web-app[2889]:       Content root path: /var/www/app

With this, our deployment is complete and we can use the previous curl command to confirm that everything still works as expected.

If you want to learn more about deploying with Nginx as the reverse proxy server, you can read Deploy ASP.NET Core on Linux with Nginx .

Conclusion

In this article, we have deployed an ASP.NET Core Web API to Ubuntu with Apache as a reverse proxy server.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!