Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion architecture/supported-subset.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,15 @@ flags. compose2pod hoists them onto `podman pod create` instead
`--no-hosts` stops Podman from managing `/etc/hosts` at all, the pre-6.0.0
bug where a container stopping inside a pod wiped the shared `/etc/hosts`
for every other container cannot fire — compose2pod works identically on
every Podman version, with no version floor. `host.containers.internal` /
every Podman version, with no version floor. `--no-hosts` also stops Podman
from adding each container's own hostname line, so `podman pod create` gets
`--uts private --hostname <pod-name>` and the pod name is added to the hosts
file (`127.0.0.1 <pod-name>`): a container resolving its own hostname
(`hostname -f`) — which Podman handled before `--no-hosts` and which images
with a self-referential startup do — resolves again. A pod shares one UTS
namespace, so this hostname is pod-wide; per-service `hostname:` values stay
resolvable by peers via the hosts file but do not set a container's own name.
`host.containers.internal` /
`host.docker.internal` are not provided: `--no-hosts` means Podman's
computed gateway IP is unavailable, so a service needing it must add an
explicit `extra_hosts` entry (see `README.md`'s Requirements section).
Expand Down
14 changes: 12 additions & 2 deletions compose2pod/emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,13 @@ def _plan(compose: dict[str, Any], options: EmitOptions) -> PlannedScript:
# Only the closure joins the pod, so only the closure is resolvable. A name
# pointing at 127.0.0.1 for a service that never runs would turn an honest
# resolution failure into a connection-refused.
hosts = hostnames({name: services[name] for name in order})
# The pod name is added as a resolvable name so every container's own
# hostname (set to the pod name below) resolves. A pod shares one UTS
# namespace, so without this the hostname is the random container ID,
# absent from the owned /etc/hosts -- `hostname -f` then fails and images
# that resolve their own hostname at startup crash. Podman used to add this
# line itself; `--no-hosts` stops it, so the script owns it too.
hosts = [*hostnames({name: services[name] for name in order}), options.pod]
host_tokens = hosts_file_tokens(services, order, hosts)
completion_gated = {
dep
Expand All @@ -355,7 +361,11 @@ def _plan(compose: dict[str, Any], options: EmitOptions) -> PlannedScript:
lines.append(f"trap '{teardown}' EXIT")
pod_flags = pod_create_flags(services, order)
_collect_vars(pod_flags, names)
pod_create = f"podman pod create --name {shlex.quote(options.pod)} --no-hosts"
# `--uts private` gives the pod its own UTS namespace so `--hostname` is
# settable (in the host UTS namespace Podman refuses it); the pod name is
# the shared hostname, resolvable via the line added to the hosts file above.
pod_name = shlex.quote(options.pod)
pod_create = f"podman pod create --name {pod_name} --no-hosts --uts private --hostname {pod_name}"
if pod_flags:
pod_create += " " + _render(pod_flags)
lines.append(pod_create)
Expand Down
55 changes: 55 additions & 0 deletions planning/changes/2026-07-22.02-pod-hostname-own-resolution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
summary: The pod gets --uts private --hostname <pod-name> and that name is added to the owned /etc/hosts, so a container resolving its own hostname works again (regressed by 0.4.0's --no-hosts).
---

# Change: give the pod a resolvable hostname

**Lane:** lightweight — one code file (`emit.py`), one straightforward test,
no new file, no public-API change.

## Goal

Restore own-hostname resolution inside the pod. 0.4.0 switched to a
script-owned `/etc/hosts` under `--no-hosts` to kill the pre-6.0.0 stop-wipe
bug — but `--no-hosts` also stopped Podman from adding each container's own
hostname line. A container's hostname is then the random container ID, absent
from the owned file, so `hostname -f` fails with `Temporary failure in name
resolution`. Images with a self-referential startup (e.g. a KeyDB-sentinel
image whose supervisor resolves its own hostname) crash-loop and never go
healthy — a regression from 0.1.8, which used no `--no-hosts` and let Podman
add the line.

## Approach

- `podman pod create` gains `--uts private --hostname <pod-name>`. `--uts
private` is required: in the host UTS namespace Podman refuses `--hostname`
(`cannot set hostname ... host UTS namespace`). A pod shares one UTS
namespace, so per-container hostnames are impossible on 5.x/6.x (verified);
the pod name is the single shared hostname.
- Add the pod name to the owned `/etc/hosts` (`127.0.0.1 <pod-name>`) so that
hostname resolves. Reuses `hosts_file_tokens`' existing merge/conflict path.

Per-service `hostname:` keys are unchanged: still resolvable by peers via the
hosts file, still not the container's own name (0.1.8 didn't honor them as
real hostnames either, and the value is irrelevant to resolution). Truth home:
`architecture/supported-subset.md` (owned-`/etc/hosts` bullet).

Verified end-to-end on real Podman **5.8.1** (`quay.io/podman/stable:v5.8.1`)
and local **6.0.1**: a service whose healthcheck is `hostname -f` goes healthy,
and a peer resolves its own hostname (`OWN-HOSTNAME-OK`).

## Files

- `compose2pod/emit.py` — add the pod name to the hosts list; emit `--uts
private --hostname <pod>` on pod create.
- `tests/test_emit.py` — new `test_pod_gets_private_uts_and_hostname`; update
the two existing pod-create string assertions.

## Verification

- [x] Failing test first — `pytest -k test_pod_gets_private_uts_and_hostname`.
- [x] Apply the change.
- [x] Test passes.
- [x] `just test-ci` — 1403 passed, 100% coverage.
- [x] `just lint-ci` — clean.
- [x] End-to-end on Podman 5.8.1 and 6.0.1 — own hostname resolves.
22 changes: 19 additions & 3 deletions tests/test_emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,20 @@ def test_hosts_file_made_world_readable(self, chats_compose: dict) -> None:
first_run_index = next(i for i, line in enumerate(lines) if line.startswith("podman run"))
assert write_index < chmod_index < first_run_index

def test_pod_gets_private_uts_and_hostname(self, chats_compose: dict) -> None:
# A pod shares one UTS namespace; with no pod hostname, every container's
# own hostname is the random container ID -- absent from the owned
# /etc/hosts -- so `hostname -f` fails ("Temporary failure in name
# resolution") and an image that resolves its own hostname at startup
# crashes. Give the pod a private UTS namespace and the pod name as its
# hostname, and put that name in the hosts file so it resolves.
script = self.make_script(chats_compose)
pod_create = next(line for line in script.splitlines() if line.startswith("podman pod create"))
assert "--uts private" in pod_create
assert "--hostname test-pod" in pod_create
printf_line = next(line for line in script.splitlines() if line.startswith("printf '%s\\n'"))
assert "127.0.0.1 test-pod" in printf_line

def test_hostsfile_removed_on_teardown(self, chats_compose: dict) -> None:
script = self.make_script(chats_compose)
trap = next(line for line in script.splitlines() if line.startswith("trap "))
Expand Down Expand Up @@ -846,16 +860,18 @@ def test_ulimits_compose_through_emit_script(self) -> None:
def test_pod_create_carries_dns_and_sysctl_flags(self) -> None:
svc = {"image": "x", "dns": ["1.1.1.1", "8.8.8.8"], "sysctls": {"net.core.somaxconn": 1024}}
script = self._single(svc)
# `--no-hosts` always precedes dns/sysctls now that /etc/hosts is script-owned.
assert 'podman pod create --name p --no-hosts --dns "1.1.1.1" --dns "8.8.8.8"' in script
# `--no-hosts` and the pod hostname always precede dns/sysctls now that /etc/hosts is script-owned.
assert (
'podman pod create --name p --no-hosts --uts private --hostname p --dns "1.1.1.1" --dns "8.8.8.8"' in script
)
assert '--sysctl "net.core.somaxconn=1024"' in script

def test_pod_create_carries_only_self_alias_without_pod_options(self) -> None:
# A service's own name is always a resolvable alias (`graph.hostnames`), so even
# with no dns/sysctls/extra_hosts declared, the alias still lands -- now in the
# hosts file rather than on pod create.
script = self._single({"image": "x"})
assert "podman pod create --name p --no-hosts\n" in script
assert "podman pod create --name p --no-hosts --uts private --hostname p\n" in script
assert "127.0.0.1 app" in script

def test_env_file_required_false_is_guarded(self) -> None:
Expand Down