Skip to content

Building Images

A container image is built from a set of instructions written in a text file. That file is called a Dockerfile. Despite the name, Podman uses the exact same Dockerfile format. You do not need Docker installed.

Think of a Dockerfile as a recipe. Each instruction adds a layer to your image. The final result is a portable snapshot that anyone can run.

Create a new directory for this example. Then create a file named Dockerfile inside it.

Terminal window
$ mkdir samtools-image
$ cd samtools-image

Write the following into Dockerfile:

# Start from Ubuntu 24.04
FROM ubuntu:24.04
# Install samtools
RUN apt-get update && \
apt-get install -y samtools && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Set the working directory
WORKDIR /data
# Default command
CMD ["samtools", "--version"]

Let’s walk through each line.

  • FROM ubuntu:24.04 sets the base image. Every Dockerfile starts with FROM. This pulls Ubuntu 24.04 as the foundation.
  • RUN apt-get update && ... executes commands during the build. Here it installs samtools. The && chains keep everything in a single layer.
  • WORKDIR /data sets /data as the working directory. Any subsequent commands will run from this location.
  • CMD ["samtools", "--version"] defines the default command. When someone runs this image without specifying a command, it will print the samtools version.

Every Dockerfile begins with FROM. It specifies which base image to start from.

FROM ubuntu:24.04

Always use a specific tag like 24.04 or 22.04. Never use latest. The latest tag can change without warning and break your builds. Pinning a version keeps your image reproducible.

RUN executes a command during the build process. It is most commonly used to install software.

RUN apt-get update && \
apt-get install -y samtools bwa fastqc && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

Chain related commands with && on a single RUN line. Each RUN creates a new image layer. Fewer layers mean smaller images.

COPY transfers files from your local machine into the image.

COPY run_analysis.sh /opt/scripts/run_analysis.sh

The first argument is the local path relative to the build context. The second argument is the destination inside the image.

WORKDIR sets the working directory for all instructions that follow.

WORKDIR /data

If the directory does not exist, it will be created automatically. This is cleaner than using RUN mkdir /data && cd /data.

ENV sets environment variables that persist when the container runs.

ENV PATH="/opt/tools/bin:${PATH}"
ENV THREADS=4

This is useful for adding custom tools to the PATH or setting default parameters.

CMD specifies the default command that runs when a container starts. Each Dockerfile should have exactly one CMD.

CMD ["fastqc", "--help"]

Users can override this by passing a different command at runtime.

From the directory containing your Dockerfile, run:

Terminal window
$ podman build -t samtools-image .
  • -t samtools-image assigns a name and tag to the image. Without -t, you would have to reference the image by its ID.
  • . is the build context. It tells Podman to look for the Dockerfile in the current directory. All COPY paths are relative to this location.

The build will download the Ubuntu base image, run the installation commands, and produce a final image. You will see output for each step.

Terminal window
$ podman build -t samtools-image .
STEP 1/4: FROM ubuntu:24.04
STEP 2/4: RUN apt-get update && ...
STEP 3/4: WORKDIR /data
STEP 4/4: CMD ["samtools", "--version"]
Successfully tagged localhost/samtools-image:latest

Suppose you want a portable environment for differential expression analysis. You need R, BiocManager, and DESeq2. You also want to include your own analysis script.

First, create a simple R script named run_deseq2.R:

library(DESeq2)
counts <- read.csv("/data/counts.csv", row.names = 1)
coldata <- read.csv("/data/coldata.csv", row.names = 1)
dds <- DESeqDataSetFromMatrix(
countData = counts,
colData = coldata,
design = ~ condition
)
dds <- DESeq(dds)
res <- results(dds)
write.csv(as.data.frame(res), "/data/results.csv")
message("DESeq2 analysis complete.")

Now write the Dockerfile:

FROM rocker/r-ver:4.4.0
# Install system dependencies for R packages
RUN apt-get update && \
apt-get install -y \
libcurl4-openssl-dev \
libxml2-dev \
libssl-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install BiocManager and DESeq2
RUN R -e "install.packages('BiocManager', repos='https://cloud.r-project.org')" && \
R -e "BiocManager::install('DESeq2', ask=FALSE)"
# Copy the analysis script
COPY run_deseq2.R /opt/scripts/run_deseq2.R
# Set working directory for data
WORKDIR /data
# Run the script by default
CMD ["Rscript", "/opt/scripts/run_deseq2.R"]

Build it:

Terminal window
$ podman build -t deseq2-image .

This build will take several minutes. R packages and their dependencies are large. Once complete, anyone with this image can run your exact DESeq2 analysis without installing anything.

Podman caches each layer of a Dockerfile. If a layer has not changed, Podman reuses the cached version instead of rebuilding it. This speeds up repeated builds significantly.

The key rule: put things that change rarely at the top. Put things that change often at the bottom.

System packages rarely change. Your analysis scripts change frequently. Structure your Dockerfile accordingly.

FROM ubuntu:24.04
# Rarely changes: install system packages first
RUN apt-get update && \
apt-get install -y samtools bwa && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Changes often: copy your script last
COPY pipeline.sh /opt/scripts/pipeline.sh
CMD ["bash", "/opt/scripts/pipeline.sh"]

If you edit pipeline.sh and rebuild, Podman will skip the package installation step entirely. It will only rerun the COPY and everything after it. This can save minutes on each rebuild.

Container images can grow large quickly. A few practices help keep them small.

Always remove package caches in the same RUN command as the installation. If you put the cleanup in a separate RUN, the cache files still exist in a previous layer.

# Good: clean up in the same RUN command
RUN apt-get update && \
apt-get install -y samtools && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Bad: cleanup in a separate layer (files still exist in the previous layer)
RUN apt-get update && apt-get install -y samtools
RUN apt-get clean

Each RUN instruction adds a layer. Combine related installation steps into a single RUN to minimize layers.

# Better: one layer
RUN apt-get update && \
apt-get install -y samtools bwa fastqc && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Worse: three layers
RUN apt-get update && apt-get install -y samtools
RUN apt-get install -y bwa
RUN apt-get install -y fastqc

When possible, use minimal base images. Alpine Linux images are much smaller than Ubuntu. However, some bioinformatics tools have dependencies that make Alpine impractical. Choose the smallest base that works for your tools.

To see all images on your system:

Terminal window
$ podman images
REPOSITORY TAG IMAGE ID SIZE
localhost/deseq2-image latest a1b2c3d4e5f6 1.82 GB
localhost/samtools-image latest f6e5d4c3b2a1 145 MB
docker.io/library/ubuntu 24.04 1234abcd5678 78.1 MB

To remove an image you no longer need:

Terminal window
$ podman rmi samtools-image

To remove all unused images at once:

Terminal window
$ podman image prune -a

Be careful with prune -a. It removes every image that is not currently used by a running container.

Instruction Purpose Example
FROM Set base image FROM ubuntu:24.04
RUN Run a command during build RUN apt-get install -y samtools
COPY Copy files into image COPY script.sh /opt/script.sh
WORKDIR Set working directory WORKDIR /data
ENV Set environment variable ENV THREADS=4
CMD Default run command CMD ["samtools", "--version"]
podman build -t name . Build an image podman build -t my-image .
podman images List all images
podman rmi name Remove an image podman rmi my-image

Now that you can build images, the next page covers Running Containers and working with them interactively.