Here at uShip, we love to try interesting things during our hackathons. Recently, Greg Walker and I decided to try to get one of our front-end solutions up and running in a docker container. Since docker containers have to run on Linux, that meant getting the project running on Mono first.
Installing Mono
To start, we set up a Ubuntu 14.04 virtual machine. The first thing we did when we got the box setup is installed MonoDevelop, which installed all the Mono dependencies for us. I would recommend following the installation instructions provided by the Mono Project. The latest version of Mono in Ubuntu’s repositories at the time of this post (3.2.8) were out of date and caused additional problems. Use at least Mono version 4.0.2 and MonoDevelop version 5.9.5. Once you get the official Mono repositories setup. You can install MonoDevelop with the command sudo apt-get update && sudo apt-get install monodevelop.
Building in MonoDevelop
The first step we took was to try to clone our repository in Linux and attempt to build it in MonoDevelop. Unfortunately, one of our test projects refused to load because the project type wasn’t supported. We decided to ignore this error and unload that test project since we weren’t planning on running any tests for this example.
We attempted to build our project in Mono, but we had a few assembly versioning issues. Luckily, these are only warnings that MonoDevelop treats as errors, thus we decided to turn those errors off and move onward.
Once we made these changes, our project was building successfully.
Running with Mono
Once we solved the issues with getting the solution building on Linux, it was just a matter of fixing our code to make everything work in the new environment.
To start up a web server to host our MVC project, we used xsp4. You can install this by running sudo apt-get install mono-xsp4. To get this up and running, we ran the command xsp4 in our project’s root directory which got a server up and running on port 8080.
Missing Assemblies
The first code-related issue we ran into was assemblies that live in the Global Assembly Cache (GAC) on Windows were not found on Linux. We fixed this by copying the DLLs over and placing them in the bin directory. Another solution to this problem is to add it to Mono’s GAC. This can be done by running the following command gacutil -i <assembly>. If you are trying to add any delayed-signed assemblies to the GAC, add the -bootstrap option before specifying the assembly.
We fixed several of these missing assembly errors until we started getting new exceptions that had our code in the stack traces. Once we saw our methods in the stack traces, this told us that Mono was able to begin executing our code!
Loading our Configuration
The next major issue was in the way we locate our external configuration files.
In our code, when we look for our configuration files, we start by calling Server.MapPath(“/”) in order to get the root directory. Unfortunately, this doesn’t quite work in Mono, and instead we needed to change it to Server.MapPath(“~”). This works in both Mono and in .NET, so this is likely the correct way to do it, anyway.
For Linux file systems, letter case matters! We had several places in our code where we looked for files without the proper casing. Solving this was simple, but tracking it down took quite a bit of time stepping through code and very thoroughly analyzing every filename in our code to make sure it matched what was on disk.
Differences in Mono
Now that configuration files were loading, we could actually use our site. We ran into one problem that was due to differences between Mono and Microsoft’s .NET Runtime.
CultureInfo Implementations
Deep in our localization code, we do a check to see if a culture is the InvariantCulture. To do this, we were comparing the culture’s ThreeLetterISOLanguageName to a constant string “ivl”.
[cc lang=”csharp”]
private const string InvariantCultureCode = “ivl”;
public static CultureSpecificity Specificity(this CultureInfo culture)
{
if (culture.ThreeLetterISOLanguageName == InvariantCultureCode)
{
return CultureSpecificity.Default;
}
//…
}
[/cc]
This works in Windows, where the ThreeLetterISOLanguageName is all lower case. But, in Mono, it is all uppercase and this check was not passing when it should have. To solve this error, we changed the code to compare the culture to CultureInfo.InvariantCulture. Doing so removes the need for the constant string and the ThreeLetterISOLanguageName.
[cc lang=”csharp”]
public static CultureSpecificity Specificity(this CultureInfo culture)
{
if(culture.Equals(CultureInfo.InvariantCulture))
{
return CultureSpecificity.Default;
}
//…
}
[/cc]
This is the only difference we encountered when running on Mono, which was a huge surprise to us. We were expecting many more things to not work quite right, but had no clue what might go wrong.
Installing Docker
Setting up docker on our Ubuntu 14.04 virtual machine was straight forward. We were able to follow the docker installation instructions for Linux and got up and running quickly.
The technologies that docker relies on are only available in Linux. Installing docker on Windows or OSX requires running a Linux virtual machine that hosts your containers.
Setting up our Docker Container
Setting up our docker container proved to be the least convoluted part of this experiment. We decided to use the official Mono container from Docker Hub which saved us a bit of time scripting out the installation of Mono in our own container.
Dockerfile
FROM mono:4.0 RUN apt-get update && apt-get -y install mono-xsp4 ADD . /app/ # RUN gacutil -i -bootstrap assembly_1.dll # RUN gacutil -i -bootstrap assembly_2.dll WORKDIR /app EXPOSE 9000 ENTRYPOINT ["xsp4", "--port=9000", "--nonstop"]
What this configuration file is doing:
-
- Start FROM the mono:4.0 base image
- RUN our install command to get our server, mono-xsp4, installed
- ADD the current directory to /app/. This makes our code available to processes within the container
- If you’ve decided to store assemblies in Mono’s GAC, but sure to register them in your container
- Set the current working directory to /app
- EXPOSE port 9000, this will be the port we expect requests to come to
- Finally, our container’s ENTRYPOINT, or what to run when the container starts
Now, we can open a console in our application’s directory and execute docker run. This will read our Dockerfile, build the container, and start our web application.
Conclusion
Getting our application up and running in a docker container was easier than we originally thought it would be when we first started. The whole process took Greg and I about 6 hours, most of which was spent debugging to figure out what was wrong with our code and not issues with Mono or docker.
We’re looking deeper into this to see how and if this can be integrated into our development and continuous integration and deployment processes.
We’re Hiring
Interested in playing with these technologies? We’re hiring!