podman-runner · Simple, safe Podman containers for Python tests and scripts
podman-runner brings the convenience of testcontainers to the rootless, daemonless world of Podman.
Start containers, wait for them to be ready, run commands, mount init scripts, map ports — all with a clean context-manager API and automatic cleanup.
Perfect for integration tests, local development scripts, or anything that needs a real database/service without Docker.
from podman_runner import Container, ContainerConfig
with Container(
ContainerConfig(
name="my-postgres",
image="docker.io/library/postgres:16-alpine",
env={"POSTGRES_PASSWORD": "secret"},
health_cmd=["pg_isready", "-U", "postgres"],
)
) as pg:
pg.exec(["psql", "-U", "postgres", "-c", "CREATE TABLE test(id serial);"])
# → container stops and is removed automatically
Features
- Context manager → guaranteed cleanup (even on exceptions or Ctrl-C)
- Automatic health-check polling (
health_cmd) - Smart port mapping (fixed or dynamic)
- Auto-mount init scripts into
/docker-entrypoint-initdb.d-style directories - Full volume and environment support
- Detailed pre-flight checks that catch the most common Podman pitfalls (WSL shm, Docker conflict, socket not running, Snap sandbox, etc.)
- 100% type-annotated, fully tested, zero external runtime dependencies
Installation
Requires Podman ≥ 4.0 and Python ≥ 3.11.
pip install podman-runner
Or with the fast uv tool (recommended):
uv pip install podman-runner
Install Podman
# Ubuntu/Debian
sudo apt install podman
# Fedora
sudo dnf install podman
# macOS (via Homebrew)
brew install podman
podman machine init
podman machine start
# Windows (WSL2 recommended)
# See https://podman.io/getting-started/install.html
Quick Examples
Basic container
from podman_runner import Container, ContainerConfig
with Container(ContainerConfig(
name="demo",
image="docker.io/library/alpine:latest",
command=["sleep", "infinity"],
)) as c:
print(c.exec(["echo", "Hello Podman!"]).stdout)
PostgreSQL with init scripts
from pathlib import Path
from podman_runner import Container, ContainerConfig
script = Path("01-setup.sql")
script.write_text("CREATE TABLE users(id serial PRIMARY KEY);\n")
with Container(ContainerConfig(
name="pg-init",
image="docker.io/library/postgres:16-alpine",
env={"POSTGRES_PASSWORD": "secret"},
init_dir="/docker-entrypoint-initdb.d",
init_scripts=[script],
health_cmd=["pg_isready", "-U", "postgres"],
)) as pg:
result = pg.exec(["psql", "-U", "postgres", "-c", "\\dt"])
print(result.stdout)
Redis with health check
with Container(ContainerConfig(
name="redis",
image="docker.io/library/redis:alpine",
health_cmd=["redis-cli", "ping"],
)) as r:
assert r.exec(["redis-cli", "PING"]).stdout.strip() == "PONG"
Nginx with port mapping
import requests
with Container(ContainerConfig(
name="web",
image="docker.io/library/nginx:alpine",
ports={80: 8080}, # fixed host port
# ports={443: None}, # auto-assign free host port
)) as web:
host_port = web.get_port(80)
resp = requests.get(f"http://localhost:{host_port}")
assert resp.status_code == 200
See the examples/ directory for more.
API Reference
ContainerConfig
| Parameter | Type | Description |
|---|---|---|
name |
str |
Unique container name (required) |
image |
str |
Image to run (required) |
command |
list[str] | None |
Override container command |
env |
dict[str, str] | None |
Environment variables |
ports |
dict[int, int | None] | None |
{container_port: host_port_or_None} — None = auto-assign |
volumes |
dict[Path, str] | None |
Host path → container path (:ro suffix for read-only) |
init_dir |
str | None |
Target init directory (e.g. "/docker-entrypoint-initdb.d") |
init_scripts |
list[Path] | None |
Scripts auto-mounted with 00-, 01- prefix and :ro |
health_cmd |
list[str] | None |
Command that exits 0 when service is ready |
health_timeout |
int |
Seconds to wait for health check (default: 30) |
health_interval |
float |
Seconds between health checks (default: 1.0) |
Container
| Method | Description |
|---|---|
container_id: str | None |
Podman container ID after start |
start() |
Start container (called automatically in context manager) |
stop() |
Stop and remove container |
exec(cmd: list[str]) |
Run command inside container → CompletedProcess |
logs(tail: int | None = None, follow=False) |
Get container logs |
get_port(internal: int) |
Return host port mapped to container port (or None) |
check_status() |
Return current status string ("running", "exited", etc.) |
Development
git clone https://github.com/Francois-NT/podman-runner.git
cd podman-runner
# Install with dev dependencies
uv sync --all-extras
# Install pre-commit hooks
uv run pre-commit install
# Run tests
uv run task unittest # fast unit tests
uv run task integration # requires running Podman socket
Why Podman instead of Docker?
- No daemon → works great in CI and restricted environments
- Rootless by default
- Full Docker CLI compatibility
- Native on Linux, excellent macOS/Windows support via
podman machine
Similar Projects
| Project | Docker | Podman | Context Manager | Init Script Support | Health Polling |
|---|---|---|---|---|---|
| testcontainers-python | Yes | No | Yes | No | Yes |
| podman-compose | No | Yes | No | No | No |
| podman-py | No | Yes | No | No | No |
| podman-runner | No | Yes | Yes | Yes | Yes |
License
MIT © François Naggar-Tremblay
Happy testing with real, rootless containers!
Metadata
Release files for podman-runner 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| podman_runner-0.1.0.tar.gz | 8.3 kB | Without provenance | Details for podman_runner-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| podman_runner-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for podman_runner-0.1.0-py3-none-any.whl |
Total release size: 17.9 kB
Release files / podman_runner-0.1.0.tar.gz
| Download URL | podman_runner-0.1.0.tar.gz |
|---|---|
| Size | 8.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
87cbc1c51c87095d253fe562847c35efbfcfa71a08416ea32e41ea1d6e822259
|
|
BLAKE2b-256 checksum How to use checksums |
110dfb4b2db8828682602ebe61a55a798c016d1ab95c6259a79072e6bb6e4c27
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.13
|
Release files / podman_runner-0.1.0-py3-none-any.whl
| Download URL | podman_runner-0.1.0-py3-none-any.whl |
|---|---|
| Size | 9.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ac12e9e9600067bf1de8b99d4c5e932e6b384826477228e5a9c725615af0a792
|
|
BLAKE2b-256 checksum How to use checksums |
5b62b8c9b69a6f68d68c38792606517f67fa248d189018026fd01df8e0db9bc1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.13
|
Security for podman-runner 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.