Podman Containerfile: Build Custom Container Images

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
Applies to Any Linux host with Podman installed
Privilege Rootful examples on the lab host; same instructions work rootless
Scope Containerfile and Dockerfile syntax — FROM, RUN, COPY, ADD, WORKDIR, ARG, ENV, USER, EXPOSE, VOLUME, LABEL, HEALTHCHECK, ENTRYPOINT, CMD, multi-stage builds, and brief .containerignore notes. Does not cover full podman build flags, cache tuning, registry workflows, multi-architecture manifests, or production hardening.
Related guides Manage images with podman images
Run containers with podman run
Install Podman on RHEL

A Containerfile is the recipe Podman follows to assemble a container image. Podman also accepts Dockerfile — the instruction syntax is the same. This page teaches what each instruction does and how they fit together. Build flags, caching, and context mechanics live in Build images with podman build.


Containerfile vs Dockerfile

Podman builds from either filename. podman build . automatically recognizes a file named Containerfile or Dockerfile in the build context. Use -f when the file has another name.

File When to use it
Containerfile Preferred on Podman-focused projects; signals container-engine-neutral intent
Dockerfile Widely recognized; works unchanged with podman build

The instructions inside are Dockerfile-compatible. Podman does not require a separate syntax for FROM, COPY, or CMD. Choosing Containerfile is a naming preference, not a feature split.

Rename an existing Dockerfile to Containerfile only when you want the neutral name — the build output is the same. Teams that share recipes with Docker users often keep Dockerfile so both tools find the file without extra flags.


Build a simple Podman Containerfile

Start with a complete working example before breaking down individual instructions. Create a directory with an HTML file and a Containerfile:

text
containerfile-demo/
├── Containerfile
└── index.html

index.html:

html
<!DOCTYPE html><html><body><h1>Containerfile demo</h1></body></html>

Containerfile:

dockerfile
FROM docker.io/library/nginx:latest
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80

The stock nginx image already starts its web server with the default command, so this example focuses on FROM, COPY, and EXPOSE rather than overriding startup behavior.

Build and tag the image:

bash
podman build -t localhost/containerfile-demo .

Sample output:

output
STEP 1/3: FROM docker.io/library/nginx:latest
STEP 2/3: COPY index.html /usr/share/nginx/html/index.html
--> 37d773819415
STEP 3/3: EXPOSE 80
COMMIT localhost/containerfile-demo
--> ae3d5f8ed489
Successfully tagged localhost/containerfile-demo:latest

Run the container and map port 80 to the host:

bash
podman run -d --name cf-demo -p 18080:80 localhost/containerfile-demo

Request the page:

bash
curl -s http://localhost:18080/

Sample output:

output
<!DOCTYPE html><html><body><h1>Containerfile demo</h1></body></html>

Stop the demo container when you are done:

bash
podman stop cf-demo && podman rm cf-demo

You now have an end-to-end reference. The sections below explain each instruction type in that file.


Choose a base image with FROM

Every stage begins with FROM:

dockerfile
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest

Use a fully qualified name when you care which registry and repository Podman contacts. Pin a tag (:1.2) or a digest (@sha256:...) when reproducibility matters.

Special forms:

  • FROM scratch — empty base for statically linked binaries you copy in yourself
  • FROM image AS builder — names a build stage for multi-stage images
  • FROM image@sha256:digest — pins the base to exact content regardless of tag movement

Each FROM starts a new stage with its own filesystem layer stack. Tag names such as latest can move; use a digest in production Containerfiles when reproducibility matters more than convenience.


Run build commands with RUN

RUN executes a command while the image is being built. The result becomes a new image layer:

dockerfile
RUN microdnf install -y curl && \
    microdnf clean all

Combine install and cleanup in one RUN when you can. A separate RUN microdnf clean all still leaves package metadata in an earlier layer.

