?

Overview

This guide moves a working go install-based rousseau setup into the reference production posture: a rootless Podman container managed by a systemd Quadlet unit, with a hardened runtime posture (read-only rootfs, dropped capabilities, seccomp, NoNewPrivileges, keep-id user namespace).

What changes

  • Binary invocation moves from rousseau whatsapp on the host to podman run under a Quadlet-managed systemd service.
  • File paths move from ~/.local/share/rousseau/*.db on the host to bind-mounted volumes inside the container.
  • The Claude CLI (~/.claude) is bind-mounted into the container so the claudecli provider keeps its OAuth token.
  • Workspace access is scoped to a single bind mount — the container sees nothing else on the host.

Config diff

The rousseau config.yaml does not change semantically. Only the file's location moves (from ~/.config/rousseau/config.yaml on the host to a bind mount inside the container).

Migration steps

1. Back up host state

tar -czf rousseau-state.tar.gz \
  -C "$HOME/.local/share" rousseau \
  -C "$HOME/.config" rousseau \
  -C "$HOME" .claude

2. Stop the host-side daemon

# If you started it manually, Ctrl+C. If under systemd (user unit):
systemctl --user stop rousseau-agent.service || true

3. Build the image

git clone https://github.com/sebastienrousseau/rousseau-agent
cd rousseau-agent
podman build -t rousseau-agent:local -f docker/Dockerfile .

4. Install the Quadlet unit

mkdir -p ~/.config/containers/systemd
cp docker/rousseau-agent.container ~/.config/containers/systemd/
systemctl --user daemon-reload

The unit ships with:

Setting Value Rationale
Network=pasta Rootless network stack slirp4netns removed from recent Podman
UserNS=keep-id Container UID 1000 → host UID 1000 Bind-mounted files retain host ownership
ReadOnly=true Root filesystem read-only The binary can't mutate the image
Tmpfs=/tmp:rw,size=64m,mode=1777 Writable scratch Anything the daemon writes lives on a bind mount
DropCapability=all + NoNewPrivileges=true Least privilege Outbound sockets need no elevated caps
Volume=%h/.local/share/rousseau:…rw,Z State survives Uses your existing sessions.db
Volume=%h/.claude:…rw,Z Claude CLI auth Reads / refreshes cached OAuth tokens
Volume=%h/team-rousseau-workspace:/workspace:rw,Z Workspace Nothing else on the host is mounted

5. Start the container

systemctl --user start rousseau-agent.service
journalctl --user -u rousseau-agent.service -f

The first launch inside the container will see the existing pairing — no QR should print. If it does, the bind mount is not resolving to the host's whatsapp.db. Verify with podman exec (if privileges allow) or check the Quadlet mount paths.

Downgrade path

Stop the container, restart the host binary:

systemctl --user stop rousseau-agent.service
rousseau whatsapp --allow '447900123456@s.whatsapp.net'

Because the container was reading/writing the same sessions.db and whatsapp.db on the host, the host binary picks up exactly where the container left off. No data migration required.

If you built up state exclusively inside the container:

# Ensure the container is stopped, then let the host binary use the same paths.

Verification checklist

  • systemctl --user status rousseau-agent.service is active (running).
  • journalctl --user -u rousseau-agent.service | grep whatsapp.connected fires within 10 s.
  • Send yourself a WhatsApp message from an allowlisted JID; the reply lands.
  • rousseau session list --limit 5 (run on the host against the same sessions.db) shows the new session.
  • rousseau doctor (host) reports green.

Common failure modes

  • Container starts, exits with permission denied on sessions.db — missing :Z on the volume, or the host directory's owner does not match keep-id. chown -R $(id -u):$(id -g) ~/.local/share/rousseau and reload.
  • QR printed inside the container even though the host had a pairing — the whatsapp.db bind mount is wrong; check the Quadlet unit's Volume= line.
  • claudecli: exec: "claude": executable file not found — the runtime image ships the Node layer with the claude CLI. If a local override omits it, install claude inside the image or switch to provider: anthropic.

Related pages

Type to search 150+ pages. Ranking: BM25 with title/description boost.