Skip to content

Run the container as root so a bind-mounted /out works (issue #7)#53

Merged
tamnd merged 1 commit into
mainfrom
fix/issue-7-container-crashpad
Jun 23, 2026
Merged

Run the container as root so a bind-mounted /out works (issue #7)#53
tamnd merged 1 commit into
mainfrom
fix/issue-7-container-crashpad

Conversation

@tamnd

@tamnd tamnd commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Problem

The Docker image still failed on Linux with the command from the README:

docker run -v "$PWD/out:/out" ghcr.io/tamnd/kage clone example.com

It printed two errors (issue #7):

page error: render: launch Chrome: [launcher] Failed to get the debug url: chrome_crashpad_handler: --database is required
could not save resume state: mkdir /out/data: permission denied

Root cause

Both errors come from the same place.
The image ran as a fixed non-root user (uid 10001) with HOME set to /out.
When you bind-mount a host directory onto /out, that directory is owned by whoever created it on the host, so uid 10001 cannot write into it.
That breaks two things at once.

kage writes its output and resume state under $HOME/data/kage, so the write fails with mkdir /out: permission denied.
Chrome derives its crash-database path from $HOME, and with $HOME unwritable that path comes out empty, so it launches chrome_crashpad_handler without --database, the handler aborts, and that brings the whole browser down on every render.

The earlier attempt set HOME to /out, but that only helps when /out is writable, which it is not for a non-root uid against a host-owned mount.
The --disable-crash-reporter and --disable-breakpad flags did not help either, because they do not stop Chrome from spawning the handler in the first place.

Fix

Run the container as root.
Container root can write a host-owned bind mount whatever it is owned by, so /out and HOME both stay writable and the README command works.
This does not weaken anything: Chrome's sandbox is already off inside a container (kage drops it on container detection), so running as root there changes nothing that was still holding.

I also fixed the stale comment and dropped the two ineffective flags in browser/pool.go.

go build ./..., go vet ./browser/, and go test ./browser/ pass.
The end-to-end testing on real Docker hosts is in the comments below.

The image dropped to a fixed non-root user (uid 10001) and pointed HOME at
/out. On native Linux Docker a bind-mounted /out is owned by whoever created
it on the host, so uid 10001 cannot write into it. Two things then failed:
kage's output and resume state under $HOME/data/kage hit "mkdir /out:
permission denied", and Chrome launched chrome_crashpad_handler with an empty
crash database path, which aborts the whole browser with
"chrome_crashpad_handler: --database is required" and fails every render.

The earlier attempt set HOME=/out, but that only helps when /out is writable,
which it is not for a non-root uid against a host-owned mount. The crash-reporter
flags in the launcher did not help either: they do not stop Chrome from spawning
the handler, so the abort stayed.

Run as root instead. Container root writes a host-owned bind mount whatever its
ownership, so both /out and HOME stay writable and the documented one-liner just
works. This does not loosen the sandbox: Chrome's sandbox is already off inside
any container (kage drops it on container detection), so root here changes
nothing that was holding.

Verified end to end in an Alpine + chromium container: the non-root image
reproduces both the crashpad abort and the permission-denied exactly as
reported, and the root image clones example.com cleanly, writing index.html and
resume state into a host-owned mounted volume.
@tamnd

tamnd commented Jun 20, 2026

Copy link
Copy Markdown
Owner Author

Tested on a real-root Linux VM

The earlier note pointed out that rootless podman cannot reproduce native bind-mount permissions, since it remaps uids.
So I reran this on a rootful VM where container root is real root (uid_map: 0 0 4294967295), with the output directory owned by a non-root uid.
That matches the Ubuntu reporter's host.

The mount source was owned by uid 1000, mode 755, like a normal $PWD/out.
Under plain DAC:

  • container uid 10001 running touch /out/p is denied, which is the bug condition.
  • container root running mkdir /out/data succeeds, through CAP_DAC_OVERRIDE.

Running clone example.com against that same uid-1000 mount:

Current image (uid 10001) reproduces the bug:

page error: render: launch Chrome: [launcher] Failed to get the debug url: chrome_crashpad_handler: --database is required
could not save resume state: mkdir /out/data: permission denied
  errors 1

Nothing is written to the mount.

Fixed image (root) works:

kage: running as root, Chrome sandbox disabled
done /out/data/kage/example.com
  pages 1   assets 0

index.html (the real Example Domain page) and _kage/state.json land in the host-owned mounted volume.

SELinux note

The VM was Fedora CoreOS with SELinux enforcing.
With an unlabeled mount, container root is blocked by SELinux on its own even with CAP_DAC_OVERRIDE.
That is the usual "relabel your volume" case, not anything specific to this change.
On those hosts you add :Z:

docker run -v "$PWD/out:/out:Z" ghcr.io/tamnd/kage clone example.com

The fixed image clones cleanly that way too.
On Ubuntu, Debian and standard Docker the plain -v "$PWD/out:/out" works as is.

@tamnd

tamnd commented Jun 20, 2026

Copy link
Copy Markdown
Owner Author

Tested on a real Ubuntu Docker host with the released image

Ran this on a bare-metal Ubuntu box: kernel 6.8.0-107-generic, x86_64, Docker 29.2.1, AppArmor and no SELinux.
That is the same kind of host as the Ubuntu 24.04 report.
The output directory was owned by a non-root uid (uid 1000, mode 755), a normal user's $PWD/out.

Before, with the released image (ghcr.io/tamnd/kage:latest, which runs as kage):

$ docker run --rm -v "$PWD/out:/out" ghcr.io/tamnd/kage clone example.com
page error: render: launch Chrome: [launcher] Failed to get the debug url: chrome_crashpad_handler: --database is required
could not save resume state: mkdir /out/data: permission denied
  errors 1

Nothing written to the mount, the same as the report.

After, with an image built from this PR's Dockerfile and the same command and mount:

$ docker run --rm -v "$PWD/out:/out" kage-fixed clone example.com
kage: container detected, Chrome sandbox disabled
done /out/data/kage/example.com
  pages 1   assets 0
  open kage serve /out/data/kage/example.com

data/kage/example.com/index.html (the real Example Domain page) and _kage/state.json land in the host-owned volume.
Ubuntu needs no :Z, the documented one-liner works as is.

One thing to be aware of: because the container runs as root, the files in the mount come out owned by root on the host, and Chrome leaves a couple of dotdirs (.config, .pki) under /out.
Not harmful, and the mirror is produced.

@tamnd tamnd merged commit 2dabb93 into main Jun 23, 2026
9 checks passed
@tamnd tamnd deleted the fix/issue-7-container-crashpad branch June 23, 2026 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant