Podman Quadlet `.build`, `.image` and `.artifact` Units

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 5.2+ for .build and .image; Podman 5.7+ for experimental .artifact
Privilege sudo for rootful examples on the lab host
Scope Quadlet .build, .image, and .artifact preparation units — build context, ImageTag, referencing from .container, pull Policy=, image-backed volumes, experimental artifact pulls, oneshot service semantics, and version gates. Assumes Quadlet basics. Does not teach Containerfiles, podman build, generic image pulling, or full OCI artifact workflows.
Related guides Podman Quadlet container file

Before a .container service starts, something has to place the image or artifact on the host. Quadlet models that prep work as oneshot units: .build runs podman build, .image runs podman image pull, and .artifact runs podman artifact pull. Downstream units reference the Quadlet filename (web.build, base.image) and inherit systemd ordering automatically where the generator understands the suffix.

I tested on Podman 5.8.2. Confirm your build supports the unit types you need:

bash
podman --version

Sample output:

output
podman version 5.8.2
Unit type Minimum Podman Status on 5.8.2
.build 5.2 Stable
.image 5.x (Quadlet) Stable
.artifact 5.7 Experimental

Why these Quadlet types exist

All three follow the same prep-then-run pattern:

text
.build   → podman build   → local image tag
.image   → podman pull    → cached OCI image
.artifact → podman artifact pull → local artifact store

Then a workload unit consumes the result:

text
prepared resource
.container / .volume / other Quadlet

Each file becomes a *-build.service, *-image.service, or *-artifact.service oneshot that runs before dependents start.


Quadlet .build

A .build unit describes a podman build that tags an image on the host. The lab build lives beside its Containerfile under /etc/containers/systemd/:

Containerfile:

ini
FROM registry.access.redhat.com/ubi9/ubi-minimal
RUN echo 'quadlet build ok' > /build-marker.txt
CMD ["sleep", "3600"]

web.build:

ini
[Build]
ImageTag=localhost/quadlet-web:latest
File=Containerfile
SetWorkingDirectory=unit

Reload systemd after adding the file:

bash
sudo systemctl daemon-reload

Run the generated build service:

bash
sudo systemctl start web-build.service

The journal shows a successful tag when the build completes. Confirm the image exists locally:

bash
sudo podman images localhost/quadlet-web:latest --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}'

Sample output:

output
REPOSITORY             TAG         IMAGE ID
localhost/quadlet-web  latest      e2b4c77c0c67

.build required fields

A minimal .build unit requires ImageTag= plus a build context supplied by either File= or SetWorkingDirectory=. In the common local-layout example below, using both File=Containerfile and SetWorkingDirectory=unit makes the Containerfile and its relative COPY sources explicit.

  • ImageTag= — the tag podman build --tag receives (for example localhost/quadlet-web:latest)
  • File= — Containerfile path relative to the working directory
  • SetWorkingDirectory= — where podman build runs so COPY and ADD resolve correctly

Without ImageTag= plus File= or SetWorkingDirectory=, the generator cannot produce a usable web-build.service.


Build context with SetWorkingDirectory=

podman build needs a directory that contains the Containerfile and any files it references. SetWorkingDirectory=unit tells Quadlet to use the directory that holds the .build file — in this lab, /etc/containers/systemd/.

The generated unit sets WorkingDirectory and passes that path as the build context:

bash
systemctl cat web-build.service

Relevant excerpt:

output
WorkingDirectory=/etc/containers/systemd
ExecStart=/usr/bin/podman build --tag localhost/quadlet-web:latest --file Containerfile /etc/containers/systemd

SetWorkingDirectory=file uses the directory containing the File= path when the Containerfile lives in a subdirectory. SetWorkingDirectory= can also accept a Git URL for remote contexts — see podman-build.unit(5) when you outgrow a local directory layout.


Reference a .build from .container

Point Image= at the Quadlet source file, not only the tagged name:

ini
[Container]
Image=web.build
ContainerName=quadlet-web-run

After daemon-reload, the generated container unit pulls in the build service:

bash
systemctl cat web.service

Relevant excerpt:

output
Requires=web-build.service
After=web-build.service
ExecStart=/usr/bin/podman run ... localhost/quadlet-web:latest

Start the container service on the lab host:

bash
sudo systemctl start web.service

Verify the running container uses the built image:

bash
sudo podman ps --filter name=quadlet-web-run --format '{{.Names}} {{.Image}}'

Sample output:

output
quadlet-web-run localhost/quadlet-web:latest

That automatic Requires= / After= wiring is the main reason to use .build instead of a manual ExecStartPre=/usr/bin/podman build … in [Service].


Build caching

.build is a oneshot service — it runs podman build each time you start web-build.service. Podman still uses its layer cache when nothing changed. A second start on the lab host finished in about a second and reused the existing image ID instead of re-running every RUN step.

