In this post, I’m going to discuss the setup I have for deploying an ASP.NET application in Linux.

The application is actually my pet project BuzzStats, so that name appears all over the place here. Feel free to think of it as ‘HelloWorldApp’ or ‘MyWebApp’.

Here’s a nice diagram I made that illustrates my current deployment process:

The basic points are:

  • the build server has the concept of a 'version number'
  • it is possible to version the assemblies with that version number
  • it is possible to use the same version number to version the deployed application
  • it is possible to switch between these deployed versions on the web server

The setup:

  • Developer's workstation:
    • doesn't matter, as long as he can push changes to GIT/SSH
  • Code server:
    • Ubuntu with SSH enabled to serve GIT repository
  • Build server:
    • Ubuntu
    • Jenkins
    • Mono
  • Web server:
    • Ubuntu
    • Apache
    • Mono
    • Mod_Mono

In my current setup, the build server and the web server are actually the same physical machine. My next step would be to separate that also.

Versioning deployed web application

To support versions of deployed web applications, I use symbolic links.

Typically, you would find the application under /var/www/BuzzStats. I follow that convention, but I make a symbolic link to the actual place where the application lives.

/var/www/BuzzStats -> /opt/BuzzStats/current

and that one is also a symbolic link:

ngeor@box:~$ ls -l /opt/BuzzStats
drwxrwsr-x 11 jenkins  www-data 4096 Nov  7 21:14 1.5.4.161
drwxrwsr-x 11 jenkins  www-data 4096 Nov  7 21:28 1.5.4.162
drwxrwsr-x 11 jenkins  www-data 4096 Nov  8 21:45 1.5.4.164
lrwxrwxrwx  1 www-data www-data    9 Nov  8 21:45 current -> 1.5.4.164

You can see that there are some folders here, e.g. 1.5.4.161. You might recognize the format of a .NET assembly version number. This version number is generated by the build server. The build server also creates this version folder here and deploys the corresponding version.

Security note: Please notice that for that to work, the folder needs to be writable by the group, which can be a security issue. Additionally, to automatically remove version folders automatically (not discussed in this blog post), you also need the version folders themselves to be writable.

So the application is served from /var/www/BuzzStats, as one would typically expect to avoid surprises. That one is a symbolic link to /opt/BuzzStats/current. And finally that one in turn is a symbolic link pointing to the active version folder.

Switching between versions is as easy as changing the symbolic link of current to point to a different version folder. Activate the latest and greatest, figure our there’s a missing image? Rollback immediately and easily to the previous version.

Automatic build

Let’s have a look at how the version number can be generated on the build server.

Version Number

In Jenkins, I use the Version Number Plugin to create a formatted version number. These are my settings:

  • Environment Variable Name : ASSEMBLY_VERSION_NUMBER
  • Version Number Format String : 1.5.4.${BUILD_NUMBER}
  • Build Display Name : Check the 'Use the formatted version number for build display name' option.
  • Project Start Date : 2010-11-27 (not important, but it's been almost three years! :-) )

With this setup, I have an environment variable called ASSEMBLY_VERSION_NUMBER which will contain a value like 1.5.4.42 that will be incremented automatically every time Jenkins bakes a new build.

A note on version semantics: for now I’m using a simple solution. The first part of the version is hard-coded in my Jenkins settings (1.5.4). The last part is Jenkins’ own build number which is automatically incremented. You can chose a different strategy for the version, e.g. based on the date of the build. I will probably also change this in the future so that only the 1.5 part is hard-coded. If you like this topic (I do), you can further read Semantic Versioning 2.0.0 and Which Version of Version?.

Build Steps

Now that we have the version number as an environment variable, we can use it during the build process.

  • Pre-Build: Modify AssemblyInfo.cs files to use the ASSEMBLY_VERSION_NUMBER.This will make the built assemblies use the same version number. I've used various ways in the past, for now I'm doing it with a simple shell script: ``` #!/bin/sh # patch assemblies if [ -z "${ASSEMBLY_VERSION_NUMBER}" ]; then echo "Missing ASSEMBLY_VERSION_NUMBER" exit 1 fi for FILE in `find . -name AssemblyInfo.cs` do echo "Patching $FILE" sed -i -r "s/[0-9]+.[0-9]+.[0-9]+.[0-9]+/${ASSEMBLY_VERSION_NUMBER}/g" $FILE done ```
  • Build: Simply run xbuild, mono's msbuild equivalent. This actually works (TM) these days, also with full NuGet support for package restore.
  • Package the web site into a folder, preparing for deployment. I currently do this with a very old NAnt script. The result is a temporary folder ready to be deployed (as in copy-pasted) to the final deployment location.
  • Deploy to a local folder. Again with the same old NAnt script:
    • create the folder /opt/BuzzStats/${ASSEMBLY_VERSION_NUMBER}
    • copy the package into that folder
    This is my current limitation: I can only deploy to a local folder, so the web server and the build server have to be on the same machine. Once I go past that (perhaps using rsync), I will be able to deploy automatically to my actual web server where this blog is hosted.

Activating and rolling-back

With this setup, after each build we have a new version folder under the main folder /opt/BuzzStats. The version is not activated; remember that the web application is still pointing to /var/www/BuzzStats which in turn points to /opt/BuzzStats/current which still points to the old version.

Activation could also be part of deployment:

  • remove the current link (unlink /opt/BuzzStats/current)
  • create a new symbolic link (ln -s /opt/BuzzStats/${ASSEMBLY_VERSION_NUMBER} /opt/BuzzStats/current)

One caveat here is that mono doesn’t detect the change and still continues to run on the old folder. The only way I’ve found so far to “fix” this is to kill the mono process with pkill mono. Obviously this will kill all mono processes which could be a bit more than what you want…

So, for now, I’ve left that as a manual step. I’ve written a small web application to manage activation and rolling-back using a simple UI and I’ll show/share that on a follow-up post.