Guide · Container Runtime
MCP Tools for Podman REST API — rootless containers, pods, and quadlets
Podman is the daemonless container engine that ships by default on RHEL, Fedora, and CentOS Stream. Unlike Docker, Podman runs containers without a persistent daemon process — each container runs directly under the user who created it, which enables rootless container execution. For MCP tools, three Podman-specific behaviors require different handling than Docker: socket path varies by mode (rootless containers use a per-user socket at /run/user/$UID/podman/podman.sock, not /var/run/docker.sock), pod lifecycle (Podman's native concept of a pod — a group of containers sharing a network namespace — has its own create/start/stop lifecycle distinct from individual containers), and rootless network stack traps (rootless Podman uses slirp4netns or pasta for network translation, which affects port binding below 1024 and container-to-container DNS). Podman's REST API is Docker-compatible for most container operations but diverges significantly for pods, secrets, and network management.
TL;DR
In rootless mode, connect to /run/user/{uid}/podman/podman.sock — not /var/run/docker.sock. Enable the socket with systemctl --user enable --now podman.socket. For rootful mode, use /run/podman/podman.sock with root access. Podman's REST API at /v5.0.0/libpod/ exposes Podman-specific endpoints (pods, play kube, secrets); Docker-compatible endpoints are at /v1.41/. Use POST /libpod/pods/create → POST /libpod/pods/{name}/start for pod lifecycle. Health probes require checking GET /libpod/containers/{name}/json for .State.Healthcheck.Status — "healthy" only appears if the container spec includes a healthcheck. Register the container's HTTP endpoint with AliveMCP for external monitoring.
Socket paths: rootless vs rootful mode
The most common mistake when porting Docker-targeting MCP tools to Podman is the socket path. Podman has two runtime modes with different socket locations:
Rootless mode (recommended for security): Each user gets their own Podman socket at /run/user/{uid}/podman/podman.sock where {uid} is the user's numeric UID. Containers run as the user, without root privileges. To get the UID: id -u. To enable the socket: systemctl --user enable --now podman.socket. The socket is created on first use and destroyed when the user logs out (unless lingering is enabled: loginctl enable-linger $USER).
Rootful mode: Podman runs as root, socket at /run/podman/podman.sock. Enable with systemctl enable --now podman.socket (system-level, not user-level). Rootful mode is needed for operations that require root: binding ports below 1024 without sysctl changes, managing system-wide networks, using certain OCI runtimes.
import http from 'http';
// Rootless mode socket path (use actual UID)
const uid = process.getuid?.() || 1000;
const ROOTLESS_SOCKET = `/run/user/${uid}/podman/podman.sock`;
const ROOTFUL_SOCKET = '/run/podman/podman.sock';
function podmanRequest(socketPath, method, path, body) {
return new Promise((resolve, reject) => {
const req = http.request({
socketPath,
method,
path: `/v5.0.0/libpod${path}`, // Podman-specific API prefix
headers: body ? { 'Content-Type': 'application/json' } : {}
}, res => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(data || 'null') }));
});
req.on('error', reject);
if (body) req.write(JSON.stringify(body));
req.end();
});
}
// Docker-compatible path prefix for portable container operations
function dockerCompatRequest(socketPath, method, path, body) {
return new Promise((resolve, reject) => {
const req = http.request({
socketPath,
method,
path: `/v1.41${path}`, // Docker-compatible API prefix
headers: body ? { 'Content-Type': 'application/json' } : {}
}, res => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(data || 'null') }));
});
req.on('error', reject);
if (body) req.write(JSON.stringify(body));
req.end();
});
}
MCP tools that work with both Docker and Podman should probe for the socket at startup: try Docker socket first, then rootful Podman socket, then rootless Podman socket. Include the detected engine type in the tool's health output so agents know which runtime they are talking to. Podman returns "PodmanVersion" in GET /libpod/info while Docker returns "ServerVersion" in GET /info.
Pod lifecycle: create, start, stop, and inspect
A Podman pod is a group of containers that share a network namespace (same IP address, same loopback interface) and optionally share IPC and PID namespaces. This is Podman's native implementation of the Kubernetes pod concept. The pod's infra container (also called the "pause container") holds the shared namespaces alive — it runs a minimal process that does nothing but sleep, keeping the namespaces open even when other containers in the pod are restarted.
Pod lifecycle is separate from container lifecycle. A pod must be created before containers are added to it; the pod must be started before containers in it can run; stopping the pod stops all containers in it simultaneously.
// 1. Create a pod (allocates shared network namespace)
const pod = await podmanRequest(ROOTLESS_SOCKET, 'POST', '/pods/create', {
name: 'mcp-workload',
portmappings: [
{ host_port: 8080, container_port: 8080, protocol: 'tcp' }
],
// Port mappings are on the pod, not on individual containers
// All containers in the pod share this IP+port space
});
const podId = pod.body.Id;
// 2. Create containers in the pod
const container = await podmanRequest(ROOTLESS_SOCKET, 'POST', '/containers/create', {
name: 'mcp-app',
image: 'node:22-alpine',
command: ['node', 'server.js'],
pod: 'mcp-workload', // attach to the pod
netns: { nsmode: 'pod', value: podId }, // share pod network
});
// 3. Start the pod (starts infra container and all member containers)
await podmanRequest(ROOTLESS_SOCKET, 'POST', `/pods/${podId}/start`, null);
// 4. Inspect pod status
const inspect = await podmanRequest(ROOTLESS_SOCKET, 'GET', `/pods/${podId}/json`, null);
// inspect.body.State: 'Running', 'Stopped', 'Degraded', 'Paused', 'Error'
// 'Degraded' = pod has some running and some stopped containers
// inspect.body.Containers: array with per-container status
The Degraded pod state is Podman-specific — it means the pod is partially running. This is a critical monitoring signal for MCP tools: a pod in Degraded state has at least one crashed container. Surface the per-container statuses from inspect.body.Containers when the pod state is Degraded to identify which container failed.
Rootless network stack: slirp4netns, pasta, and port binding traps
Rootless Podman cannot configure host network interfaces directly (that requires root). Instead, it uses a userspace network stack — either slirp4netns (older, wider support) or pasta (newer, faster). This has several practical implications for MCP tools:
Port binding below 1024 requires a sysctl change. In rootless mode, binding to port 80 or 443 fails because unprivileged users cannot bind to ports below 1024. Fix with: sysctl net.ipv4.ip_unprivileged_port_start=80 (temporary) or add to /etc/sysctl.d/99-podman.conf (persistent). Alternatively, bind to an unprivileged port (8080) and use a reverse proxy in a rootful container or the host's nginx to forward from 80.
Container-to-container DNS differs from rootful mode. In rootful Podman, containers on a user-defined Podman network can reach each other by container name (similar to Docker). In rootless mode, container-to-container communication depends on the network driver: the default slirp4netns creates a separate network per-container (no direct container-to-container connectivity without a pod); pasta mode (Podman 4.4+) creates a bridged network with container name DNS when using --network pasta:slirp4netns:.... For MCP tools that need multi-container communication in rootless mode, use a Podman pod — all pod containers share the same network namespace and communicate via localhost.
Host.docker.internal equivalent: In rootless mode, the host is not at a fixed IP. To reach the host from inside a container, use host.containers.internal (resolved by Podman's DNS plugin since Podman 4.7) or pass the host IP explicitly as an environment variable. Docker's host.docker.internal does not work in Podman by default (though it can be added as a host alias).
// Detect rootless vs rootful and surface in health output
async function podmanInfo(socketPath) {
const info = await podmanRequest(socketPath, 'GET', '/info', null);
const body = info.body;
return {
rootless: body.Host.Security.rootless,
networkBackend: body.Host.NetworkBackend, // 'netavark' or 'cni'
ociRuntime: body.Host.OCIRuntime.name,
slirpNetns: body.Host.SlirpInfo,
version: body.Version.Version,
};
}
Quadlets: managing Podman containers via systemd
Quadlets are Podman's approach to integrating container management with systemd. A quadlet is a systemd unit file with a .container, .pod, or .network extension that Podman's systemd generator transforms into a full systemd service unit at boot time. Quadlets replace the older podman generate systemd command (deprecated in Podman 4.4).
A minimal quadlet for an MCP server looks like:
# /etc/containers/systemd/mcp-server.container (rootful, system-wide)
# ~/.config/containers/systemd/mcp-server.container (rootless, per-user)
[Unit]
Description=MCP Server container
After=network-online.target
[Container]
Image=docker.io/myorg/mcp-server:latest
PublishPort=8080:8080
Environment=NODE_ENV=production
HealthCmd=curl -sf http://localhost:8080/health
HealthInterval=30s
HealthRetries=3
AutoUpdate=registry # podman auto-update checks registry for new image
[Service]
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target # or default.target for rootless
After placing the quadlet file, reload the systemd daemon and start the service:
# System-wide (rootful)
systemctl daemon-reload
systemctl enable --now mcp-server
# Per-user (rootless)
systemctl --user daemon-reload
systemctl --user enable --now mcp-server
For MCP tools that manage Podman services, checking a container's systemd service status is more informative than checking the container status directly. A container might be in "running" state in Podman's view but be about to be restarted by systemd (the Restart=always directive restarts the container after it exits). Query both: systemctl show mcp-server --property=ActiveState,SubState,RestartCount via exec or via the D-Bus API gives the systemd service health, while the Podman REST API gives the container's current state. Surface both in the MCP tool's health output.
Health probe depth: Podman HEALTHCHECK and external monitoring
Podman supports HEALTHCHECK in two ways: via the HEALTHCHECK directive in the image (same as Docker), or via the HealthCmd directive in a quadlet unit. The health state is exposed in GET /libpod/containers/{name}/json under .State.Healthcheck.Status (values: "healthy", "unhealthy", "starting") and .State.Healthcheck.Log for recent check outputs.
async function podmanContainerHealth(socketPath, containerName) {
const resp = await podmanRequest(socketPath, 'GET', `/containers/${containerName}/json`, null);
const c = resp.body;
const result = {
status: c.State.Status, // running, stopped, paused, ...
exitCode: c.State.ExitCode,
restartCount: c.RestartCount,
startedAt: c.State.StartedAt,
};
if (c.State.Healthcheck) {
result.healthStatus = c.State.Healthcheck.Status;
result.lastHealthCheck = c.State.Healthcheck.Log?.[0];
}
// Pod membership: if in a pod, surface pod state
if (c.Pod) {
const podResp = await podmanRequest(socketPath, 'GET', `/pods/${c.Pod}/json`, null);
result.podState = podResp.body.State; // Running, Degraded, Stopped
result.podName = podResp.body.Name;
}
if (c.State.Status === 'running' && c.State.Healthcheck?.Status === 'unhealthy') {
result.summary = 'process_running_but_unhealthy';
} else if (c.RestartCount > 0 && c.State.Status === 'running') {
result.summary = 'running_after_restarts';
} else {
result.summary = c.State.Status;
}
return result;
}
Rootless Podman containers that publish ports use the slirp4netns or pasta network stack to forward traffic from the host port to the container — this forwarding is an additional hop that adds a small latency overhead and can fail independently of the container. For AliveMCP monitoring: register the host port (the published port), not the container's internal port, since AliveMCP probes from outside the host. The host port is what your users and agents connect to; the container's internal port is only reachable from within the container's network namespace.
Frequently asked questions
Can I use the Docker SDK or dockerode with Podman?
Yes, for most operations. Podman's REST API is Docker-compatible at the /v1.41/ path prefix, which means the dockerode Node.js library works against Podman by pointing it at the Podman socket instead of the Docker socket: new Dockerode({ socketPath: '/run/user/1000/podman/podman.sock' }). Container create, start, stop, exec, logs, and inspect all work via the Docker-compatible endpoint. What does NOT work via Docker-compatible path: pod management (no Docker equivalent), Podman secrets (different API from Docker secrets), Podman-specific network features, quadlet management, and podman play kube (running Kubernetes YAML). For pure container lifecycle operations, the Docker-compatible path is sufficient. For Podman-specific features, use the /v5.0.0/libpod/ path prefix with raw HTTP requests or the Podman client library.
How do I use Podman secrets in MCP tools?
Podman secrets are managed via POST /libpod/secrets/create (create a new secret) and injected into containers via the secrets field in the container create spec. Unlike Docker secrets (which are Swarm-mode only and mount at /run/secrets/), Podman secrets work with regular containers and can be mounted at any path or injected as environment variables. To create a secret: POST /libpod/secrets/create?name=db-password with the secret value as the request body (plain text). To use it in a container: include "secrets": [{ "source": "db-password", "type": "mount", "target": "/run/secrets/db-password" }] in the container create request, or use "type": "env", "target": "DB_PASSWORD" to inject as an environment variable. Podman secrets are stored encrypted in the Podman data store (under ~/.local/share/containers/storage/secrets/ for rootless mode). For MCP tools that manage secrets, always use Podman secrets rather than environment variables for credentials — they don't appear in podman inspect output or in container logs.
What is the difference between Podman rootless and rootful for security?
Rootless Podman runs the container's root user mapped to the invoking user's UID via user namespaces. Inside the container, the process appears to run as root (UID 0), but on the host, it runs as the invoking user's UID. This means: a process that escapes the container namespace cannot gain host root privileges — it can only access files the invoking user owns. Rootful Podman (like Docker daemon by default) runs containers as actual host root, so a container escape grants the attacker root on the host. The security trade-offs: rootless mode cannot bind to ports below 1024 without sysctl changes; some OCI features (device access, certain seccomp overrides) require root; performance overhead is slightly higher due to user namespace mapping. For MCP tools that create container workloads from agent requests (which may be untrusted), always use rootless mode — the blast radius of a container escape is limited to the invoking user's permissions. For infrastructure management tools where the MCP server itself runs as root (appropriate only in controlled, trusted environments), rootful mode has fewer restrictions.
How do I run Kubernetes pod YAML files with Podman?
Podman supports running Kubernetes YAML files directly via POST /libpod/play/kube (API) or podman play kube manifest.yaml (CLI). This translates Kubernetes Pod or Deployment manifests into Podman containers and pods without requiring a Kubernetes cluster. Not all Kubernetes features are supported: supported — Pod, Deployment, ConfigMap, Service (partial), PersistentVolumeClaim (via named volumes); not supported — most cluster-level resources (Namespace, ClusterRole, Ingress), multi-replica Deployments scale to 1, NodeAffinity, and most admission webhooks. For MCP tools that need to run workloads defined in Kubernetes-compatible YAML without a full Kubernetes cluster (development, testing, CI environments), podman play kube is the fastest path. The API endpoint accepts the YAML as the request body with Content-Type: application/yaml. After play kube completes, the created pods are manageable via the standard Podman pod API. Register the pods' published port endpoints with AliveMCP for health monitoring.
How do I set up auto-update for Podman containers managed by an MCP tool?
Podman's auto-update feature checks registries for newer image tags and restarts containers using the new image. For quadlet-managed containers, add AutoUpdate=registry to the [Container] section — this enables auto-update for that container. For containers created via the API, add the label io.containers.autoupdate=registry at creation time. The auto-update check runs when you call podman auto-update (CLI) or trigger it via systemd with podman-auto-update.timer (enable with systemctl --user enable --now podman-auto-update.timer). For MCP tools that need to trigger an auto-update check programmatically, use POST /libpod/auto-update — the response includes which containers were updated and which updates failed. Auto-update only works when the container's io.containers.autoupdate label is set; containers without this label are skipped. After an auto-update, the old container is removed and a new container is created from the updated image — the container ID changes. MCP tools should not cache container IDs across auto-update events; use container names instead.
Further reading
- MCP Tools for Docker Engine API — container lifecycle, exec vs attach, image cache
- MCP Tools for containerd — gRPC API, task lifecycle, snapshot management
- MCP Tools for Amazon ECS — task state machine, service health, and capacity providers
- MCP Tools for Google Cloud Run — revision lifecycle, traffic splitting, and cold start
- MCP Server Docker — Dockerfile, signal handling, and health checks
- MCP Servers on Kubernetes — readiness probes, PDBs, and session affinity
- MCP Server Health Checks — implementing and monitoring protocol probes
- MCP Tools for DevOps — patterns across container runtimes and orchestrators