Skip to content

[Feature]: Podman support for the container terminal backend #4084

@petterssonjonas

Description

@petterssonjonas

Problem or Use Case

Summary

Hermes currently hardcodes docker as the only OCI container runtime for sandboxed command execution. Podman — a daemonless, rootless-by-default, drop-in replacement for Docker — is the default container runtime on Fedora, RHEL, and CentOS, and is widely used on Debian, Ubuntu, and Arch as well. There is currently zero mention of podman or rootless anywhere in the codebase.

Supporting Podman would bring genuine security improvements (rootless user-namespace isolation out of the box, no privileged daemon), align with Hermes's own zero-telemetry security posture, and unblock a meaningful segment of Linux self-hosters who run Podman exclusively — including those deploying Hermes on low-power hardware like a Raspberry Pi 5 with rootless Podman quadlets.

The scope is moderate. Hermes already centralizes all container CLI calls through self._docker_exe (resolved once via find_docker()), so the primary work is making that resolution runtime-aware, handling a few Podman-specific flags, and updating the setup wizard and doctor.

Proposed Solution

Motivation

Security. Hermes markets itself around security: zero telemetry, Tirith command scanning, Docker sandboxing with --cap-drop ALL and --security-opt no-new-privileges. Rootless Podman goes further. There is no privileged daemon — Podman runs as a regular user process. Containers use kernel user namespaces so that even a container escape only yields access as an unprivileged host user, not root. This is a meaningful improvement over Docker's model where the daemon runs as root and group membership effectively grants root-equivalent access. Supporting Podman would make Hermes's security story stronger and more consistent.

User base. Podman is the default on Fedora (the OS behind RHEL/CentOS) and ships out of the box on current Ubuntu and Debian. Users who run Podman often do not have Docker installed at all — there is no fallback. hermes setup terminal → Docker → "Docker not found in PATH" is where it currently ends for them.

Self-hosting on constrained hardware. Hermes positions itself as something you can run on a $5 VPS or a Raspberry Pi. Podman's daemonless architecture is ideal for this — no background daemon eating RAM. Rootless Podman quadlets (systemd .container unit files) are the modern way to manage containers as user services on Linux, and they're a natural fit for running the Hermes gateway as a persistent user service.

Not a Red Hat-only thing. Podman is packaged and well-supported on Fedora, RHEL, CentOS Stream, Debian, Ubuntu, Arch, openSUSE, Alpine, and Gentoo. It's an OCI-compliant tool, not a vendor lock-in play.


What would need to change

I cloned the repo and audited every file that touches the Docker backend. Here's the concrete breakdown.

1. Runtime discovery: tools/environments/docker.pyfind_docker()

Current behavior: find_docker() calls shutil.which("docker"), then probes macOS Docker Desktop paths (/usr/local/bin/docker, /opt/homebrew/bin/docker, /Applications/Docker.app/...). Returns the path or None. Result is cached in a module-level global _docker_executable.

What needs to change: Rename or extend to find_container_runtime(). Check for a user-configured preference first (see config below), then auto-detect: shutil.which("podman") and shutil.which("docker"). The macOS fallback paths in _DOCKER_SEARCH_PATHS are Docker Desktop-specific and can be skipped for Podman (Podman on macOS is typically installed via Homebrew and lands in PATH). The cached global should store both the path and which runtime was detected, since downstream code needs to know (for Podman-specific flags).

2. Config option: hermes_cli/config.py

Current state: Config keys are terminal.backend: "docker", terminal.docker_image, terminal.docker_forward_env, terminal.docker_volumes, terminal.docker_mount_cwd_to_workspace.

Proposed addition: Add terminal.container_runtime: "auto" (or "docker" / "podman") to let users explicitly choose. "auto" would auto-detect (prefer podman if found, fall back to docker, or vice versa — open to discussion on default order). The existing docker_* config keys should continue to work for both runtimes since the CLI flags are compatible. No breaking rename needed.

3. Security flags: _SECURITY_ARGS in docker.py

Current flags:

_SECURITY_ARGS = [
    "--cap-drop", "ALL",
    "--cap-add", "DAC_OVERRIDE",
    "--cap-add", "CHOWN",
    "--cap-add", "FOWNER",
    "--security-opt", "no-new-privileges",
    "--pids-limit", "256",
    "--tmpfs", "/tmp:rw,nosuid,size=512m",
    "--tmpfs", "/var/tmp:rw,noexec,nosuid,size=256m",
    "--tmpfs", "/run:rw,noexec,nosuid,size=64m",
]