RUN is not the command your container runs at startup — that is CMD or ENTRYPOINT. RUN only affects the built image filesystem.

For build-time secrets without baking credentials into layers, Podman supports:

dockerfile
RUN --mount=type=secret,id=mysecret ...

See Build images with podman build for --secret usage. Do not put passwords in plain RUN or ENV lines.


Copy files with COPY and ADD

Both instructions copy content from the build context into the image. Paths are relative to the context directory — files outside the context are not available. See Build images with podman build for context boundary errors.

COPY

dockerfile
COPY app.conf /etc/myapp/app.conf

COPY is the normal choice for files and directories from the build context.

ADD

ADD can also fetch remote URLs and auto-extract local archives (.tar, .tar.gz, and similar) into the destination path:

dockerfile
ADD archive.tar.gz /opt/extracted/
COPY file.txt /opt/file.txt
RUN ls -la /opt/extracted/

Only local archives are automatically extracted. If ADD downloads an archive from a URL, Podman copies the downloaded file as-is rather than unpacking it.

Build that Containerfile:

bash
podman build .

The RUN ls step in the build log shows the archive was unpacked:

output
STEP 3/4: RUN ls -la /opt/extracted/
/opt/extracted/:
total 4
drwxr-xr-x. 2 root root 22 Aug 22 16:56 .
drwxr-xr-x. 1 root root 22 Aug 22 16:56 ..
-rw-r--r--. 1 root root  8 Aug 22 16:56 file.txt

COPY places file.txt at the destination path without extraction logic.

Prefer COPY when you only need local files. Reach for ADD when you intentionally want archive extraction or URL fetch semantics.


Set the working directory and environment

WORKDIR

dockerfile
WORKDIR /app
COPY . .

WORKDIR sets the working directory for later instructions such as RUN, CMD, and ENTRYPOINT, and it also affects relative destination paths in instructions such as COPY and ADD. Source paths for COPY and ADD still come from the build context.

ENV

dockerfile
ENV APP_ENV=production

ENV sets key/value pairs stored in the image configuration. They are available to RUN steps that follow and to the process when a container starts.

ARG

dockerfile
ARG APP_VERSION=1.0.0
ENV APP_VERSION=${APP_VERSION}
Instruction Lifetime
ARG Build time only unless copied into ENV or a file
ENV Persists in the image and at container runtime

Pass build-time values with --build-arg:

bash
podman build --build-arg APP_VERSION=2.1.0 -t demo .

Do not use ARG or ENV for secrets. Build secrets and runtime secret objects have dedicated workflows.


Configure the container user

Full UBI includes useradd; ubi-minimal does not ship user-management tools by default. On a base image that provides them:

dockerfile
FROM registry.access.redhat.com/ubi9/ubi
RUN useradd -r -u 10001 appuser
USER 10001

USER sets the account for subsequent build instructions (where the instruction supports it) and for the default process when the container starts. A numeric UID is explicit on minimal images that may not ship /etc/passwd entries for your username.

Running as a non-root user is common when the application supports it. Full hardening guidance is outside this syntax reference.


CMD vs ENTRYPOINT

These instructions define what runs when a container starts. They answer different questions:

Instruction Role
ENTRYPOINT Defines the primary executable
CMD Supplies default arguments, or the default command when no ENTRYPOINT is set
Both together ENTRYPOINT executable plus CMD default arguments

Example:

dockerfile
FROM registry.access.redhat.com/ubi9/ubi-minimal
COPY greet.sh /usr/local/bin/greet.sh
RUN chmod +x /usr/local/bin/greet.sh
ENTRYPOINT ["/usr/local/bin/greet.sh"]
CMD ["container"]

greet.sh:

sh
#!/bin/sh
echo "Hello from ${1:-world}"

Build the image:

bash
podman build -t localhost/ep-demo .

With no runtime arguments, CMD supplies the default:

bash
podman run --rm localhost/ep-demo

Sample output:

output
Hello from container

