So You Want to Containerize Jamf Pro

Containers. They’re what all the cool kids talk about these days.

But really, if you haven’t been moving everything you’re doing to either containerized or serverless deployments you may want to make a commitment to putting that at the forefront of your 2020 objectives.

And yeah, we’re already well into that internally at Jamf (more on that in the future).

For the Jamf admin who has yet to migrate to Jamf Cloud and still operates their own environment, you may have already considered moving off your virtual machine (or, shudder, bare metal) installs.

But where to start?

Let’s start with the basics. We’re going to use Docker on your laptop to get Jamf Pro up and running without having to install Java, Tomcat, or MySQL.

You’re Going to Need Docker

Before continuing, you’re going to need Docker Desktop. 17.09 is the latest at time of posting. Finish this before continuing (you won’t need anything else!).

Mac: https://docs.docker.com/docker-for-mac/install/

Windows 10: https://docs.docker.com/docker-for-windows/install/

Building a Jamf Pro Docker Image

First thing we need is a Docker image we can deploy.

What is an image? A Docker image is an artifact that contains everything needed to run an application. Think of it like a virtual machine snapshot, but immutable and highly portable. Containers are the running processes launched using an image.

To build an image we need a Dockerfile that contains all the instructions on what to install and configure. Engineers at Jamf maintain a “starter” image that we will be using for our environment.

The source code is located here: https://github.com/jamf/jamfpro

But, we don’t need to build this image ourselves. The latest version of it is already available on DockerHub: https://hub.docker.com/r/jamfdevops/jamfpro

DockerHub is a public repository of already built images you can pull down and use! You’ll find all sorts of images readily available from members of the open source community as well as software vendors.

The jamfdevops/jamfpro image is going to be the foundation of our jamfpro image we launch.

Some of you might be thinking at this point, “Why are we building another image from this one?” True, we can use the image on its own and provide a ROOT.war file at run time, but this is only helpful for one-off testing.

In a production, or production-like, setting we won’t be running docker commands to launch our instances. We’ll be following best practices with pipelines, and infrastructure and configuration as code. To accomplish this we need an immutable deployment artifact.

Pull a copy of this image down to your computer before continuing (it doesn’t have a latest tag so you have to specify – 0.0.10 is the latest at time of posting).

docker pull jamfdevops/jamfpro:0.0.10

Download the ROOT.war

We need the WAR file for a manual installation of Jamf Pro first. Customers cant obtain one from their Assets page on Jamf Nation: https://www.jamf.com/jamf-nation/my/products

Yes, you have to be a customer for this step. ¯\_(ツ)_/¯

Click Show alternative downloads and then Jamf Pro Manual Installation. Extract the zip file into an empty directory.

Build the Deployable Image

Here’s a quick script that will use the jamfdevops/jamfpro image as a base for our output deployment image. The VERSION variable can be changed for what you are using (10.17.0 is the current at time of posting).

# Run from a directory that _only_ contains your ROOT.war file.
# Change VERSION to that of the ROOT.war being deployed

VERSION=10.17.0

docker build . -t jamfpro:${VERSION} -f - <<EOF
FROM jamfdevops/jamfpro:0.0.10
ADD ROOT.war  /data/
EOF

Grab this script on my GitHub as a Gist: build_jamfpro_docker_version.py

I’m using an inline Dockerfile for this script. FROM tells it what the base image is. ADD is the command that will copy the ROOT.war file into the image’s /data directory. This is where the jamfdevops/jamfpro startup script checks for a WAR file to extract into the Tomcat webapps directory.

You should see the following after a successful build of the image.

Sending build context to Docker daemon  227.4MB
Step 1/2 : FROM jamfdevops/jamfpro:0.0.10
 ---> f87f303293bc
Step 2/2 : ADD ROOT.war  /usr/local/tomcat/webapps/
 ---> 83cd51f8b622
Successfully built 83cd51f8b622
Successfully tagged jamfpro:10.17.0

To learn more, see https://docs.docker.com/develop/develop-images/dockerfile_best-practices/

Create a Docker Network

Our environment is going to need two running containers: the Jamf Pro web app, and a MySQL database. We’re going to create a virtual network for them to launch in that will allow communication between the web app and the database.

It’s a quick one-liner.

docker network create jamfnet

Our new jamfnet network is a bridge network (the default). We don’t need to do any additional configuration for our purposes today.

To learn more, see https://docs.docker.com/network/bridge/

Start a MySQL Container

Now to start a database. We’re going to use the official mysql:5.7 image from DockerHub, and this image gives us some handy shortcuts to make our local environment easier to setup, but let’s talk about what we’re about to do and why you shouldn’t do this in production, or production-like environments.

  • We’re running a database in a container.
    This isn’t necessarily something that you shouldn’t do, but the way we’re doing this is not production grade. Running databases in Docker is fantastic for having the service available without needing to install and configure it on your own. You should really, reallyreally know what you’re doing.
    Protip: use managed database services from your provider of choice (which we’re going to explore in the near future).
  • We’re not using a volume to persist the database.
    Containers are ephemeral. They don’t preserve state or data without the use of some external volumes (either on the host or remote). You can preserve the data on disk by mounting a local directory into the running container using an addtional argument: -v /my/local/dir:/var/lib/mysql.
  • We’re allowing the image startup scripting to create the Jamf Pro database.
    The MySQL image has a handy feature exposed through the MYSQL_DATABASE environment variable to create a default database. This is a shortcut for setting up Jamf Pro as we don’t have to connect afterwards to create it, but this means the only user available is root which leads us to the last point…
  • We’ll be using the root MySQL user for Jamf Pro.
    Never do this in production. For our local test environment it’s fine – we’re not hosting customer or company data and it’s temporary.

Refer to this Jamf Nation KB on how to properly setup the Jamf Pro database on MySQL: https://www.jamf.com/jamf-nation/articles/542/manually-creating-the-jamf-pro-database

With all that said, let’s take a look at the docker run command to start up our test MySQL database.

docker run --rm -d \
    --name jamf_mysql \
    --net jamfnet \
    -e MYSQL_ROOT_PASSWORD=jamfsw03 \
    -e MYSQL_DATABASE=jamfsoftware \
    -p 3306:3306 \
    mysql:5.7

The --rm argument it so delete the container once it stops. If you omit this then you can stop and restart containers while preserving their last state (going back to the issue of not preserving our MySQL data in a mounted volume: it would still persist while the container was stopped).

-d is short for --detach and will start the container in a new process without tying up the current shell. If you omit this you will remain attached to the running process and view the log stream.

These two options are specific to us treating this as a short lived test environment. In a production container deployment you wouldn’t even think about this because you won’t be deploying the containers with the docker command.

The --name argument provides a friendly name we can use to interact with our container instead of the randomized ones that will be generated otherwise. This is important when it comes to the internal networking to Docker.

Which, with --network​, the container will launch attached to our jamfnet network, and it will be discoverable via DNS by any other container in the same network. Our web app will be able to reach its database by connecting to mysql://jamf_mysql:3306.

The two -e arguments set the values for environment variables in the running container. Most configurable options for a containerized service are handled through environment variables. You can use the same image across multiple environments by changing the values that are passed.

In addition to the web app container, we’re going to want to be able to interact with the data being written to MySQL from our own command line or utilities. In order to do so, we must expose the service’s ports on the host. The -p or --publish argument is a mapping of host ports to container ports. MySQL communication is over port 3306 so we are mapping that port on our computer to the same port on our container.

Run the command and you will see a long randomized ID printed out. You can verify what containers are running by typing docker ps -a.

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                               NAMES
bd2e35ebcdd1        mysql:5.7           "docker-entrypoint.s…"   40 seconds ago      Up 39 seconds       0.0.0.0:3306->3306/tcp, 33060/tcp   jamf_mysql

To learn more about the MySQL Docker image, view the documentation on DockerHub: https://hub.docker.com/_/mysql

Connect Using MySQL CLI or MySQL Workbench

Now that MySQL is up an running we can connect to it either on the command line using mysql or with a GUI application such as MySQL Workbench.

If you want to use the CLI, you don’t need to install anything addtional. You already have everything you need with the mysql:5.7 Docker image!

docker run --rm -it \
    --net jamfnet \
    mysql:5.7 \
    mysql -u root -h jamf_mysql -p

There are a few new things here. Instead of running a service and detaching it, we are telling it to launch the container with an interactive terminal with the -it arguments (--interactive and --tty respectively).

We’re also passing a command after the name of the image we want to use. This overrides the entrypoint for the image (I’ve been referring to this as a “startup” script up until now). The entrypoint is the default command that runs for an image if another is not provided.

You can also see that we’ve attached to the jamfnet network again and are passing the name of our MySQL container as the host argument for mysql. If we didn’t have port 3306 exposed this method allows us to launch a shell to access private resources.

But, because we do have port 3306 exposed, we can run this container in a slightly different way.

docker run --rm -it \
    --net host \
    mysql:5.7 \
    mysql -u root -h 127.0.0.1 -p

The host network pretty much does as it sounds. This container will come up without network isolation that bridge networks provide and access resources much like other clients would. Here you can see we pass the localhost IP instead of the container name and the MySQL connection will be established.

Launch a Jamf Pro Container

We’re finally ready to launch the Jamf Pro web app container itself. The command for this is going to look very similar to what we did with MySQL.

docker run --rm -d \
    --name jamf_app \
    --net jamfnet \
    -e DATABASE_USERNAME=root \
    -e DATABASE_PASSWORD=jamfsw03 \
    -e DATABASE_HOST=jamf_mysql \
    -p 80:8080 \
    jamfpro:10.17.0

MySQL doesn’t talk to Jamf Pro, so naming it doesn’t really do anything for us here, but it does make managing the container using docker a little easier.

