| 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:
containerfile-demo/
├── Containerfile
└── index.htmlindex.html:
<!DOCTYPE html><html><body><h1>Containerfile demo</h1></body></html>Containerfile:
FROM docker.io/library/nginx:latest
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80The 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:
podman build -t localhost/containerfile-demo .Sample 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:latestRun the container and map port 80 to the host:
podman run -d --name cf-demo -p 18080:80 localhost/containerfile-demoRequest the page:
curl -s http://localhost:18080/Sample output:
<!DOCTYPE html><html><body><h1>Containerfile demo</h1></body></html>Stop the demo container when you are done:
podman stop cf-demo && podman rm cf-demoYou 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:
FROM registry.access.redhat.com/ubi9/ubi-minimal:latestUse 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 yourselfFROM image AS builder— names a build stage for multi-stage imagesFROM 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:
RUN microdnf install -y curl && \
microdnf clean allCombine 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:
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
COPY app.conf /etc/myapp/app.confCOPY 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:
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:
podman build .The RUN ls step in the build log shows the archive was unpacked:
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.txtCOPY 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
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
ENV APP_ENV=productionENV 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
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:
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:
FROM registry.access.redhat.com/ubi9/ubi
RUN useradd -r -u 10001 appuser
USER 10001USER 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:
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:
#!/bin/sh
echo "Hello from ${1:-world}"Build the image:
podman build -t localhost/ep-demo .With no runtime arguments, CMD supplies the default:
podman run --rm localhost/ep-demoSample output:
Hello from containerArguments on podman run replace CMD, not ENTRYPOINT:
podman run --rm localhost/ep-demo --versionSample output:
Hello from --versionExec form vs shell form
Exec form runs the binary directly:
CMD ["/usr/bin/nginx", "-g", "daemon off;"]Shell form wraps the command in /bin/sh -c:
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
EXPOSE 8080EXPOSE documents which port the containerized service listens on. It does not publish the port on the host — you still need -p on podman run:
podman run -d -p 18080:8080 localhost/containerfile-demoThe left side is the host port; the right side matches the container port from EXPOSE.
VOLUME
VOLUME /dataVOLUME 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
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
HEALTHCHECK --interval=30s --timeout=3s \
CMD test -f /app/app.sh || exit 1Image-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:
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:
podman build -t localhost/multistage-demo .Start a throwaway container to see which stage output survived:
podman run --rm localhost/multistage-demoSample output:
built-in-builderInspect layer history — the final image does not include the builder RUN layer as a separate top-level step:
podman history localhost/multistage-demoSample 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... 109MBPublishing multi-architecture manifest lists is a separate workflow — see Create multi-architecture images with Podman.
Use .containerignore
.containerignore excludes paths from the build context:
.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:
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:
#!/bin/sh
echo "APP_ENV=${APP_ENV} APP_VERSION=${APP_VERSION}"Build with a version argument:
podman build --build-arg APP_VERSION=2.1.0 -t localhost/complete-demo .Run the container:
podman run --rm localhost/complete-demoSample output:
APP_ENV=production APP_VERSION=2.1.0ARG 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.

