Public registries provide container images for most use cases but they might not cover all of them. That’s why container engines such as Podman & Docker and CLI tools like buildah provide utilities for creating custom container images.

The build steps are written in a plaintext file called Containerfile and parsed by container engines (or buildah) during the build process.

# Containerfile
FROM node:18-alpine
LABEL version="1.0" 
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

Containerfile Instructions

Steps inside the containerfile are defined using instructions such as FROM, RUN, ADD, COPY, etc. Container Engines go through the containerfile line-by-line and perform each step, stacking a new image layer on top of the previous one.


FROM instruction specifies the base layer container image. The base layer image could be chosen based on OS preference (like Fedora, Kali, etc.) or programming language preference (like gcc, python, etc.), or any other preference. It is mandatory to have a FROM instruction in the containerfile.


Variables for FROM instruction are defined using ARG.


After FROM instruction is executed, the value for the variable declared in ARG becomes inaccessible.


The Environment variables (used by other instructions or container at runtime) are declared using ENV.


The = operator between the variable name and its value is optional.


LABEL is used to add metadata for container images in the form of key-value pairs. Metadata usually contains the author’s name, author’s email, version of the container image, etc.


ADD instruction will copy files/directories from the source (host OS/URL/tarball) to the destination (container image). ADD allows permissions to be omitted on the copied files/directories with the --chown flag.

ADD --chown=user:group src/ dest

Builds are performed inside a build context directory on the host. ADD will fail if the source file/directory is outside the build context.


COPY is similar to ADD but it is limited to sources on the host, it doesn’t support URLs or tarball. COPY is sufficient for most use cases, so it is preferred over ADD.


WORKDIR changes the current working directory (inside the image) for the next instructions.


RUN instruction executes the shell command passed as an argument.

A temporary container is created to execute the shell command and the changes on the file system (after command execution) are layered on top of existing image layers. By default, the instruction RUN apt-get update -y will be executed as /bin/bash -c apt-get update -y.

SHELL instruction is used in containerfile to change the default shell for RUN, for example, SHELL ["/bin/zsh", "-c"] will change the default shell from bash to zsh.


EXPOSE specifies the network ports to be used by the container on runtime. The ports still have to be published when the container is provisioned.


CMD adds a shell command that will be executed on container startup. CMD instructions could be written as CMD param1 param2 or CMD ["/bin/executable", "param1", "param2"].

Adding a CMD instruction can define the behavior of the container on startup, for example, a webserver container should start the webserver automatically on startup by running the invocation command defined in CMD.


If the executable on container startup is expected to be the same in most conditions then it is preferable to add ENTRYPOINT over CMD. Parameters for the executable could be passed from CMD instruction.

ENTRYPOINT /bin/executable
CMD ["param1", "param2"]


To create a mount point inside the container, VOLUME instruction can be used. During runtime, the --volume flag is used to map a directory on the host to the mount point declared by VOLUME instruction. The data stored on the mount point will persist after container execution.


The command added in the ONBUILD instruction is executed when the built image is used as the base container image.

If container image container-image-b uses container-image-a as a base image, then the ONBUILD instruction defined in container-image-a will be triggered after FROM instruction is executed in container-image-b.

Execution of ONBUILD instruction

Execution of ONBUILD instruction

Using ONBUILD, the developers can ensure that any new image built upon their container image has the latest source code or that packages are up-to-date.

ONBUILD RUN git clone source-code-repo-link.git


USER instruction sets UID or GID during the build process.

Building container images using Docker

Docker and Podman provide build subcommand for building container images from containerfile.

docker build .

. will be substituted with the current working directory on the host.

By default Docker expects Dockerfile (instead of Containerfile) in the build context, --file flag could be used to pass the path to containerfile.

docker build --tag node-application:latest . --file ./Containerfile

Container Engines cache the built layers to save time and storage. Cached layers are reused in other image builds to disable this behavior --no-cache flag could be used with the build subcommand.

--rm flag ensures that the intermediate container images are removed once the build process is successful.

Adding files with sensitive data to .dockerignore will make sure that they aren’t included in the container image.

Building container images using buildah

buildah provides a bud subcommand for building container images from containerfile.

buildah bud --tag "webserver:latest" .

Images built by buildah could then be executed using Podman and Docker or any other OCI image-compatible container engine. Podman also has a build subcommand for building container images and it uses the same code as buildah.

buildah provides finer control over image building process that allows users to

  • Create base images using the scratch image, an image with a minimal footprint.
  • Mount a container’s root filesystem, useful for debugging container-related issues.
  • Create a container image from a working container, helpful if Containerfile isn’t accessible.
  • Execution of containerfile instructions using subcommands. For example, RUN instruction could be executed on a container image using buildah run. Similarly for COPY and ADD instructions.
  • Debug image building process by executing instructions manually with buildah.
  • Making ad-hoc changes to existing container images.
  • Automating build process.

Thank you for taking the time to read this blog post! If you found this content valuable and would like to stay updated with my latest posts consider subscribing to my RSS Feed.


About the Open Container Initiative
OCI Image Manifest Specification
Image Layer Filesystem Changeset
OCI Image Configuration
About storage drivers
Dockerfile reference
docker build
scratch container image
Buildah Tutorial 1
Buildah and Podman relationship