Skip to content

feat: run the dev stack under rootless Docker - #7309

Merged
gustavosbarreto merged 1 commit into
masterfrom
feat/dev-rootless-docker
Oct 2, 2026
Merged

gustavosbarreto merged 1 commit into
masterfrom
feat/dev-rootless-docker

Conversation

@gustavosbarreto

Copy link
Copy Markdown
Member

Summary

The dev stack runs under a rootless Docker daemon. The wrapper finds the daemon's socket and mounts it into the server service, which testcontainers uses to start the Postgres and Redis containers the suites need, and refuses to start when rootless cannot publish the configured ports.

 bin/docker-compose
+  socket: DOCKER_HOST, else the active context's endpoint
+  a unix:// endpoint is exported as SHELLHUB_DEV_DOCKER_SOCKET
+  in development, when `docker info` reports rootless:
+    the lower of SHELLHUB_HTTP_PORT and SHELLHUB_SSH_PORT is below
+    ip_unprivileged_port_start and rootlesskit lacks cap_net_bind_service
+    then abort and print the setcap command

 docker-compose.dev.yml, server
-  /var/run/docker.sock:/var/run/docker.sock
+  ${SHELLHUB_DEV_DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
+  TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=${SHELLHUB_DEV_DOCKER_SOCKET:-/var/run/docker.sock}

The container side stays /var/run/docker.sock, so the tests do not change. The override is the host path testcontainers gives the daemon when it bind-mounts the socket into Ryuk.

On the rootful daemon both values resolve to /var/run/docker.sock, as before.

Evidence

  • Before: bats tests/compose/wrapper.bats, 15 ok. With the new scenarios added and no wrapper change, 5 fail: the three socket resolutions, the privileged-port abort, and the ssh-port abort.
    After: bats tests/compose/, 39 ok.
  • ./bin/docker-compose config on the rootful daemon renders the server mount from /var/run/docker.sock and the override with the same path.
  • go test ./pkg/cache/ -run Redis inside the server container, recreated with the new environment, passes, so Ryuk still reaches the daemon through the override on rootful.
  • A real rootless daemon was not exercised here. The socket path and the name=rootless security option come from the testcontainers-go v0.44.0 source and the Docker API changelog, not from a run.

Merge Danger

Door: two-way

Blast Radius: dev-only

docker-compose.dev.yml and the wrapper's development branch are the only changes. The production compose files are untouched, and the wrapper now runs docker context inspect on every call and docker info on every development call, one daemon round trip more.

@gustavosbarreto
gustavosbarreto requested a review from a team as a code owner October 2, 2026 19:54
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Claude finished @gustavosbarreto's task in 7m 5s —— View job


Code Review Complete

Reviewed 4 files and posted 2 inline issues. Standards: 0 violations, 0 judgement calls. Spec: did not run. Correctness: 2.

Standards

No violations. The diff adds no comments, the reasoning behind the workaround is in the commit message, and the new bats tests name the behaviour they cover.

Correctness

The worst one is in bin/docker-compose:74, and also affects getcap at line 63. On Debian, sysctl and getcap are in /usr/sbin, which isn't on a regular user's PATH. The guard then falls back to its strictest assumption and keeps aborting even after the user applies the setcap fix it prints.

Spec

The Spec axis did not run because the PR references no issue to review against.

Additional notes

  • One correctness concern was dropped because validation came back at 75% confidence, under the 80% bar. Lines 52–55 export SHELLHUB_DEV_DOCKER_SOCKET for any unix:// context endpoint, not only when the daemon is rootless. Some examples:

    • Docker Desktop on macOS: ~/.docker/run/docker.sock
    • Docker Desktop on Linux: ~/.docker/desktop/docker.sock
    • Colima: ~/.colima/default/docker.sock

    Each of these is a path on the client host, but the daemon resolves bind-mount sources inside its VM. Before this change, /var/run/docker.sock worked on those setups. A real run could not confirm this, so it is not posted inline. It is worth checking on a Mac before merging. Exporting the variable only when daemon_is_rootless is true would rule the problem out either way.

  • I dropped four low-value judgement calls under the review bar:

    • helpers.bash doesn't unset SHELLHUB_DEV_DOCKER_SOCKET.
    • The pass-path tests assert *"COMPOSE_FILE="* instead of status -eq 0.
    • The port 22 substring match is loose.
    • The socket fallback is duplicated in docker-compose.dev.yml.

To request another review round, comment /review.

Comment thread bin/docker-compose Outdated
Comment thread bin/docker-compose
@gustavosbarreto

Copy link
Copy Markdown
Member Author

/review

@gustavosbarreto
gustavosbarreto force-pushed the feat/dev-rootless-docker branch from d64ad85 to e66e34d Compare October 2, 2026 20:05
@gustavosbarreto

Copy link
Copy Markdown
Member Author

/review

The server mounts the Docker socket for testcontainers. Under a rootless
daemon that socket lives at $XDG_RUNTIME_DIR/docker.sock, so in
development, when `docker info` reports rootless, the wrapper asks the
CLI where the daemon is (DOCKER_HOST, else the active context) and hands
the path to the dev overlay. Only then: Docker Desktop and Colima also
report a unix:// endpoint under the user's home, but the daemon resolves
bind-mount sources inside its VM, where only /var/run/docker.sock exists.

The container side stays /var/run/docker.sock so nothing in the tests
changes. TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE carries the host path
because testcontainers passes it to the daemon when it bind-mounts the
socket into Ryuk; the default there is /var/run/docker.sock again, which
under rootless is the wrong daemon or nothing.

Rootless cannot publish ports below net.ipv4.ip_unprivileged_port_start
unless rootlesskit carries cap_net_bind_service. The dev defaults are 80
and 22, so in development the wrapper checks both and aborts with the
setcap command instead of leaving Compose to fail on the bind. The 1024
fallback is the kernel default for that sysctl. sysctl and getcap sit in
/usr/sbin on Debian, outside a user's PATH, so the wrapper adds the sbin
directories when calling them; a getcap that is still missing (exit 127)
turns the abort into a warning, since the wrapper then cannot tell.
SHELLHUB_SSH_PORT may carry a bind address, 127.0.0.1:22, so the
comparison strips everything up to the last colon.
@gustavosbarreto

Copy link
Copy Markdown
Member Author

/review

@gustavosbarreto
gustavosbarreto force-pushed the feat/dev-rootless-docker branch from e66e34d to 3f90d0d Compare October 2, 2026 21:12
@gustavosbarreto
gustavosbarreto enabled auto-merge (rebase) October 2, 2026 21:16
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Code Review Complete

The automated review ran but did not post an updated summary — this usually means no new issues were found since the previous review. If you've pushed changes and want a fresh pass, comment /review.

View job

@gustavosbarreto
gustavosbarreto merged commit 9345949 into master Oct 2, 2026
45 checks passed
@gustavosbarreto
gustavosbarreto deleted the feat/dev-rootless-docker branch October 2, 2026 21:23
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