Skip to main content

Embracing the Docker Lifestyle

·6 mins

I’m not a professional programmer, but I do a bit here and there to automate certain work tasks or build a web application. Before I discovered Docker, I usually tried to install the tools I needed directly on my Mac. This led to a number of problems:

  1. The libraries I needed varied from one project to another, and installing them directly on my Mac invariably led to version incompatibility as the version of one Python library conflicted with the version I needed for a different project.
  2. Homebrew was my tool of choice for installing most of the development tools I need that don’t come standard on my Mac. Theoretically, I could install a more recent version of Python homebrew and have it live in peace with the version installed on my Mac, but things never worked out quite that cleanly.
  3. I’ve used MAMP in the past to run Apache, PHP, etc. on my Mac, but that doesn’t mean the versions match up with what’s available on my webhost.

With the advent of better virtualization technologies, it became feasible to run a version of Linux on top of MacOS. Docker took it another step by isolating software inside containers within the Linux virtual machine. (Yes, there’s a bit of Russian matryoshka doll feel to this whole thing.)

Installing Docker #

The easiest way to get Docker going on a Mac computer is to install Docker Desktop. (Make sure you get the version for your Mac chip: Intel or M series.) Docker Desktop provides the underlying Docker tools with a useful GUI interface to manage your Docker images, containers, volumes, etc. That’s not the only way, however. You can also install Docker via homebrew and then use something like Rancher Desktop or Colima. I use Docker Desktop, and it’s been fine for me.

This isn’t meant to be a Docker tutorial. There are so many online, and scores of great YouTube videos as well. Here are some excellent ones.

And here’s a link to one of many good YouTube tutorials.

How I’m Using Docker #

Now that I’m fully onboard with the “Docker lifestyle,” I find that it has become my default approach to nearly every development project. The usefulness for building hosted applications was obvious from the start. If you’re working on PHP, Django, PostgreSQL, MySQL, or any other of a bunch of other tools, Docker is a no-brainer. Developing a complex web application on a laptop and then deploying it unchanged to a Linode server (my host of choice) is a huge win.

Building a small utility that I run on my Mac using Docker was less obvious to me at first. I was concerned that the overhead of starting up the image before running the program would make it too slow. I was wrong! When I have Docker Desktop running, the Linux VM and its kernel is already booted, and the extra overhead of launching the image is negligible. It would take longer if the image didn’t already exist and had to be built before running, but there’s no reason to delete an image that you use occasionally. They don’t take up much space.

Here is an example of how I’m using Docker in my own work.

Running a Python script #

As part of my coaching and leadership development business at Acuity Leadership Group, I often use the DISC tool to help folks understand some of their behavioral and communication tendencies. The combination of shell scripts with R and Python programs makes it easy and fast to generate charts and reports for my clients. A bit of the system is documented below.

Here’s the Dockerfile that creates the image that contains all my programs.

FROM timothydwilson/r-tidy:1.3.0

# Some useful ARGs
ARG APP_HOME="/alg"
ARG USER="r_user"

# Install required R packages
RUN R -e 'install.packages("fmsb", version = "0.7.3")'
RUN R -e 'install.packages("optparse", version = "1.7.1")'
RUN R -e 'install.packages("RColorBrewer", version = "1.1-2")'
RUN R -e 'install.packages("GGally", version = "2.1.2")'
RUN R -e 'install.packages("RSQLite", version = "2.2.11")'

# Install system packages
RUN apt update -qq && \
    DEBIAN_FRONTEND=noninteractive apt-get install -y \
    wkhtmltopdf

# Create a non-privileged user and set up the environment
WORKDIR $APP_HOME
RUN useradd -ms /bin/bash $USER && chown -R $USER $APP_HOME
USER $USER

RUN mkdir -p 01_code 02_data 03_output
COPY requirements.txt ${APP_HOME}/
RUN pip install -r requirements.txt
COPY src 01_code/

# Set some environment variables
ENV TZ=UTC

CMD ["bash", "./01_code/mk_reports.sh"]

There are a few things to notice about this Dockerfile.

  1. Right at the top you can see that I’m using a custom image. I use R regularly for anything related to statistics or making visualizations. I prefer using the Tidyverse collection of R libraries, so I made a Docker image that has a consistent version of R with the Tidyverse libraries already included. Working from a base image like this makes it quicker for me to start a new project.
  2. Even though I started with my base R image, there were some libraries that I needed to include for this specific project. You can see those in the section labeled “# Install required R packages.” In the next section I also install an extra Debian package.
  3. I create some directories in the image, copy over the requirements.txt file, and install the Python libraries with a pip install.
  4. Finally, when the container launches, I have it automatically run a shell script called mk_reports.sh to build the various charts or reports.

The mk_reports.sh script is pretty simple. It just runs different Python or R programs to do the main work based on the commands I pass in.

#!/bin/sh

# DISC assessment data location
data_dir="$HOME/ALG/DISC/data"

case $OPTION in
    "team")
        export GROUP_NAME=$TEAM_NAME_OR_PERSON_ID
        Rscript 01_code/team_disc.R
        ;;
    "person")
        export PERSON_ID=$TEAM_NAME_OR_PERSON_ID
        echo "Generating DISC report for ID $PERSON_ID"
        Rscript 01_code/individual_disc.R
        python3 01_code/one_pager.py
        ;;
    *)
        echo "Invalid option: '$1'."
        exit 0
        ;;
esac

The Docker container is launched from the separate shell script listed below.

#!/bin/sh

# Get and process commandline options, create needed directories, and 
# pass them on to the Docker image as environment variables.
data_dir="$HOME/SynologyDrive/ALG/DISC/data"

case $1 in
    "team")
        chart_dir="$HOME/Downloads/$2"
        mkdir -p "$chart_dir" ;;
    "person")
        chart_dir="$HOME/Downloads" ;;
    *)
        echo "Invalid option: '$1'."
        exit 0 ;;
esac

# Run the script
docker run -it --rm \
    -v "$data_dir":/alg/02_data \
    -v "$chart_dir":/alg/03_output \
    -e OPTION="$1" \
    -e TEAM_NAME_OR_PERSON_ID="$2" \
    timothydwilson/disc-tools:latest

The key part of this script is the final bit where the Docker container is launched. The -v flags connect the running container to a couple directories on my local machine; the -e flags set a couple environment variables that get passed to the container; and the image timothydwilson/disc-tools:latest is specified. Note that “timothydwilson” is my username at hub.docker.com; “disc-tools” is the name of the image; and “latest” indicates that docker should use the image tagged “latest.”

This is probably the most complicated example I have of utilizing Docker to run local programs. (Like I said, I’m not a professional programmer.) Because everything runs in a Docker container, I know that the versions of Python, R, and all the associated libraries will never change and is completely independent of anything else installed on my Mac. That’s a big win!