| 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 and rootless examples; system drop-ins need administrator access |
| Scope | How Podman loads containers.conf — search paths, drop-in precedence, [containers], [network], [engine], and [secret] highlights, CONTAINERS_CONF and CONTAINERS_CONF_OVERRIDE, opt-in --module profiles, string-array {append=true}, and verification with podman info and container inspection. Does not cover storage.conf, registries.conf depth, full secrets workflows, or exhaustive every-key reference. |
Repeating the same flags on every podman run gets old fast. containers.conf is where Podman reads persistent defaults — environment variables, capability sets, logging, network tooling, event destinations — before your command-line options apply. This guide maps the file locations, shows which layer wins when several files disagree, and covers modules and append syntax that rarely appear in thin tutorials.
If you only need one persistent tweak, a drop-in under containers.conf.d/ is usually enough. Reach for CONTAINERS_CONF, modules, or append markers when you are debugging precedence, running CI with isolated settings, or building reusable profiles that should not load on every host by default.
What is containers.conf?
containers.conf supplies default configuration consumed by Podman and other tools in the containers/common stack. Think of the flow as:
containers.conf defaults
│
▼
Podman command
│
+ command-line flags
▼
effective runtime configurationCommand-line flags override many configured defaults for a single invocation. The file shapes what happens when you omit those flags — default environment, capabilities, log driver, rootless network command, events logger, runtime selection, and secret driver backend.
Registry hostname routing lives in registries.conf; disk layout lives in storage.conf. This page stays on containers.conf behavior only.
Find the active containers.conf files
Configuration is layered. Typical paths, from vendor baseline through administrator and user overrides:
| Layer | Path |
|---|---|
| Vendor | /usr/share/containers/containers.conf |
| Administrator main | /etc/containers/containers.conf (optional — many hosts use drop-ins only) |
| Administrator drop-ins | /etc/containers/containers.conf.d/*.conf |
| Rootless-specific | /etc/containers/containers.rootless.conf, containers.rootless.d/*.conf, containers.rootless.d/$UID/*.conf |
| User main | $XDG_CONFIG_HOME/containers/containers.conf or ~/.config/containers/containers.conf |
| User drop-ins | ~/.config/containers/containers.conf.d/*.conf |
On this lab host the vendor file exists but /etc/containers/containers.conf does not — only drop-ins under containers.conf.d/ customize the system layer.
Rootful and rootless sessions do not always read identical paths. Rootless Podman may also load /etc/containers/containers.rootless.conf and files under containers.rootless.d/, including per-UID directories when your distribution ships them. If a system-wide drop-in changes rootful behavior but a rootless user sees something else, compare paths as that user before assuming the administrator file was ignored.
List what is present before you edit:
ls -la /usr/share/containers/containers.conf /etc/containers/containers.conf /etc/containers/containers.conf.d/ 2>/dev/nullThe vendor file is long but mostly commented examples. Read it once to see which keys your distribution ships disabled by default — pasta networking, compose provider order, and capability lists are often documented there with # prefixes you can copy into a drop-in instead of inventing syntax from memory.
Later-loaded values override earlier ones for the same key within the normal merge rules. The exact merge order is documented in containers.conf(5); the sections below prove behavior with podman info rather than only listing directories.
Prefer drop-ins for small overrides
Copying the entire vendor file into /etc/containers/containers.conf makes upgrades painful. Add a small drop-in instead:
/etc/containers/containers.conf.d/90-custom.confExample:
[engine]
events_logger = "journald"Drop-ins keep local changes visible, preserve vendor defaults you did not touch, and make rollback a single rm of your file. Per-user tweaks belong under ~/.config/containers/containers.conf.d/ when you need settings that apply only to one account.
Numeric prefixes such as 90-custom.conf are a convention, not a Podman requirement. Files in containers.conf.d/ load in lexical order, so 10-foo.conf merges before 90-bar.conf. When two drop-ins set the same key, the later filename wins unless a more specific layer (user config, environment variable, or command flag) overrides it afterward.
See configuration precedence in practice
Layering is easier to trust after you watch one field change. Add a system drop-in:
# /etc/containers/containers.conf.d/90-events.conf
[engine]
events_logger = "journald"Add a user drop-in that sets the same key differently:
# ~/.config/containers/containers.conf.d/10-user-events.conf
[engine]
events_logger = "file"Read the effective value Podman reports:
podman info --format '{{.Host.EventLogger}}'Sample output when the same user's ~/.config/containers/... drop-in is present:
fileUser configuration is loaded after administrator configuration, so the user value wins. Verify with podman info rather than assuming file order alone.
[containers] defaults that matter
The [containers] table sets defaults for every container unless a flag overrides them. You do not need every key from the man page — these are the ones administrators touch most often:
| Key | Controls |
|---|---|
env |
Default environment variables injected into containers |
default_capabilities |
Capability set added when --cap-add / --cap-drop are not used |
pids_limit |
Process limit inside the container |
log_driver |
Logging backend (k8s-file, journald, etc.) |
tz |
Container timezone |
umask |
Umask for container processes |
label |
SELinux label behavior |
netns |
Default network namespace mode |
userns |
Default user namespace mode |
volumes |
Default volume mounts |
devices |
Default device nodes |
init |
Whether an init process is inserted |
dns_servers, dns_searches, dns_options |
Default resolver settings written into container /etc/resolv.conf |
This article demonstrates env and points at the others so you know which table to open — not as a hardening checklist.
A pids_limit drop-in shows the same pattern as environment defaults. Add to a drop-in:
[containers]
pids_limit = 512The CLI equivalent for one container is podman run --pids-limit 512. Podman does not print the limit in podman info; start a short-lived container and read the stored limit:
podman run -d --name pids-demo registry.access.redhat.com/ubi9/ubi-minimal:latest sleep 120HostConfig.PidsLimit in inspect output is the value Podman applied at create time:
podman inspect pids-demo --format '{{.HostConfig.PidsLimit}}'Sample output when the drop-in is active:
512If you omit pids_limit from containers.conf, Podman falls back to vendor defaults or leaves the limit unset depending on version and cgroup configuration.
Set default environment variables
Add a drop-in with a default environment entry:
# /etc/containers/containers.conf.d/91-env.conf
[containers]
env = ["APP_ENV=production"]Run a container that prints the variable (the lab uses a minimal image with printenv):
podman run --rm registry.access.redhat.com/ubi9/ubi-minimal:latest printenv APP_ENVSample output:
productionA command-line -e overrides the default for that run:
podman run --rm -e APP_ENV=staging registry.access.redhat.com/ubi9/ubi-minimal:latest printenv APP_ENVSample output:
stagingDefaults come from containers.conf; explicit flags win for the invocation you are running.
[network] defaults
The [network] table controls networking defaults such as the network backend, subnet pools, network configuration paths, and rootless networking tooling. RHEL 10 ships with pasta commented in the vendor file:
#default_rootless_network_cmd = "pasta"Confirm the effective network settings Podman is using:
podman info --format 'rootlessNetworkCmd={{.Host.RootlessNetworkCmd}} networkBackend={{.Host.NetworkBackend}}'Sample output:
rootlessNetworkCmd=pasta networkBackend=netavarkOther keys you may see in production configs include network_config_dir, default_subnet_pools, and pasta_options. DNS defaults such as dns_servers, dns_searches, and dns_options belong under [containers], not [network].
To pin rootless networking explicitly, uncomment or add in a drop-in:
[network]
default_rootless_network_cmd = "pasta"After a change under [network], restart long-running rootless pods or verify with a fresh podman run — existing network namespaces keep the settings they were created with. Full networking walkthroughs belong in Podman networking — here the point is which table owns those defaults.
[engine] defaults
The [engine] table contains defaults for Podman engine behavior:
| Key | Purpose |
|---|---|
events_logger |
Where lifecycle events are recorded (file, journald, …) |
runtime |
OCI runtime path (typically crun or runc) |
helper_binaries_dir |
Directory for network and utility helpers |
service_timeout |
Remote service timeout |
compose_providers |
Ordered list for podman compose provider selection |
hooks_dir |
OCI hook directories |
podmansh_timeout |
Timeout for podmansh login shells |
Example drop-in:
[engine]
events_logger = "journald"Verify:
podman info --format '{{.Host.EventLogger}}'The precedence section above used this same field. compose_providers is covered next because it is a common practical tweak.
Compose provider configuration
podman compose is a wrapper that delegates to an external Compose implementation. The vendor file documents:
#compose_providers=[]When unset, Podman picks an available provider (often preferring docker-compose over podman-compose when both exist — behavior can vary by version). Set explicit order in a drop-in:
[engine]
compose_providers = ["podman-compose", "docker-compose"]Decision tables, socket workflows, and provider differences live in Podman Compose. containers.conf only names which provider to try first.
[secret] and [secret.opts]
Default secret backend selection also lives here:
[secret]
driver = "file"That changes which backend Podman uses when you do not pass --secret driver flags. Creating and mounting secrets is covered in Manage Podman secrets.
Replace normal config with CONTAINERS_CONF
CONTAINERS_CONF is stronger than adding another drop-in. When set, Podman uses only that file as the normal configuration source — system and user containers.conf trees are ignored.
Create an isolated test file:
# /tmp/test-containers.conf
[engine]
events_logger = "file"
[containers]
env = ["LAB_MARKER=containers_conf_only"]Point Podman at it:
export CONTAINERS_CONF=/tmp/test-containers.confCheck the events logger:
podman info --format '{{.Host.EventLogger}}'Sample output:
fileConfirm a variable from the test file is present while system drop-ins are ignored:
podman run --rm registry.access.redhat.com/ubi9/ubi-minimal:latest printenv LAB_MARKERSample output:
containers_conf_onlyUnset the variable when you finish testing — otherwise every Podman command keeps using the isolated file:
unset CONTAINERS_CONF CONTAINERS_CONF_OVERRIDEForgetting to unset CONTAINERS_CONF can make administrator and user drop-ins appear to be ignored.
Add a final override with CONTAINERS_CONF_OVERRIDE
CONTAINERS_CONF_OVERRIDE loads one more file after everything else in the normal merge, including when CONTAINERS_CONF is set.
With CONTAINERS_CONF=/tmp/test-containers.conf still exported, create:
# /tmp/final.conf
[engine]
events_logger = "journald"Apply the override:
export CONTAINERS_CONF_OVERRIDE=/tmp/final.conf
podman info --format '{{.Host.EventLogger}}'Sample output:
journaldThe override file changed events_logger from file to journald even though CONTAINERS_CONF alone specified file. Use this in CI or debugging when you need one forced field without rebuilding a full config tree.
Compare CONTAINERS_CONF and CONTAINERS_CONF_OVERRIDE
| Variable | Behavior |
|---|---|
CONTAINERS_CONF |
Replaces the normal system/user config search path with a single file |
CONTAINERS_CONF_OVERRIDE |
Loads last as a final override on top of the already-merged configuration |
Setting only CONTAINERS_CONF explains “my /etc/containers edits do nothing.” Setting CONTAINERS_CONF_OVERRIDE explains “one field changed in tests but nothing else did.”
Neither variable replaces storage.conf or registries.conf. Those files follow their own precedence rules and environment variables (CONTAINERS_STORAGE_CONF, registry config paths). Mixing concerns across files is another frequent misconfiguration: a pull mirror belongs in registries.conf, while default capabilities belong in containers.conf.
Podman configuration modules
Modules are reusable configuration profiles stored outside the automatic drop-in merge. Search paths include:
$XDG_CONFIG_HOME/containers/containers.conf.modules/
~/.config/containers/containers.conf.modules/
/etc/containers/containers.conf.modules/
/usr/share/containers/containers.conf.modules/Modules are not loaded automatically. Opt in per command with --module as a global flag before the subcommand:
podman --module dev.conf run --rm IMAGE printenv ENVIRONMENTPlacing --module after run fails:
Error: unknown flag: --moduleCreate a module file:
# /etc/containers/containers.conf.modules/dev.conf
[containers]
env = ["ENVIRONMENT=development"]Load it (rootful sessions resolve module names from /etc/containers/containers.conf.modules/):
podman --module dev.conf run --rm registry.access.redhat.com/ubi9/ubi-minimal:latest printenv ENVIRONMENTSample output:
developmentUse the filename including .conf — podman --module dev without the suffix looks for a directory named dev and fails on this host.
Load multiple modules
Pass --module more than once. Later modules override earlier ones for the same key:
podman --module module1.conf --module module2.conf run --rm registry.access.redhat.com/ubi9/ubi-minimal:latest printenv MODULE_VARWhen module2.conf sets MODULE_VAR=from_module2, sample output:
from_module2Modules load after normal system/user configuration and before CONTAINERS_CONF_OVERRIDE. They are opt-in profiles, not substitutes for drop-ins you want on every command.
Typical uses include a dev.conf module with relaxed labels for local hacking, a ci.conf module checked into a pipeline repository and loaded by absolute path, or a performance.conf module with tuned pids_limit and logging that operators enable only during benchmarks. None of those profiles apply until someone passes --module, which keeps production defaults safe on shared hosts.
Load a module by absolute path
For CI or one-off tests, point at a file directly and run a container that reads a variable from the module:
podman --module /tmp/testing.conf run --rm registry.access.redhat.com/ubi9/ubi-minimal:latest printenv ABS_MODULEWith /tmp/testing.conf containing env = ["ABS_MODULE=absolute_path_works"], sample output:
absolute_path_worksAbsolute paths skip module search directories entirely.
Append to string arrays instead of replacing them
Later TOML normally replaces entire string arrays. Two drop-ins:
# 92-append-base.conf
[containers]
env = ["ONE=1"]# 93-append-second.conf
[containers]
env = ["TWO=2"]Only the last file's values survive:
podman run --rm registry.access.redhat.com/ubi9/ubi-minimal:latest printenv ONE TWOSample output:
2ONE is unset because the second file replaced the whole env array.
Enable append on the later file:
[containers]
env = ["TWO=2", {append=true}]Retry:
podman run --rm registry.access.redhat.com/ubi9/ubi-minimal:latest printenv ONE TWOSample output:
1
2Once {append=true} is set for that array, subsequent loading steps keep appending until a later file sets {append=false} to return to replace behavior. The same pattern applies to other string arrays such as default_capabilities and volumes.
Check effective configuration
Podman does not ship one command that dumps every merged key. Use observable output instead:
podman infoFields such as eventLogger, rootlessNetworkCmd, and networkBackend reflect [engine] and [network] choices. For container defaults, start a disposable container and inspect its environment list:
podman run -d --name conf-check registry.access.redhat.com/ubi9/ubi-minimal:latest sleep 300podman inspect shows the merged env array Podman applied at create time:
podman inspect conf-check --format '{{.Config.Env}}'Sample output includes defaults from active drop-ins (your list will differ):
[PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin APP_ENV=production ...]Match what you see against your drop-ins, modules, and environment variables. When results disagree with expectations, re-check user drop-ins, CONTAINERS_CONF, and module opt-in flags before editing the vendor file.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Edited /etc/containers/containers.conf but rootless behavior unchanged |
User drop-ins or ~/.config/containers/ override system files |
Inspect user paths; test with podman info as that user |
| System drop-ins seem ignored | CONTAINERS_CONF points at an isolated file |
Unset CONTAINERS_CONF or include needed keys in that file |
| Module file exists but nothing changes | Modules are not auto-loaded | Use podman --module name.conf before the subcommand |
--module unknown flag on podman run |
Flag placed after subcommand | Use podman --module … run … |
Vendor env values disappeared after a drop-in |
Array replaced, not appended | Add {append=true} on the later env entry |
References
- containers.conf(5) — format, tables, and merge rules
- Podman containers.conf tutorial — upstream examples
- Red Hat documentation — Building, running, and managing containers — RHEL configuration overview
Summary
containers.conf is how Podman remembers defaults between commands. Vendor files under /usr/share/containers/ start the stack; administrator and user drop-ins override specific keys; command-line flags override defaults for one invocation. podman info is the quickest sanity check when you need to know which layer won.
CONTAINERS_CONF replaces the entire normal search path with one file — powerful for tests, surprising when you forget it is set. CONTAINERS_CONF_OVERRIDE applies one last file on top. Modules sit between those layers: opt-in profiles via podman --module, global flag first, filename or absolute path.
String-array append syntax fixes the other common foot-gun: without {append=true}, a later drop-in silently replaces the whole array. When you add a user drop-in and vendor environment entries vanish, append is usually the fix — not another copy of the vendor file.
Registry routing and image storage live in sibling files — use Configure registries.conf and Podman storage location when the task is pulls or disk layout, not container defaults. For day-to-day operations, keep system policy in numbered drop-ins, personal tweaks under ~/.config/containers/, and reserve modules for profiles you want to opt into explicitly.

