Docker has exploded in popularity as the de facto standard for containerization and microservices. The 2020 StackOverflow survey found Docker usage grew from 35% in 2016 to 49% by 2020. And Statista reports over 100 million Docker image pulls per month from the public Hub in 2021.

With containerization now mainstream, Docker skills are highly in-demand.

One key Docker concept is the Dockerfile – a script containing instructions for automating image builds. Dockerfiles streamline creating ready-to-run container environments.

But basic Dockerfiles only allow linear sequences of instructions. To support dynamic behavior, we need conditionals that execute logic based on runtime values.

In this comprehensive 3500+ word guide, you‘ll learn expert techniques for integrating conditional logic in Docker build workflows.

We‘ll cover:

  • Real-world use cases for Dockerfile conditionals
  • Core IF/ELIF/ELSE syntax and implementation
  • Comparing conditionals to multi-stage builds
  • Best practices for debugging complex flows
  • Integrating BuildKit for advanced builds
  • Sample conditional snippets for diverse scenarios

So let‘s dive in to mastering next-level Dockerfiles!

Why Use Conditionals in Dockerfiles?

Before looking at syntax details, it helps to understand some motivating use cases. Here are common reasons for needing Dockerfile conditionals:

1. Specialize builds based on operating system

A core Docker advantage is portable images that run identically on any platform. But what works on Debian may not work on RHEL.

Conditionals allow customizing syntax based on the host OS:

ARG BASEOS=debian
RUN if [ "$BASEOS" = "debian" ]; \
     then apt-get update; \
     elif [ "$BASEOS" = "redhat" ]; \ 
     then yum update; fi

Now we can reuse the same Dockerfile on Ubuntu, CentOS, and more.

2. Optimize builds for dev vs production

During development, we want fast iterate cycles and debuggability. But production demands lean images with only essentials.

Conditionals can skip development tooling in prod:

ARG ENV=dev

RUN if [ "$ENV" = "dev" ]; then \
    apk add --no-cache gcc python3-dev; fi

This keeps prod slim by only installing build tools in dev.

3. Create configurable base images

Library maintainers can utilize conditionals to enable customization in downstream Dockerfiles that inherit a base.

For example:

FROM base AS build
ARG RUN_TESTS=true

ONBUILD COPY ./ /src
RUN if [ "$RUN_TESTS" = true ]; then \
    pytest /src; \ 
    fi

Now derived images control whether the ONBUILD step runs tests.

4. Implement multi-variant workflows

We can utilize conditionals to build several variants from a single Dockerfile.

An example with debug/release options:

ARG BUILD_TYPE=release

RUN if [ "$BUILD_TYPE" = "debug" ]; then \
    apt-get install -y gdb valgrind; \
    elif [ "$BUILD_TYPE" = "release" ]; then \
    apt-get install -y nginx; \
    fi

Now we get debug or release images from the same input by just changing BUILD_TYPE.

So in summary, Dockerfile conditionals enable use-case specific customizations. Next let‘s explore how they technically work.

Dockerfile Conditionals: Under the Hood

Now that we‘ve seen some motivating use cases, let‘s look at what‘s happening technically inside conditional logic.

The core Dockerfile instructions that facilitate conditionals are:

  • ARG – Defines a build-time variable
  • ENV – Sets an environment variable
  • IF/ELIF/ELSE – Controls logic flow based on bash tests
  • ONBUILD – Adds a trigger inherited by child images

To implement conditional logic, the Dockerfile interpreter runs something like:

1. Evaluate variable definitions from ARG or ENV

2. Process any IF/ELIF/ELSE blocks, evaluating bash test expressions

3. Execute subsequent RUN statements based on conditions matched

4. Repeat evaluate/execute until Dockerfile complete

Conceptually, it works much like:

Set myapp = "Go Program"   

IF myapp == "Go Program"     
    Compile with golang container
ELIF myapp == "Java Application"
   Compile with maven container 
END

Run appropriate container to build app type    

We first initialize variables, then route execution through conditionals, running relevant logic per first match.

Note: The expressions and tests occur at Docker build-time, not in running containers.

Understanding this flow helps debug unexpected Dockerfile behavior.

Now let‘s explore core syntax options for expressing conditions.

Dockerfile Conditional Syntax Options