We have a different set of environment variables specific to our Jamf Pro image. Again, an image is a static artifact we use for a deployment. The deployment is customized through the use of environment variable values that are applied on startup (the entrypoint scripting). You could easily spin up numerous local Jamf Pro instances all pointing to their own unique databases just by switching out those values all from the one image.

We’re also passing theMySQL container name for the DATABASE_HOST like in our previous example with the mysql CLI.

There’s one difference with out -p argument. We’re mapping port 80 on our host to port 8080 of the container (which is what Jamf Pro uses when it comes up). This is a handy feature of publishing ports: you can effectively perform port forwarding from the host to the container. Instead of interacting with the web app at http://localhost:8080 in our browser we can just use http://localhost.

Run the command and you will again see another long randomized ID printed. Verify the running containers using docker ps -a as before.

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                               NAMES
382d3fd60afe        jamfpro:10.17.0     "/startup.sh"            24 seconds ago      Up 22 seconds       0.0.0.0:80->8080/tcp                jamf_app
bd2e35ebcdd1        mysql:5.7           "docker-entrypoint.s…"   43 minutes ago      Up 42 minutes       0.0.0.0:3306->3306/tcp, 33060/tcp   jamf_mysql

Access in Your Browser

Open up Safari, enter localhost into the address bar, and you’ll be greeted by the warm glow of a Jamf EULA.

Consistency, Repeatability

You’ve achieved your first major milestone: you’re running Jamf Pro in a containerized environment. Now, how do we take what we have done above and ensure we can repeat it several hundred (thousand) times exactly as we did now when we spun everything manually.

Infrastructure/configuration as code is the means to achieve this. The way you implement this is going to be different depending on where it is you’re deploying to. For Docker, the tool we want to use to define how to bring up Jamf Pro is Docker Compose (which I’ve presented about).

If you installed Docker Desktop you will already have this CLI tool available to you! Docker Compose uses YAML files to define the elements of your application (services, networks, volumes) and allows you to spin up and manage environments from them.

Here’s a Docker Compose file that automates everything we went through in this post.

version: "3"
services:
  mysql:
    image: "mysql:5.7"
    networks:
      - jamfnet
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: "jamfsw03"
      MYSQL_DATABASE: "jamfsoftware"
  app:
    image: "jamfpro:10.17.0"
    networks:
      - jamfnet
    ports:
      - "80:8080"
    environment:
      DATABASE_USERNAME: "root"
      DATABASE_PASSWORD: "jamfsw03"
      DATABASE_HOST: "mysql"
    depends_on:
      - mysql
networks:
  jamfnet:

Grab this file on my GitHub as a Gist: jamfpro-docker-compose.yml

Notice anything about the attributes on our services? Our definitions map almost 1:1 with the CLI arguments to docker run. The service’s key becomes the name which we are able to use as a reference just as before. In the app service (previously jamf_app) we are passing mysql as the DATABASE_HOSTNAME – Docker’s DNS magic continuing to do the work for us. There’s also a Docker Compose specific option in here, depends_on, that references it. This tells Docker Compose that the mysql service must finish starting before it brings up the app.

The beauty of Docker Compose is that it takes very little explaining to understand once you’ve already done some work with the docker CLI. From the manual commands we ran as a part of this post you can understand what the YAML file is defining and what will happen when we run it.

To do, save the above into to a docker-compose.yml file and run the following command from the same directory (I’m in a directory called build):

/build % docker-compose up --detach
Creating build_mysql_1 ... done
Creating build_app_1   ... done
/build %

Fast, consistent, and repeatable deployments from a single definition. To tear this stack down and clean up, run:

/build % docker-compose down
Stopping build_app_1   ... done
Stopping build_mysql_1 ... done
Removing build_app_1   ... done
Removing build_mysql_1 ... done
Removing network build_jamfnet
/build %

The Next Phase

From here your Jamf Pro server is up, running, and ready for whatever testing you have in store. Ideally, as you progress on this journey, the Docker image you use at this step would be the one that you are ultimately deploying to production. The running application itself is identical at each stage it moves through your deployment processes.

You’ve taken your first steps with running Jamf Pro containerized on your computer, but as alluded to in the beginning this is only an exercise in the basics. A production environment is not going to be a laptop with Docker installed (…at least, I certainly hope not). What you’re most likely looking at in that case is a managed container service in the cloud from one of the big three: Amazon Web Services, Google Cloud Platform, or Microsoft Azure.

If you’ve followed me long enough you’ll know that I’m an AWS developer. That’s where my production environment would be, and I can define in CloudFormation templates (AWS’s infrastructure as code implementation) the things I’m going to need:

  • Virtual Private Cloud
  • Application Load Balancer
  • Fargate Cluster
  • Aurora MySQL Database (with a twist)

So stay tuned for the next phase in our containerized journey:

“So You Want to Run Serverless Jamf Pro”

Author: Bryson Tyrrell

AWS serverless developer from the Twin Cities. Former benevolent Casper Admin at Jamf, helped cofound Twin Cities Mac Admins @MspMacAdmns,, avid Python coder.

One thought on “So You Want to Containerize Jamf Pro”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s