Building your own container images with Containerfile and Buildah: understanding the FROM, RUN, COPY, CMD, ENTRYPOINT, and HEALTHCHECK directives, building with podman build along with cache and build context, running buildah bud without a daemon, and executing multi-stage builds.

For eight episodes you've used ready-made images from a registry. Episode 9 teaches you how to build your own images — from writing a Containerfile, building with podman build, to using Buildah for a more granular, daemonless approach. This is the skill that turns you from an image consumer into an image producer.
Podman accepts two file names: Containerfile and Dockerfile. Both are text files with the same format; only the name differs. Containerfile is the preference of the Podman ecosystem, while Dockerfile exists for compatibility. Podman looks for Containerfile first, then Dockerfile.
The contents are a series of directives executed in sequence. Each directive produces one new layer in the image:
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Each layer stores the delta from the previous one — this layered structure is what enables build cache and layer sharing between images.
The most commonly used directives are explained in the table below:
| Directive | Function | Example |
|---|---|---|
FROM | Sets the base image | FROM python:3.12-slim |
RUN | Runs a command during build | RUN apt-get update |
COPY | Copies files from the build context | COPY . /app |
CMD | Default command when the container runs | CMD ["node", "app.js"] |
ENTRYPOINT | Fixed command that can't be overridden | ENTRYPOINT ["python"] |
HEALTHCHECK | Checks container health | HEALTHCHECK CMD curl -f ... |
FROM must be the first directive. RUN executes a command and stores the result as a layer. COPY copies files from the build context directory into the image.
The CMD and ENTRYPOINT pair is often confusing. ENTRYPOINT is the command that always runs and can't be replaced — it defines how the program is launched. CMD is the default argument that can be replaced by arguments in podman run. If ENTRYPOINT is set, the CMD value becomes the argument for ENTRYPOINT:
| Scenario | Execution result |
|---|---|
Only CMD | podman run image runs CMD; run arguments replace it |
Only ENTRYPOINT | Always runs ENTRYPOINT |
| Both | Runs ENTRYPOINT with CMD as the default arguments |
HEALTHCHECK tells Podman how to confirm a container is still healthy — useful for orchestration and monitoring, and will be revisited in the observability episode later.
The build command is simple enough; its most important part is the build context — the directory copied in as the build workspace and the source for COPY:
podman build -t myapp:latest .
podman build -t myapp:latest ./srcpodman build -t myapp:latest . builds an image tagged myapp:latest from the current directory as build context. -t can be used repeatedly for multiple tags at once. podman build -t myapp:latest . uses Buildah behind the scenes to carry out the build, so the process needs no daemon — consistent with the architecture you've learned since episode 1.
Layers that have already been built and haven't changed will be cached and reused. This makes the second build much faster:
podman build -t myapp:latest .
podman build -t myapp:latest . --cache-from myapp:latestThe cache works based on directive order: if a RUN changes, the layers after it are rebuilt too. So place directives that rarely change (like dependency installation) at the top, and those that change often (like COPY of application code) at the bottom — this simple strategy cuts build time dramatically.
A multi-stage build uses several FROM directives in one Containerfile. The final stage only takes the artifacts it needs, producing a much smaller final image:
FROM golang:1.24 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app/server .
FROM alpine:latest
COPY --from=builder /app/server /usr/local/bin/server
EXPOSE 8080
CMD ["server"]The builder stage compiles the Go program; the second stage copies the build output --from=builder into a compact Alpine-based image. The full toolchain isn't carried into the final image — this is the heart of multi-stage builds: the production image contains the result, not the build tools.
Tip
Use explicit distro tags, for example alpine:latest or golang:1.24, and update them regularly. Images that are never re-pulled accumulate old CVEs; the habit of rebuilding from an up-to-date base image is a cheap security layer.
Buildah is Podman's sibling focused on building images. Podman uses it internally, but Buildah can also be used directly in a more granular style. buildah bud is equivalent to podman build:
buildah bud -t myapp:latest .Beyond bud, Buildah offers an imperative style: each step is run one by one from a working container:
buildah from --name builder alpine:latest
buildah copy builder ./server /usr/local/bin/server
buildah run builder -- sh -c "chmod +x /usr/local/bin/server"
buildah commit builder myapp:latestbuildah from starts a working container, buildah copy copies files, buildah run executes a command, and buildah commit immortalizes the result as an image. This approach is useful when the build logic is too complex to represent as static directives — you control every step like a script.
| Tool | Main focus | Key commands |
|---|---|---|
| Podman | Runs and manages containers | podman build, podman run |
| Buildah | Builds images with granular control | buildah bud, buildah commit |
| Skopeo | Inspects and moves images | skopeo inspect, skopeo copy |
All three share the same OCI image format and storage, so images built by Buildah are directly used by podman run without conversion. podman build is essentially the convenient path into Buildah's capabilities.
Once the image is ready, verify it with commands you already know:
podman images myapp:latest
podman history myapp:latest
podman run -d -p 8080:8080 --name myapp myapp:latestpodman images confirms the image is registered, podman history shows the build layers — useful for assessing image size — and podman run runs it with the familiar pattern from episode 3.
Episode 9 covered building images: the difference between Containerfile and Dockerfile, the FROM, RUN, COPY, CMD, ENTRYPOINT, and HEALTHCHECK directives, building with podman build along with cache and build context, multi-stage builds for slim images, and Buildah as a more granular build path via buildah bud and imperative commands.
The key points to take home:
podman build — and can be used directly when you need full control.In the next episode, Episode 10, you'll take containers to the service level: Quadlet and systemd integration — declarative .container, .pod, .volume, and .image units, auto-start, restart policies, all the way to socket activation and podman generate systemd.