ASP.NET Core Web API in Production on OS X
My main web site (http://www.tallent.us/), which is mostly comprised of galleries of my art photography, runs on a MAPP stack — Mac OS X, Apache, PHP, and PostgreSQL. I’ve been playing with ASP.NET Core and loving it, and I’ve been wanting to make the switch to using it for the web gallery API. I prefer C# over PHP, and it’s a good real-world project to learn the new bits, since my day job will be using legacy ASP.NET for the foreseeable future.
However, I didn’t want ASP.NET Core to run the whole show — Apache is perfectly capable of handling my static files and unrelated PHP code, I just wanted Kestrel to handle database communication for the photo galleries. Creating the photo gallery API replacement in Visual Studio Code was straightforward. With some POCOs, Dapper, and the .NET Core PostgreSQL driver, I had Kestrel serving up my JSON API on port 5000 in a Terminal window without too much effort. The future is here!
But to use it “in production,” I didn’t want Kestrel exposed directly (nor does Microsoft recommend that), and I didn’t want my web site to go down if I log out or close the Terminal window where the dotnet
process was running. A workable solution would require (a) proxying the API through Apache, and (b) finding a way to reliably and automatically run Kestrel in the background.
Creating a Reverse Proxy in Apache
The first goal was to get Apache to call my Web API application and convey its response to the calling browser. This required setting Apache to load the proxy module by uncommenting these two lines in the Apache configuration file (/private/etc/apache2/httpd.conf
):
LoadModule proxy_module libexec/apache2/mod_proxy.so
LoadModule proxy_http_module libexec/apache2/mod_proxy_http.so
The mod_proxy module requires some new configuration settings in the same file (or, if you have multiple virtual hosts, in whatever configuration file has your site’s vhost config):
ProxyPass /api/ http://localhost:5000/
ProxyPassReverse /api/ http://localhost:5000/
ProxyHTMLEnable On
ProxyHTMLURLMap http://localhost:5001 http://[your domain]/api
ProxyPreserveHost On
<Location /api/>ProxyHTMLURLMap / /api/</Location>
This tells Apache to send any request to http://[your domain]/api
over to Kestrel and return Kestrel’s response. So, for example, a request to http://www.yourdomain.com/api/blah/1
” will make Apache retrieve http://localhost:5000/blah/1
and return the response to the client. The ProxyPreserveHost
option tells Apache to relay the request headers (user agent, cookies, etc.) to Kestrel.
Note that the client’s IP address will be sent in the “X-Forwarded-For” header (rather than the usual “REMOTE_ADDR”). Also, if your Controller uses the default attribute [Route("api/[controller]")]
and you use the above configuration, your client-side end-point will look like http://www.yourdomain.com/api/<b>api/</b>blah/1
. I removed the extra “api/” in the attribute to solve this, but could have just adjusted the mappings above as well.
After making these changes and restarting Apache, I had a working proxy! Half of the job was done, now I just needed the dotnet
process to run my application on boot.
Running Kestrel Automatically
Under OS X, the launchd
service replaces the old-school nix cron
utility. It can be used to launch processes automatically on boot, or when a user logs in. I wanted my ASP.NET Core application to launch on boot, without requiring a user login, so I needed launchd
to run it as a Launch Daemon. This is done by creating a “plist” XML file. launchd
looks in several folders for “.plist” files, I put mine in /Library/LaunchDaemons/
. I used the freeware LaunchControl application to create the file, but you can also create it by hand. Here’s my plist file:
This tells launchd
to run the dotnet
process from the directory where the ASP.NET Core application is published (by default, in ./bin/Debug/netcoreapp1.0/publish/
relative to your project folder) and load the application (which is actually a DLL file in Core-world). It starts dotnet
using the _www
user account, the same one used to run Apache, so it’s important that this account has proper access to that directory. The other configuration options here set up the log files. I actually haven’t seen anything written to stderr
from dotnet — errors appear to be going to stdout
in the current version. There’s also an option there that tells launchd
to re-run the process alive if it exits.
Since my project files are in my user folder, I didn’t want _www
having permissions there, and I wanted to be able to quickly relaunch the process if I make changes. To handle both of these issues, I created a shell script in my main project folder, called deploy.sh
:
#!/bin/sh<br />
sudo pkill -f "dotnet MyWebApi.dll"
rsync -a --delete bin/Debug/netcoreapp1.0/publish/ /full/path/to/where/api/application/should/launch`
sudo launchctl load /Library/LaunchDaemons/dotnet.api.plist
This does the following:
- Kills the existing
dotnet
process, if it is running.pkill -f
uses the full command line to find the process(es) to kill, unlikekillall
, which would only be able to kill alldotnet
processes. - Overwrites my
_www
-accessible folder with the latest compiled and published application files. - Tells
launchd
to restart the application.
This does prompt for the superuser password, but other than that, deploying changes is painless. To make it even more convenient, I added the following to my project.json
file to the script runs automatcally when I run dotnet publish
:
"scripts": { "postpublish": "bash ./deploy.sh" }
(This is dead code walking, since project.json
is on the way out, so if you’re reading this a few months from now, you’ll need to look up the equivalent MSBuild setting.)
Final Thoughts
There may be an easier way to do some of this, I’m just showing how I muddled through it. I’m pretty happy with my solution, and it’s doing a great job serving up my galleries now. Hopefully other developers working on OS X will find this useful.