Cache invalidation rules belong in Build images with podman build. Here the takeaway is that repeated systemctl start web-build.service is not a full unconditional rebuild unless your Containerfile or context changed.


Build secrets and other useful options

Quadlet exposes high-value [Build] keys without turning this page into a podman build reference:

Key Role
Secret= BuildKit secret mount (id=, src=, env=, type=)
Pull= Base image pull policy during build
Target= Multi-stage build stage name
Network= Network mode for the build
Volume= Additional build volumes

Example with a file-based secret:

ini
[Build]
ImageTag=localhost/quadlet-web:latest
File=Containerfile
SetWorkingDirectory=unit
Secret=id=token,src=token.txt

Place token.txt in the build context directory. Full flag lists live in podman-build.unit(5).


Quadlet .image

A .image unit ensures an OCI image exists locally before dependents start:

ini
[Image]
Image=registry.access.redhat.com/ubi9/ubi-minimal:latest
Policy=missing

Reload and start the pull service:

bash
sudo systemctl daemon-reload

Pull the base image with the generated oneshot service:

bash
sudo systemctl start base-image.service

The generated command shows how Policy= maps to the CLI:

bash
systemctl cat base-image.service

Relevant excerpt:

output
ExecStart=/usr/bin/podman image pull --policy missing registry.access.redhat.com/ubi9/ubi-minimal:latest

Reference the unit from a container:

ini
[Container]
Image=base.image

Or from an image-backed volume (see below).


Image pull policy

Policy= on a .image unit controls the oneshot pull service. Podman 5.8.2 supports:

Policy Behavior
always Always pull; fail if the pull fails (default)
missing Pull only when the image is absent locally
never Use local image only; fail if missing
newer Pull when the registry digest differs from local storage

Policy=missing suits base images you mirror once per node. Policy=newer suits tags you want refreshed when the registry moves.

This is not AutoUpdate= on a .container file. Policy= runs when the image unit starts; AutoUpdate= governs a separate runtime update path for long-running services. Keep preparation and runtime refresh separate in your design.


Fully qualified image references

Use registry-qualified names in .image units:

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

Short names like ubi-minimal:latest depend on registries.conf search paths and can resolve differently across hosts. Fully qualified references make pull behavior predictable. Registry login and TLS belong in dedicated guides — wire AuthFile= or TLSVerify= only when your environment requires them.


Use .image with .volume

Image-backed volumes can reference a .image unit so the base layer exists before volume creation:

base.image:

ini
[Image]
Image=registry.access.redhat.com/ubi9/ubi-minimal:latest
Policy=missing

imgdata.volume:

ini
[Volume]
VolumeName=img-data
Driver=image
Image=base.image

Start the volume service:

bash
sudo systemctl start imgdata-volume.service

Quadlet adds the image dependency:

bash
systemctl cat imgdata-volume.service

Relevant excerpt:

output
Requires=base-image.service
After=base-image.service
ExecStart=/usr/bin/podman volume create --ignore --driver image --opt image=registry.access.redhat.com/ubi9/ubi-minimal:latest img-data

See Podman Quadlet volume and network for volume naming and mount references.


Quadlet .artifact

IMPORTANT
Artifact Quadlet units are experimental in Podman 5.8.2. Inputs, options, and generated service behavior may change between releases. Verify on your target Podman version before production use.

An .artifact unit runs podman artifact pull so non-image OCI content exists in the local artifact store before a workload needs it:

ini
[Artifact]
Artifact=localhost:5000/quadlet-config:v1
TLSVerify=false
Retry=3
RetryDelay=2s

The lab pushed quadlet-config:v1 to a local registry on port 5000. Production artifacts should use a fully qualified registry name such as quay.io/example/config:v1 with normal TLS.

Reload and start:

bash
sudo systemctl daemon-reload

Pull the artifact into local storage:

bash
sudo systemctl start config-artifact.service

On success the service is active (exited):

bash
systemctl show config-artifact.service -p Result,ExecMainStatus,ActiveState --no-pager

Sample output:

output
Result=success
ExecMainStatus=0
ActiveState=active

List artifacts in local storage:

bash
podman artifact ls

Sample output:

output
REPOSITORY                     TAG         DIGEST        CREATED         SIZE
localhost:5000/quadlet-config  v1          a03ee86942dc  19 seconds ago  14B

What is an OCI artifact here?

An OCI artifact packages files distributed through OCI registries — configuration bundles, policy data, or other non-container payloads you might otherwise bake into an image. Podman stores them separately from container images in the artifact store. This page only covers the Quadlet pull unit; extraction, mounts, and registry workflows belong in Manage OCI artifacts with podman artifact.


Artifact registry options

Useful [Artifact] keys map to podman artifact pull flags:

Key Purpose
AuthFile= Registry credentials file
TLSVerify=false Skip TLS verification (lab registries only)
Retry= Pull retry count
RetryDelay= Delay between retries

Do not duplicate registry login or certificate troubleshooting here — configure auth on the host, then reference AuthFile= when the default path is wrong.