Arguments on podman run replace CMD, not ENTRYPOINT:

bash
podman run --rm localhost/ep-demo --version

Sample output:

output
Hello from --version

Exec form vs shell form

Exec form runs the binary directly:

dockerfile
CMD ["/usr/bin/nginx", "-g", "daemon off;"]

Shell form wraps the command in /bin/sh -c:

dockerfile
CMD nginx -g "daemon off;"

Exec form avoids an extra shell process and is usually better for signal handling. Shell form can be convenient for environment substitution but makes PID 1 a shell unless you use a wrapper. Containers that must respond to SIGTERM for graceful shutdown should prefer exec form or an init wrapper — exit troubleshooting is a separate topic.


EXPOSE, VOLUME, LABEL, and HEALTHCHECK

These instructions describe image metadata and defaults.

EXPOSE

dockerfile
EXPOSE 8080

EXPOSE documents which port the containerized service listens on. It does not publish the port on the host — you still need -p on podman run:

bash
podman run -d -p 18080:8080 localhost/containerfile-demo

The left side is the host port; the right side matches the container port from EXPOSE.

VOLUME

dockerfile
VOLUME /data

VOLUME declares a path intended to be backed by externally managed or Podman-managed storage at runtime. It does not by itself publish, back up, or guarantee persistence of that data. Volume create, mount, and backup commands are separate topics.

LABEL

dockerfile
LABEL org.opencontainers.image.title="demo" \
      org.opencontainers.image.version="1.0"

Labels attach OCI-compatible metadata for tooling, policy engines, and registries. Common keys include org.opencontainers.image.source for the Git repository URL and org.opencontainers.image.licenses for SPDX license identifiers. Registries and CI pipelines can read labels without parsing the Containerfile again.

HEALTHCHECK

dockerfile
HEALTHCHECK --interval=30s --timeout=3s \
  CMD test -f /app/app.sh || exit 1

Image-level health checks can be overridden or disabled at runtime. Podman may warn that HEALTHCHECK is ignored for OCI-format output images on some builds; the instruction still documents intended probe behavior and works when the image format supports it. Detailed probe behavior and status fields are covered in Podman health checks.


Build multi-stage images

Multi-stage builds keep build tools out of the final image. A builder stage compiles or prepares artifacts; the final stage copies only what it needs. Compilers, header packages, and intermediate object files stay in the builder stage and do not ship to production tags. This pattern produces a small runtime image from a large build environment without maintaining two separate Containerfiles:

dockerfile
FROM registry.access.redhat.com/ubi9/ubi-minimal AS builder
RUN echo "#!/bin/sh" > /tmp/hello && \
    echo "echo built-in-builder" >> /tmp/hello && \
    chmod +x /tmp/hello

FROM registry.access.redhat.com/ubi9/ubi-minimal
COPY --from=builder /tmp/hello /usr/local/bin/hello
CMD ["/usr/local/bin/hello"]

Build and run:

bash
podman build -t localhost/multistage-demo .

Start a throwaway container to see which stage output survived:

bash
podman run --rm localhost/multistage-demo

Sample output:

output
built-in-builder

Inspect layer history — the final image does not include the builder RUN layer as a separate top-level step:

bash
podman history localhost/multistage-demo

Sample output:

output
ID            CREATED                 CREATED BY                                     SIZE
ced6369b9e34  Less than a second ago  /bin/sh -c #(nop) CMD ["/usr/local/bin/hel...  0B
<missing>     1 second ago            /bin/sh -c #(nop) COPY file:4357fc3d4e7bfd...  3.58kB
591c6dfb4400  4 days ago              /bin/sh -c #(nop) LABEL "org.opencontainer...  109MB

Publishing multi-architecture manifest lists is a separate workflow — see Create multi-architecture images with Podman.


Use .containerignore

.containerignore excludes paths from the build context:

text
.git
*.log
tmp/
secrets/

