Custom Docker Containers on Azure Web Apps - Part 1
I know really catchy title.
Overview
Over the last few months, we've moved all our sites to Custom Docker Containers deployed on Azure Web Apps. This gives us a lot of flexibility in testing and deployment. In this series, I'm going to show you how to create a Docker Image, tag it and push it to a Docker Registry. Build in a CI/CD pipeline with Visual Studio Team Services and deploy the end result app image as a custom container in Azure Web Apps.
What you're going to get
Rather than repeating the Docker web page with the folks at MS have written, its pretty good btw, I just want to add in a few pointers which I've learned. We've deployed a number of Custom Docker Web Apps since the functionality was released and have containerised pretty much every site we have. We're just that excited about this technology stack.
Ultimately this entails, setting up a private registry, creating the Dockerfiles for the apps, getting the images built and deployed into the registry, getting application configurations setup and getting the SSL certs working with Web App sites and the Docker Containers therein.
I'll cover all of theses in this three part series, as well as how to setup a CI/CD pipeline with Visual Studio Team Services (VSTS) taking code changes from Github, building the app into a container, pushing the container to your registry and then having the Web App refresh with the new container. I'll cover most of this in the next post but for now, let's just get the Docker Images built and running with all the bits and pieces we need for our app and Azure.
Custom Docker Containers for Azure Web Apps
To keep this nice and concise, I'm going to assume you know a bit about Docker, if not there are lots of great resources online regarding the tech. Dockers very own tutorials, I find are one of the best.
For Azure Web Apps, the main thing to be concerned about is getting your image nice and small and to not have it do too much so start up times are not too long either. We want the container to deploy as fast as possible while also being very small so it doesn't take up too much space in any future Docker Registry entry.
After that, we need to add in a SSH server for console debugging via SSH in the Azure Portal. In all cases, I put Nginx in front of the app server, be it within its own container or within the same container as the app. Doing this as a reverse proxy allows us to put some caching in on the content but also keeps the site protected further as most app servers are not designed to be on the Web in "raw" form.
The Microsoft team have a pretty good tutorial on how to do all this MS tuturial on creating custom docker containers in Azure Web Apps, so there isn't a need to repeat it all, have a read through it first, then we'll start.
For the purposes of this step through, I'm going to use Ghost Blog as my app, setting this up as container, adding an SSH server, build and test it locally before we pushed it to a Docker Registry.
A Docker Builder And Base Image
Generally, I use a Docker Builder and Docker Base Image, this may be a bit of work initially but has its advantages.
This is a pattern where we put all the build requirements and dependencies into a single container to build the end result we want to run in the app container. You might put unit and integration test verification steps in here too. The app container then runs a top a base image. With a builder we get one easy to use format, in one place and I can build it locally or remotely later in VSTS or wherever else a Docker runtime exists.
You can also seporate out the building of a base image and manage its updates, something that most likely will happen at a slower rate than your app. The end app container will be a third container run on top of the layers from the base image. We can also use the builder and base image later for other projects but the primary reason is app container build speed.
By having less layers in our app container Dockerfile, we can build it faster each time we run it, an ideal optimisation for Azure Web Apps as the site refreshes quite frequently, and thus need to restart the container quite frequently.
All in all, normally we'll have three containers, one for the builder, one for the base image with Azure and app essentials and finally we'll have our app image with Ghost running on top of the base image. However, in this case, the application is so trivial we'll just have a base image and our app image built on top of that.
In this case, this is what the base image DockerFile looks like:
If you want to checkout a working example of an Azure Docker Image with Nginx, Node6 and OpenSSH, you can clone this repository I setup earlier here.
Note
I created the init.sh script in VS Code on Windows. It added a windows escape character so to remove it and all the init.sh script to be executable on Linux, I've added a Docker Layer to remove it on Line20 A bit nasty but does the job for now.
Next you can run this locally like so to make sure the image is buildable:
docker build -t mycompany/azure-nginx-node6-base-image .
After a couple of minutes all the layers will be built and your console window should look something like:
Next, lets check the base image is healthy and working at this early stage, let's see what's in our local docker repository:
Lets just check we can run it and navigate to the running container on a web browser:
docker run -p 80:80 --rm -it --name my-site mycompany/azure-nginx-node6-base-image
I've left a tail on the Nginx access log running in the init script to keep the docker container from exiting when the script completes. You should see that scrolling to the console when you navigate to http://localhost in your browser.
Finally, in your browser you should see:
And that's done.
Pushing our Docker Image to a Docker Registry
We have a few options here, given its pretty generic base image and we don't have any industrial secrets in it, we could just push it to the main docker registry by first logging in and then pushing it on the comandline, let's do that first.
docker login
And login with your Docker Hub credentials. Create one first if you don't have one.
docker push mycompany/azure-nginx-node6-base-image
This will push the base image to the registry so we can use it later.
Or we can push it to the Azure Container Registry which is a private registry you can use for hosting all your built containers so that other services within Azure can consume and run them. We'll do this next. Note, later we'll push our CD docker containers to here.
docker login mycompanyregistry.azurecr.io
And login with your Azure Container Registry Credentials, they can be found in the Azure Portal after the Registry has been created.
docker push mycompanyregistry.azurecr.io/mycompany/azure-nginx-node6-base-image
You'll need to specify the full registry name and then push.
Navigate to the Azure Container Registry and have a look at the artefacts there in to see your pushed container. You should also see any tags that have been pushed with the container, latest in this case.
Adding Ghost to our Image
Building on the previous image we created, the base image, we create one more Dockerfile, which pulls in the image and then adds our local app stuff to it.
Like I mentioned before I'm going to base this on self-hosted Ghost Blog instance.
What I've done is just download the latest release from their github repo here and extracted it into the a directory locally.
Next I create a Dockerfile in the same directory. The first line in the file should build on the previous base image we built. The next step should be to copy the extracted contents of the Ghost zip file from our local directory into our app image.
The init.sh script here is a little more involved to do some db migrations with Knex and have the Ghost blog run with pm2 at the end. The full implementation of this is below:
After this we go through the same process again to build, test and then push it to the Azure Registry or Docker Hub Registry.
Build And Tag
docker build -t mycompany/azure-ghost .
Test locally
docker run -p 80:80 --rm -it --name my-site mycompany/azure-ghost
And navigate to your localhost in a browser It might take a few minutes to start as ghost has to setup a local Sqlite database for the site. You should be able to see what its doing in the console output.
After you're happy with all this, push the app image to the registry for use later from anyone that wants to pull it. In the next part of this series, we'll pull it into the Azure App Service.
docker push mycompanyregistry.azurecr.io/mycompany/azure-ghost
Conclusion
That's the end part 1, where we gave an overview of the series, discussed docker builders, base images and app images. We also stepped though creating Dockerfiles, building, tagging, testing locally and eventually pushing them to both Docker Hub and to an Azure Container Registry.
In the next part of the series, we'll look at setting up the Azure Web App to run this custom app image and have it available on the public web with Azure.