Generated service type and oneshot status

.build, .image, and .artifact units are oneshot preparation services. Depending on the generated RemainAfterExit= setting, a successful unit may appear as inactive (dead) or active (exited). Neither state alone indicates failure; check Result and ExecMainStatus.

That does not mean the build or pull failed. Check:

  • systemctl show <unit> -p Result,ExecMainStatus
  • journalctl -u <unit>
  • podman images, podman artifact ls, or the resource the unit prepared

A failed pull shows Result=exit-code or failed with a non-zero ExecMainStatus — as with the lab's first localhost/quadlet-config:v1 attempt against HTTPS localhost without a registry.


Complete dependency example

These four files model a stack that prepares base image, application image, and config artifact before the container runs:

text
base.image      → pre-pull UBI base
web.build       → build application image (explicitly ordered after base-image.service)
config.artifact → pull config artifact (experimental)
web.container   → run service (Image=web.build)

Image=web.build automatically makes web.service depend on web-build.service, but Quadlet cannot infer that a FROM line inside the Containerfile corresponds to base.image. Wire that ordering explicitly on web.build:

ini
[Unit]
Requires=base-image.service
After=base-image.service

[Build]
ImageTag=localhost/quadlet-web:latest
File=Containerfile
SetWorkingDirectory=unit
Pull=never

Pull=never keeps podman build from independently re-pulling the UBI base that base.image already placed locally.

Add explicit [Unit] Requires=config-artifact.service only when your container truly needs the artifact present before start — Quadlet does not infer artifact dependencies unless you reference the .artifact file from a supported key on another unit.

Start only the container after all unit files are in place:

bash
sudo systemctl daemon-reload

Start the application container after prep units are defined:

bash
sudo systemctl start web.service

On the lab host, web.service and web-build.service were active after start. With Requires=base-image.service on web.build, starting web.service also runs base-image.service first. Wire artifact ordering manually when the application reads pulled artifact content at startup.


Troubleshooting

Symptom Likely cause Fix
Build fails with missing COPY source Wrong SetWorkingDirectory= or context path Use SetWorkingDirectory=unit when the Containerfile sits next to the .build file; verify files exist in that directory
web-build.service inactive (dead) or active (exited) Normal oneshot success Check Result=success and podman images for ImageTag=
Container starts before image exists Referenced raw image name instead of .image / .build Set Image=base.image or Image=web.build so Quadlet adds Requires=
.image pull every boot Policy=always (default) Set Policy=missing or Policy=newer for cached images
Artifact unit fails TLS Registry cert or localhost HTTPS mismatch Use a fully qualified registry host; set TLSVerify=false only for known-insecure lab registries
config-artifact.service failed Short artifact name or missing registry Use fully qualified Artifact= reference; confirm podman artifact pull works manually first

References


Summary

Quadlet .build, .image, and .artifact files front-load preparation as oneshot systemd services. A .build unit tags a local image from a Containerfile context — SetWorkingDirectory=unit keeps COPY paths aligned with the .build file location. Reference it from a container with Image=web.build and Quadlet adds Requires=web-build.service automatically.

.image units pull registry images with Policy= values (always, missing, never, newer) that are separate from container AutoUpdate=. Use fully qualified image names and reference base.image from containers or image-backed volumes when you need pull ordering without hand-written dependencies.

.artifact units are experimental on Podman 5.7+. They run podman artifact pull into the local artifact store; treat registry names, TLS, and retries explicitly. After success, preparation units may show inactive (dead) or active (exited) depending on RemainAfterExit= — read Result, journals, and podman images or podman artifact ls instead of expecting a continuously active service.


Frequently Asked Questions

1. What is the difference between a Quadlet .build and .image unit?

A .build unit runs podman build and tags a local image. An .image unit runs podman image pull according to Policy= so the image exists before a container starts. Use .build when you compile from a Containerfile; use .image when you consume a registry image as-is.

2. How do I reference a Quadlet build from a container unit?

Set Image=web.build in the Container section. Quadlet resolves the .build suffix, adds Requires= and After= for web-build.service, and passes the built ImageTag to podman run.

3. Is Policy= on a .image unit the same as AutoUpdate= on a container?

No. Policy= controls the oneshot image-pull service when the unit starts. AutoUpdate= is a separate runtime update workflow for long-running containers. They solve different lifecycle stages.

4. Why do build, image, and artifact Quadlet services show inactive or active (exited) after success?

Those units generate Type=oneshot preparation services. Depending on the generated RemainAfterExit= setting, a successful unit may appear as inactive (dead) or active (exited). Neither state alone indicates failure — check Result, ExecMainStatus, and the prepared image or artifact.

5. Are Quadlet .artifact units production-ready?

No. Artifact units are experimental in Podman 5.8.2. Inputs, options, and behavior may change. Test on your target Podman version and pin versions before relying on them in production.
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)