Randomize

Richard Tallent’s occasional blog

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:

  1. Kills the existing dotnet process, if it is running. pkill -f uses the full command line to find the process(es) to kill, unlike killall, which would only be able to kill all dotnet processes.
  2. Overwrites my _www-accessible folder with the latest compiled and published application files.
  3. 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.


Share

comments powered by Disqus