The Dockerfile reference lists two instructions that facilitate conditions:

  • ONBUILD – Adds inheritance triggers
  • IF/ELIF/ELSE – Control flow based on bash tests

Let‘s start with the IF family, then see how ONBUILD fits in.

IF Instruction Syntax

The IF instruction is modeled after bash conditional syntax:

# Basic IF form  
IF expression 
   RUN do_something
  • expression can be any valid bash conditional test
  • do_something is usually a RUN command

Expressions commonly check:

  • File existence (-f, -d)
  • Environment variable values ($VAR)
  • Truthiness (true, false)

Some examples Dokerfile snippets:

# Inline variable check
RUN echo ${MYVAR:+Starting}

# String equality 
IF [ "$BUILDMODE" = "devel" ]
    RUN echo In Dev Mode

# Numeric comparisons  
IF [ $UID -lt 1000 ] 
    RUN echo Non-root user   

IF chains can also include ELIF (else if) and ELSE:

IF [ $USER = "root" ]
   RUN echo Root access
ELIF [ $USER = "admin" ]
   RUN echo Limited access
ELSE 
   RUN echo Restricted!   

Multiple conditions get evaluated until one matches.

available test operators

As mentioned, Docker leverages the bash test command to evaluate conditions. This exposes all the standard bash comparison operators:

File Checks:

  • -d FILE – File exists and is a directory
  • -e FILE – File exists
  • -f FILE – File exists and regular file

String Comparisons:

  • STRING1 = STRING2 – Equal
  • STRING1 != STRING2 – Not equal
  • -n STRING – String length > 0
  • -z STRING – String length is 0

Numeric Comparisons:

  • INT1 -eq INT2 – Equal
  • INT1 -ne INT2 – Not equal
  • INT1 -lt INT2 – Less than
  • INT1 -le INT2 – Less than or equal
  • INT1 -gt INT2 – Greater than
  • INT1 -ge INT2 – Greater than or equal

And more.

So between shell test operators, IF chains, and RUN commands, we unlock total control over Dockerfile logic flow.

Now let‘s examine how ONBUILD fits in.

ONBUILD conditional syntax

The ONBUILD instruction allows base images to define triggers executed in child images.

For example:

FROM node:12-slim AS base

ONBUILD COPY package.json /tmp
ONBUILD RUN npm install

Any Dockerfile that starts with FROM base will auto run the ONBUILD steps.

We can make these inheritable actions conditional too:

FROM python:3.8-slim AS base  

ARG RUN_LINTER=true

ONBUILD COPY requirements.txt .
ONBUILD RUN if [ $RUN_LINTER = true ]; then \
    pip install flake8 && flake8; fi  

Now derived Dockerfiles control whether ONBUILD runs the linter by overriding the arg.

So between IF and ONBUILD, we have techniques to branch conditionally at both buildtime and inherit-time.

Next we‘ll see how these concepts come together in common build patterns.

Dockerfile Conditionals vs Multi-Stage Builds

A best practice for optimizing Dockerfiles is multi-stage builds. The idea is to use multiple FROM statements to split discrete logical phases:

#################
## Build Stage ###
FROM maven AS build
WORKDIR /app
COPY . .
RUN mvn package

