As data scientists, we often find ourselves juggling dependencies, environment setups, and reproducibility issues. One way to streamline this process is by using Docker (or Podman) to create containerized environments. However, repeatedly typing long docker build and docker run commands can be tedious. This is where magnificent GNU Make comes in handy—it allows us to automate these commands efficiently. And is a recipe for others reproducing our projects.
In this post, we’ll explore how to build, test, and run a Docker (or Podman) container using repeatable workflows with GNU Make
We will explain everything working with the following Makefile snippet to build and run Quarto in a container, which we described in a previous post.
.PHONY: all build test save load clean
# Detect whether podman or docker is available
ENGINE := $(shell command -v podman >/dev/null 2>&1 && echo podman || echo docker)
# Image name and tag
IMG_NAME := r-quarto
TAG := latest
IMG := $(IMG_NAME):$(TAG)
# Define build context
BUILD_CONTEXT := r-quarto
DOCKERFILE := $(BUILD_CONTEXT)/Dockerfile
# Ensure build dependencies exist
BUILD_DEPS := $(DOCKERFILE) $(BUILD_CONTEXT)/r-pkgs.txt $(BUILD_CONTEXT)/tex-pkgs.txt
# Default target (build and test)
all: build test
# Build the Docker image
build: $(BUILD_DEPS)
$(ENGINE) build -t $(IMG) -f $(DOCKERFILE) $(BUILD_CONTEXT)
# Run a test container interactively
test:
$(ENGINE) run --rm --user=root -it -v $(PWD):/home/dockeruser $(IMG) bash
# Save the image as a tarball for easy sharing
save:
$(ENGINE) save $(IMG) | gzip > $(IMG_NAME).tar.gz
@echo "Docker image saved as $(IMG_NAME).tar.gz"
# Load a previously saved image
load:
gunzip -c $(IMG_NAME).tar.gz | $(ENGINE) load
@echo "Docker image $(IMG) loaded"
# Remove the image (clean up)
clean:
$(ENGINE) rmi -f $(IMG)
@echo "Docker image $(IMG) removed"
Understanding the Makefile
Let’s break down the key components of our Makefile.
1. Declaring .PHONY Targets
.PHONY: all build test save load clean
- Declares phony targets, meaning these are not actual files but just labels for actions.
- Ensures that
makealways executes the commands for these targets, even if files with the same names exist.
2. Selecting the Container Engine
ENGINE := $(shell command -v podman >/dev/null 2>&1 && echo podman || echo docker)
- Checks whether
podmanis available. - If
podmanexists, it setsENGINE=podman; otherwise, it defaults todocker. - Ensures the same commands work regardless of whether Podman or Docker is installed.
3. Defining Variables
IMG_NAME := r-quarto
TAG := latest
IMG := $(IMG_NAME):$(TAG)
IMG_NAMEis the base name of the container image (r-quarto).TAGis set tolatest, meaning the most recent version.IMGcombines the two:r-quarto:latest.
BUILD_CONTEXT := r-quarto
DOCKERFILE := $(BUILD_CONTEXT)/Dockerfile
BUILD_DEPS := $(DOCKERFILE) $(BUILD_CONTEXT)/r-pkgs.txt $(BUILD_CONTEXT)/tex-pkgs.txt
- Defines the build context (where the Docker build files are located).
DOCKERFILEpoints tor-quarto/Dockerfile.BUILD_DEPSlists files required for building the image:Dockerfiler-pkgs.txt(likely a list of R packages)tex-pkgs.txt(likely a list of LaTeX packages)
4. Default Target (all)
all: build test
- Runs both
buildandtestwhen you just typemake.
5. Building the Image
build: $(BUILD_DEPS)
$(ENGINE) build -t $(IMG) -f $(DOCKERFILE) $(BUILD_CONTEXT)
- Builds the container image using Podman or Docker.
- Ensures required files (
BUILD_DEPS) exist before building. - Tags the image as
r-quarto:latest.
6. Running a Test Container
test:
$(ENGINE) run --rm --user=root -it -v $(PWD):/home/dockeruser $(IMG) bash
- Starts a container interactively (
-it) with a root user. - Mounts the current directory (
$(PWD)) inside the container at/home/dockeruser. - Runs a
bashshell. --rmremoves the container after exit.
7. Saving the Image to a Compressed Tarball
save:
$(ENGINE) save $(IMG) | gzip > $(IMG_NAME).tar.gz
@echo "Docker image saved as $(IMG_NAME).tar.gz"
- Exports (
save) the container image to a.tar.gzfile for easy sharing. - Uses
gzipto compress it. - Prints a confirmation message.
8. Loading a Saved Image
load:
gunzip -c $(IMG_NAME).tar.gz | $(ENGINE) load
@echo "Docker image $(IMG) loaded"
- Uncompresses (
gunzip -c) the saved.tar.gzfile. - Loads the image into Docker/Podman.
- Prints a confirmation message.
9. Cleaning Up (Removing the Image)
clean:
$(ENGINE) rmi -f $(IMG)
@echo "Docker image $(IMG) removed"
- Removes (
rmi -f) the image from the system. - Prints a confirmation message.
How to Use This Makefile
| Command | What it Does |
|---|---|
make | Builds and tests the image (make all) |
make build | Builds the container image |
make test | Runs an interactive test container |
make save | Saves the image as a .tar.gz file |
make load | Loads the image from .tar.gz |
make clean | Deletes the image from the system |
Final Thoughts
Using GNU Make with Docker (or Podman) provides a structured, repeatable, and automated way to manage container builds and executions. By defining targets in a Makefile, you simplify commands and ensure consistency in your development workflow.
🚀 Happy coding, and may your containers always build successfully! 🎯