Run the Apus Gateway in a rootless Podman container, managed by your current non-root user.

The model:

Prerequisites

Quick start

One-time setup

From the repo root, run `./scripts/podman/setup.sh`.

This builds `apus:local` in your rootless Podman store (or pulls `APUS_IMAGE` / `APUS_PODMAN_IMAGE` if set), creates `~/.apus/apus.json` with `gateway.mode: "local"` if missing, and creates `~/.apus/.env` with a generated `APUS_GATEWAY_TOKEN` if missing.

Optional build-time env vars:

| Var | Effect |
| --- | --- |
| `APUS_IMAGE` / `APUS_PODMAN_IMAGE` | Use an existing/pulled image instead of building `apus:local` |
| `APUS_IMAGE_APT_PACKAGES` | Install extra apt packages during image build (also accepts legacy `APUS_DOCKER_APT_PACKAGES`) |
| `APUS_IMAGE_PIP_PACKAGES` | Install extra Python packages during image build; pin versions and use only package indexes you trust |
| `APUS_EXTENSIONS` | Compile/package supported selected plugins and install their runtime dependencies |
| `APUS_INSTALL_BROWSER` | Pre-install Chromium and Xvfb for browser automation (set to `1`) |

For Quadlet-managed setup instead (Linux + systemd user services only):

```bash
./scripts/podman/setup.sh --quadlet
```

Or set `APUS_PODMAN_QUADLET=1`.

Start the Gateway container

```bash
./scripts/run-apus-podman.sh launch
```

Starts the container as your current uid/gid with `--userns=keep-id` and bind-mounts your Apus state into the container.

Run onboarding inside the container

```bash
./scripts/run-apus-podman.sh launch setup
```

Then open `http://127.0.0.1:18789/` and use the token from `~/.apus/.env`.

Model auth: use Apus-managed auth during setup (Anthropic API keys, or OpenAI Codex browser OAuth/device-code auth for Codex-backed OpenAI). The Podman launcher does not mount host CLI credential homes such as `~/.claude` or `~/.codex` into the setup or gateway container. Existing host CLI logins are same-host convenience paths only -- for container installs, keep provider auth in the mounted `~/.apus` state that setup manages.

Manage the running container from the host CLI

```bash
export APUS_CONTAINER=apus
```

Then normal `apus` commands run inside that container automatically:

```bash
apus dashboard --no-open
apus gateway status --deep   # includes extra service scan
apus doctor
apus channels login
```

On macOS, Podman machine may make the browser appear non-local to the gateway. If the Control UI reports device-auth errors after launch, use the Tailscale guidance in [Podman and Tailscale](#podman-and-tailscale).

The manual launcher reads only a small allowlist of Podman-related keys from ~/.apus/.env and passes explicit runtime env vars to the container; it does not hand the full env file to Podman.

Agent sandbox backend

This page covers running the Gateway itself in a Podman container. Agent sandboxing is separate. Set agents.defaults.sandbox.backend: "podman" to select the native Podman CLI directly. The default "docker" backend remains Docker-only.

Podman reuses the same agents.defaults.sandbox.docker.* container settings as Docker but executes them through the native podman CLI. Browser sandboxes remain Docker-only for now.

See Sandboxing for the config example and image-build command.

Podman and Tailscale

For HTTPS or remote browser access, follow the main Tailscale docs.

Podman-specific notes:

See Tailscale and Control UI.

Systemd (Quadlet, optional)

If you ran ./scripts/podman/setup.sh --quadlet, setup installs a Quadlet file at ~/.config/containers/systemd/apus.container.

Action Command
Start systemctl --user start apus.service
Stop systemctl --user stop apus.service
Status systemctl --user status apus.service
Logs journalctl --user -u apus.service -f

After editing the Quadlet file:

systemctl --user daemon-reload
systemctl --user restart apus.service

For boot persistence on SSH/headless hosts, enable lingering for your current user:

sudo loginctl enable-linger "$(whoami)"

The generated Quadlet service keeps a fixed, hardened default shape: 127.0.0.1 published ports (18789 gateway, 18790 bridge), --bind lan inside the container, keep-id user namespace, APUS_NO_RESPAWN=1, Restart=on-failure, and TimeoutStartSec=300. It reads ~/.apus/.env as a runtime EnvironmentFile for values such as APUS_GATEWAY_TOKEN, but does not consume the manual launcher's Podman-specific override allowlist. For custom publish ports, publish host, or other container-run flags, use the manual launcher instead, or edit ~/.config/containers/systemd/apus.container directly and then reload and restart the service.

Config, env, and storage

The launch script and Quadlet bind-mount host state into the container: APUS_CONFIG_DIR -> /home/node/.apus, APUS_WORKSPACE_DIR -> /home/node/.apus/workspace. By default those are host directories, not anonymous container state, so apus.json, shared and per-agent SQLite auth stores, channel/provider state, sessions, and workspace survive container replacement. Setup also seeds gateway.controlUi.allowedOrigins for 127.0.0.1 and localhost on the published gateway port so the local dashboard works with the container's non-loopback bind.

Useful env vars for the manual launcher (persist these in ~/.apus/.env; the launcher reads that file before finalizing container/image defaults):

Var Default Effect
APUS_PODMAN_CONTAINER apus Container name
APUS_PODMAN_IMAGE / APUS_IMAGE apus:local Image to run
APUS_PODMAN_GATEWAY_HOST_PORT 18789 Host port mapped to container 18789
APUS_PODMAN_BRIDGE_HOST_PORT 18790 Host port mapped to container 18790
APUS_PODMAN_PUBLISH_HOST 127.0.0.1 Host interface for published ports
APUS_GATEWAY_BIND lan Gateway bind mode inside the container
APUS_PODMAN_USERNS keep-id keep-id, auto, or host

If you use a non-default APUS_CONFIG_DIR or APUS_WORKSPACE_DIR, set the same variables for both ./scripts/podman/setup.sh and later ./scripts/run-apus-podman.sh launch commands -- the repo-local launcher does not persist custom path overrides across shells.

Upgrading images

After you rebuild or pull a new image, restart the container or Quadlet service. On first startup for a new Apus version, the gateway runs safe state and plugin repairs before reporting ready.

If the gateway exits instead of becoming ready, run the same image once with apus doctor --fix against the same mounted state/config, then restart the gateway normally:

APUS_CONFIG_DIR="${APUS_CONFIG_DIR:-$HOME/.apus}"
APUS_WORKSPACE_DIR="${APUS_WORKSPACE_DIR:-$APUS_CONFIG_DIR/workspace}"
APUS_PODMAN_IMAGE="${APUS_PODMAN_IMAGE:-${APUS_IMAGE:-apus:local}}"

podman run --rm -it \
  --userns=keep-id \
  --user "$(id -u):$(id -g)" \
  -e HOME=/home/node \
  -e NPM_CONFIG_CACHE=/home/node/.apus/.npm \
  -v "$APUS_CONFIG_DIR:/home/node/.apus:rw" \
  -v "$APUS_WORKSPACE_DIR:/home/node/.apus/workspace:rw" \
  "$APUS_PODMAN_IMAGE" \
  apus doctor --fix

On SELinux hosts, add ,Z to both bind mounts if Podman blocks access to the mounted state.

After restarting the Gateway with the updated image, run the read-only deployment preflight through the container-aware host CLI:

export APUS_CONTAINER=apus
apus doctor --json

Useful commands

Troubleshooting

Related