Podman compatibility: Most of these flags work identically with Podman. The important addition for rootless Podman is --userns=keep-id, which maps the host user's UID/GID to the same UID/GID inside the container. Without this, bind-mounted files (workspace, credentials, skills) get remapped through user namespaces and appear owned by a different UID on the host, breaking file access. With --userns=keep-id, the container runs as the host user, which is actually more secure than Docker's model of running as container-root mapped to host-root.

The --cap-add DAC_OVERRIDE, CHOWN, FOWNER capabilities are restricted in rootless Podman (rootless containers can't grant capabilities the host user doesn't have). In practice, with --userns=keep-id, these capabilities are unnecessary because the container user already owns the bind-mounted files. The fix is to conditionally skip these --cap-add flags when running under rootless Podman, or accept the harmless warning Podman emits.

4. Storage opt check: _storage_opt_supported() in docker.py

Current behavior: Probes whether Docker's overlay2 driver supports --storage-opt size= (requires XFS with pquota). This is entirely Docker-specific.

Podman behavior: Podman does not support --storage-opt size= in rootless mode (the overlay driver in rootless mode uses fuse-overlayfs or native overlay without pquota). The check should be skipped entirely when the runtime is Podman.

5. Availability check: _ensure_docker_available() in docker.py

Current behavior: Runs docker version and checks the exit code.

Podman compatibility: podman version works identically. Just needs to use the detected runtime binary instead of hardcoded docker. The error messages should be updated to say "container runtime" rather than "Docker" when Podman is in use.

6. Container lifecycle commands

Current behavior: The DockerEnvironment class uses docker run -d, docker exec, docker stop, docker rm via subprocess calls with self._docker_exe.

Podman compatibility: All of these commands are CLI-compatible with Podman. podman run -d, podman exec, podman stop, podman rm work the same way. Since Hermes already centralizes the binary path in self._docker_exe, this is essentially free once the discovery is updated.

7. Setup wizard: hermes_cli/setup.py lines 2051–2069

Current behavior: When the user selects "Docker" as the terminal backend, the wizard checks shutil.which("docker") and prints Docker-specific install instructions.

Proposed change: Rename the menu entry to "Container (Docker/Podman)" or add Podman as a separate entry that maps to the same backend. The availability check should probe for both runtimes and report what's found. Install guidance should mention both Docker and Podman.

8. Doctor: hermes_cli/doctor.py lines 406–425

Current behavior: When terminal_env == "docker", checks shutil.which("docker") and runs docker info.

Proposed change: Check for the configured runtime (or auto-detect). podman info works the same way as docker info and would pass the same health check.

9. Terminal tool factory: tools/terminal_tool.py

Current behavior: _create_environment() dispatches on env_type == "docker" to create _DockerEnvironment.

Proposed change: Either accept "podman" as an alias for "docker" (since it uses the same DockerEnvironment class), or let the DockerEnvironment class itself handle which binary to use based on the container_runtime config. The second approach is cleaner — the user sets terminal.backend: docker and terminal.container_runtime: podman, and the Docker environment class uses Podman internally.

10. Tests: tests/tools/test_docker_find.py, test_docker_environment.py

Current state: Tests mock shutil.which("docker") and verify find_docker() behavior.

What's needed: Extend tests to cover Podman discovery (mock shutil.which("podman")), verify --userns=keep-id is added when Podman is detected, and verify _storage_opt_supported() is skipped for Podman.


Suggested implementation approach

The lowest-friction path that avoids breaking changes:

  1. Add terminal.container_runtime config key ("auto", "docker", "podman"). Default "auto".
  2. Extend find_docker() to become runtime-aware: check config preference, then auto-detect. Return a (path, runtime_type) tuple or store both in the cached global.
  3. In DockerEnvironment.__init__(), conditionally add --userns=keep-id and skip _storage_opt_supported() when runtime is Podman.
  4. Update _ensure_docker_available(), setup.py, and doctor.py to use the detected runtime.
  5. Add/extend tests for Podman code paths.

This is a targeted change — roughly 100-200 lines across 5-6 files — not a rewrite.

Alternatives Considered

I want to use Hermes. I'm not swapping my whole home lab over to Docker, where i run rootless podman quadlets - integrating with systemd. It is imo a simpler and more secure system that fits the Hermes architecture and use case better.

If you are up for a purely vibe-coded PR id be up for providing that.
I would defer to you, the maintainers, for implementation specifics tough.

As for the obvious AI generated issue, i know some communities find them offensive, if so I'm sorry.

Feature Type

Other

Scope

Medium (few files, < 300 lines)

Contribution

  • I'd like to implement this myself and submit a PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions