How to write if statements in a Dockerfile

Oct 5, 2024

One of the first things that you learn in programming is how to control the flow of your program based off some information stored in a variable. In other words, we want to be able to conditionally do something based off some value. This is called an if statement. If a condition is met, then do something; otherwise, do something else. If statements are essential for writing any useful and reusable block of code.

In Python for example, a simple if statement looks like this:

user_input = input()
if user_input == "apple":
    print("red")
elif user_input == "banana":
    print("yellow")
else:
    print("unknown")

In this program, we check if the user input is equal to a couple of known objects and then we print its color; otherwise, we print "unknown".

Unlike Python (or any other standard programming language), Dockerfile is not a programming language. Rather, a Dockerfile is more like a configuration file. It contains a set of instructions for the Docker program to interpret and build your Docker image. Still, there are benefits of conditionally doing something in a Dockerfile. You can imagine that it can help save duplicate code by conditionally installing various packages depending upon your build arguments instead of defining everything verbosely. A scenario where this might be helpful is where you want to have your commands run on different base images. It would be hard to maintain two separate Dockerfiles with all the same commands except for the first line.

To start, let's understand how we pass a user input when building a Dockerfile. The command looks like this:

docker buildx build --build-arg ARG_NAME=value .

Within a Linux based Dockerfile, each of our run commands use the Bourne shell (/bin/sh) to execute. So, we could write an if statement within the run command to conditionally install something. This would look something like this:

FROM debian:12
 
ARG ARG_NAME
 
RUN if [ "$ARG_NAME" = "apple" ]; then echo red; \
    elif [ "$ARG_NAME" = "banana" ]; then echo yellow; \
    else echo unknown; fi

While this works, your Dockerfile can be very hard to read if you need to run more complex commands or have a lot of different build arguments.

Let's go back to the user case where we want to run the same set of commands on different base images. Instead of hard coding the from statement, we can use our build argument to parameterize that. Say we wanted to build multiple images, one for Python 3.9 and one for Python 3.10. This could look like this:

ARG PYTHON_VERSION
FROM python:$PYTHON_VERSION
...

And could be built with these commands:

docker buildx build --build-arg PYTHON_VERSION=3.9 .
docker buildx build --build-arg PYTHON_VERSION=3.10 .

Finally, taking this a step further, let's see how we can solve our original problem of writing an if statement in a Dockerfile with multi-stage builds. In this example, we want to install our application on two different operating system flavors; thus, we want to conditionally run the OS specific commands on the appropriate base image, but we still want to run a shared set of commands to install our application on both operating systems. Given these requirements, our Dockerfile can look something like this:

Dockerfile
ARG ENVIRONMENT
 
# debian based image
FROM python:3.9 AS debian
 
RUN apt-get update && apt-get install -y --no-install-recommends package-name
 
# alpine based image
FROM python:3.9-alpine AS alpine
 
RUN apk update && apk add --no-interactive package-name
 
# our common commands
FROM $ENVIRONMENT AS base
 
# installing our requirements first
WORKDIR /workdir
COPY requirements.txt .
RUN pip install -r requirements.txt
 
# copying our source code
COPY setup.py .
COPY src src/
 
# installing our package
RUN pip install --no-deps .
 
# extra commands to run on our debian based image
FROM base AS debian-final
 
RUN echo this is in the debian image
 
# extra commands to run on our alpine based image
FROM base AS alpine-final
 
RUN echo this is in the alpine image
 
# final image to use
FROM $ENVIRONMENT-final

Let's break down the various layers in this Dockerfile. We have two initial base images, and we have used the AS keyword to name them.

# debian based image
FROM python:3.9 AS debian
 
# alpine based image
FROM python:3.9-alpine AS alpine

So, when we pass the ENVIRONMENT build argument as either debian or alpine, our next from statement uses one of the previously named stages.

FROM $ENVIRONMENT AS base

Just like our Python version example, now we have conditionally selected our image base. Then, we run a common set of commands in the next few layers. Finally, we define the final layer with our last stage by selecting our base named image.

# extra commands to run on our debian based image
FROM base AS debian-final
...
 
# extra commands to run on our alpine based image
FROM base AS alpine-final
...
 
# final image to use
FROM $ENVIRONMENT-final

Continue reading...