Podman and Buildah can build container images that are more secure and efficient than those built with Docker.
Let’s see how this works. Imagine you want to build a Go application into a container.
# First, get your Go app source code
# Let's assume it's in a directory called 'go-app'
# and it has a main.go file.
# Create a basic Dockerfile (we'll improve this)
cat <<EOF > Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]
EOF
# Build it with Docker (for comparison)
# docker build -t my-go-app-docker .
# docker run --rm my-go-app-docker
# Now, let's do it with Podman and Buildah
# We'll use a multi-stage build approach for better security and smaller size.
# Stage 1: Build the Go application
BUILDER_CONTAINER=$(buildah from golang:1.21-alpine)
buildah copy $BUILDER_CONTAINER . /app
buildah run $BUILDER_CONTAINER go build -o /app/myapp .
IMAGE_ID=$(buildah commit $BUILDER_CONTAINER)
buildah rm $BUILDER_CONTAINER
# Stage 2: Create a minimal runtime image
RUNTIME_CONTAINER=$(buildah from alpine:latest)
buildah copy --from $IMAGE_ID $RUNTIME_CONTAINER /app/myapp /app/myapp
buildah config --cmd '["/app/myapp"]' $RUNTIME_CONTAINER
RUNTIME_IMAGE_ID=$(buildah commit $RUNTIME_CONTAINER)
buildah rm $RUNTIME_CONTAINER
# Tag and run the final image
podman tag $RUNTIME_IMAGE_ID my-go-app-buildah
podman run --rm my-go-app-buildah
This workflow uses Buildah’s granular control to achieve what a multi-stage Dockerfile does, but with more transparency. The builder stage uses a golang:1.21-alpine image to compile the Go application. buildah from creates a container from a base image, buildah copy transfers your source code, and buildah run executes the build command. buildah commit captures the state of the container as an intermediate image.
The second stage starts with a minimal alpine:latest image. buildah copy --from is the key here: it copies only the compiled binary from the intermediate image ($IMAGE_ID) into the new runtime image. This means the final image doesn’t contain the Go SDK or any source code, making it significantly smaller and more secure. buildah config --cmd sets the default command to run. Finally, buildah commit creates the final runtime image, which is then tagged with podman tag and can be run with podman run.
The core problem this solves is image bloat and security. Traditional single-stage builds often include build tools, development dependencies, and source code in the final image. This increases the attack surface and the image size. Multi-stage builds, as demonstrated with Buildah, create a clean separation between the build environment and the runtime environment. You compile your application in one isolated environment and then copy only the resulting artifact into a completely separate, minimal runtime environment. This results in smaller, more secure images because the final image only contains what’s absolutely necessary to run the application.
One powerful aspect of Buildah is its ability to work directly with image layers and mount points. You can even mount a container’s filesystem (buildah mount <container>) and directly manipulate files, add users, or change permissions as if it were a regular directory. This offers a level of control far beyond what a Dockerfile can express, allowing for highly customized and optimized image creation, such as adding specific security configurations or fine-tuning filesystem permissions without relying on RUN commands that might be less efficient or harder to inspect.
The next logical step is to integrate this into a CI/CD pipeline, potentially using buildah bud for a Dockerfile-like experience but with Buildah’s backend.