Ignored files are excluded from the build context seen by COPY and ADD, which can reduce unnecessary build input and prevent accidental copying of local secrets. Podman also reads .dockerignore; when both exist, .containerignore takes precedence.

Mechanics and failure examples are in Build images with podman build.


Complete Containerfile example

This example combines the instructions from earlier sections into one file you can adapt:

dockerfile
FROM registry.access.redhat.com/ubi9/ubi-minimal
ARG APP_VERSION=dev
ENV APP_ENV=production
ENV APP_VERSION=${APP_VERSION}
WORKDIR /app
COPY app.sh /app/app.sh
RUN chmod +x /app/app.sh
USER 10001
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s CMD test -f /app/app.sh || exit 1
ENTRYPOINT ["/app/app.sh"]
CMD []

app.sh:

sh
#!/bin/sh
echo "APP_ENV=${APP_ENV} APP_VERSION=${APP_VERSION}"

Build with a version argument:

bash
podman build --build-arg APP_VERSION=2.1.0 -t localhost/complete-demo .

Run the container:

bash
podman run --rm localhost/complete-demo

Sample output:

output
APP_ENV=production APP_VERSION=2.1.0

ARG supplied the build-time version, ENV carried it into the runtime environment, USER dropped privileges, and ENTRYPOINT ran the application script. Change APP_VERSION on the next build with --build-arg without editing the file, or override APP_ENV at runtime with podman run -e when you need a one-off environment.


Common Containerfile mistakes

Symptom Likely cause Fix
Container listens on a port but is unreachable from the host EXPOSE used without -p Add -p host:container on podman run
ARG value missing at runtime ARG is build-time only Copy into ENV or a config file when the running container needs the value
podman run arguments ignored ENTRYPOINT is set Runtime args replace CMD; override ENTRYPOINT with --entrypoint when you need a different executable
Image larger than expected after multi-stage build Accidental COPY from wrong stage or leftover builder layers in final tag Copy only required paths with COPY --from=builder

References

Summary

A Podman Containerfile uses the same instruction set as a Dockerfile. FROM selects the base, RUN mutates the filesystem during the build, and COPY or ADD brings context files into the image. ARG and ENV look similar but differ in lifetime — build-time versus runtime. ENTRYPOINT and CMD together define the process that starts when you run the container; runtime arguments replace CMD, not ENTRYPOINT.

EXPOSE documents ports but does not publish them. Multi-stage builds copy artifacts from a named builder stage so compilers and headers never ship in the final tag. Keep secrets out of plain instructions and use build secrets or runtime secret objects instead.

Once the Containerfile is written, podman build turns it into an image. For context rules, caching, and build flags, open the build guide. For tags, cleanup, and inspection after the build, see the image-management lesson. When the image is ready to ship, registry login and push workflows are separate from the Containerfile itself.


Frequently Asked Questions

1. What is the difference between a Containerfile and a Dockerfile?

Podman builds both with the same Dockerfile-compatible syntax. Containerfile is the preferred name when you want container-engine-neutral wording. podman build detects Containerfile or Dockerfile in the build context automatically.

2. Should I use COPY or ADD in a Containerfile?

Use COPY for files and directories from the build context. Use ADD when you need archive extraction or remote URL fetch behavior. For ordinary local files, COPY is clearer and more predictable.

3. What is the difference between CMD and ENTRYPOINT?

ENTRYPOINT sets the main executable the container runs. CMD supplies default arguments to that executable, or the default command when ENTRYPOINT is not set. Arguments on podman run replace CMD, not ENTRYPOINT.

4. What is the difference between ARG and ENV?

ARG exists only at build time and does not automatically persist in the running container. ENV sets image configuration that remains available when the container starts unless overridden at runtime.

5. Does EXPOSE publish a port on the host?

No. EXPOSE documents which container port the image expects. Publishing to the host still requires runtime flags such as -p on podman run.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)