################## 
## Run stage #####
FROM java:8-jre-alpine
COPY --from=build /app/target/*.jar /opt/.
CMD ["java", "-jar", "/opt/app.jar"] 

The first FROM compiles Java with Maven. The second FROM copies the artifacts to run production. This keeps images lean.

However, multi-stage builds have limitations:

  • Still require a separate Dockerfile per "mode"
  • Increase Dockerfile complexity
  • Can‘t dynamically customize workflows

Conditionals provide an alternative approach to customizing pipelines.

Rather than separate Dockerfiles, we incorporate logic flows within a single pipeline.

For example:

ARG build_mode=prod

FROM maven AS build
WORKDIR /app
COPY pom.xml .

# Conditionally copy source and build 
RUN if [ "$build_mode" = "dev" ]; then \
    COPY src ./src && mvn package; fi

FROM java:8-jre-alpine  
COPY --from=build /app/target/*.jar /opt/app.jar
CMD ["java", "-jar", "/opt/app.jar"]

Now we can control copying source and compiling based on a single build_mode arg.

So conditionals offer flexibility similar to multi-build stages without extra Dockerfiles.

Debugging Conditional Logic Flows

However, with flexibility comes complexity. Too many nested IF chains can quickly become difficult to trace and debug.

Here are some tips for debugging complex Dockerfile conditionals:

1. Structure logic depth-first

Keep IF chains short and modular – evaluate defaults early:

IF [ $DEBUG = true ]; then
   # Debug logic
ELSE
   IF [ $MODE = "prod" ]; then
       # Production logic 
   ELSE  
      # All other cases
   END  
END

2. Use descriptive variable names

ARG install_monitoring=false

IF [ $install_monitoring = true ]; then
    # Install telegraf, influxdb, etc
END

3. Comment expected logic flow

# Install monitoring if flag enabled
# Otherwise skip monitoring packages
ARG monitor=false 

IF [ $monitor = true ]; then
   # (monitoring install steps)   
END

4. Break into reusable functions

Use RUN scripts to abstract complex installs:

RUN install_telegraf() {
    apk add telegraf
    # configure   
}

IF [ $install_monitoring ]; then
     install_telegraf  
END

5. Follow best practices

  • Properly indent IF/RUN blocks
  • Use \ line continuations in RUN
  • Double-quote variable references
  • Leverage intermediate containers

By carefully structuring logic, we can mitigate "Dockerfile sprawl".

Advanced: BuildKit Integration

For managing ultra complex flows, Docker recently introduced BuildKit integration.

BuildKit replaces Dockerfile internals with a standalone toolkit for building images. Benefits include:

  • Faster builds via concurrency and caching
  • Better layer control
  • State snapshotting
  • Dynamic ARG usage
  • Improved security model

Configuring BuildKit looks like:

# Enable BuildKit builds
export DOCKER_BUILDKIT=1 

# Build Dockerfile with BuildKit 
docker build --progress=plain -t myimage .

BuildKit requires Docker v17.09+ and daemon restart.

The syntax is backwards-compatible, but it unlocks more powerful functionality. For advanced use cases like machine learning model pipelines, BuildKit simplifies enormously complex flows.

Dockerfile Conditional Snippets

To help incorporate learnings into real configurations, I‘ve curated a Dockerfile "recipe book" below.

These snippets demonstrate conditionals for diverse use cases:

A) Base Image OS

ARG BASEOS=alpine
RUN if [ "$BASEOS" = "alpine" ]; \
    then apk add --update bash; \
    elif [ "$BASEOS" = "debian" ]; \
    then apt-get update; fi  

B) Build Artifacts

ARG BUILD_DOCS=false

COPY docs /tmp/docs
RUN if [ "$BUILD_DOCS" = true ]; then \
    pip install mkdocs && cd /tmp/docs && mkdocs build \
fi

C) Environment Variables

ENV NODE_ENV=production
RUN if [ "$NODE_ENV" = "development" ]; \
    then npm install --only=development; \ 
    fi

D) File Checks

RUN if [ ! -f /etc/myconfig ]; then \
    COPY mydefaultconfig /etc/; fi

E) Debug vs Release

ARG BUILD_TYPE=release

RUN if [ "$BUILD_TYPE" = "debug" ] \   
    then apk add --no-cache gdb valgrind; fi 

For additional real-world examples, check popular base images on Docker Hub such as python and node which utilize conditionals.

You can mix and match snippets as templates for your images.

Conclusion

In this comprehensive guide, we explored conditionals for creating advanced Dockerfile logic. Key takeaways include:

  • Conditionals help customize image pipelines based on args
  • Core syntax includes IF, ONBUILD, and bash tests
  • Alternatives like multi-build stages have tradeoffs
  • Clean flows require careful structure and planning
  • BuiltKit enables managing ultra complex Dockerfiles

While Dockerfile conditionals enable powerful workflows, also consider just using separate Dockerfiles per discrete pipeline. Evaluate complexity versus maintainability for your use case.

Overall, leveraging conditional logic where appropriate unlocks way more flexible Dockerized pipelines. You‘re now equipped with expert techniques to improve your image builds!

As next steps, consider more advanced Docker patterns like creating reusable base images and image inheritance.

Happy Dockerizing!

Docker logo

Image Source: Docker Blog

Similar Posts