From 099d770eebdf861962f180a5b3c02e46301b1b76 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Sat, 20 Jun 2026 21:58:53 +0200 Subject: [PATCH] feat(docker): add host command bridge --- .changeset/docker-host-command-bridge.md | 5 +++ docker/Dockerfile | 6 +++ docker/bin/hostexec | 48 ++++++++++++++++++++++++ docker/compose.yml | 5 +++ 4 files changed, 64 insertions(+) create mode 100644 .changeset/docker-host-command-bridge.md create mode 100755 docker/bin/hostexec diff --git a/.changeset/docker-host-command-bridge.md b/.changeset/docker-host-command-bridge.md new file mode 100644 index 0000000..b740e7d --- /dev/null +++ b/.changeset/docker-host-command-bridge.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Add a Docker runtime host command bridge for explicitly running host administration commands from containerized PI WEB sessions. diff --git a/docker/Dockerfile b/docker/Dockerfile index bc68941..787aad0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,9 @@ # syntax=docker/dockerfile:1.7 ARG NODE_VERSION=22-bookworm-slim +ARG DOCKER_CLI_VERSION=29-cli + +FROM docker:${DOCKER_CLI_VERSION} AS docker-cli FROM node:${NODE_VERSION} AS package @@ -32,6 +35,7 @@ ENV NODE_ENV=production \ PI_WEB_DATA_DIR=/data/pi-web \ PI_WEB_SESSIOND_SOCKET=/data/pi-web/sessiond.sock \ PI_CODING_AGENT_DIR=/data/pi-agent \ + HOSTEXEC_IMAGE=alpine:3.22 \ SHELL=/bin/bash \ TERM=xterm-256color @@ -50,6 +54,8 @@ RUN apt-get update \ COPY --from=package /usr/local/lib/node_modules /usr/local/lib/node_modules COPY --from=package /usr/local/bin /usr/local/bin +COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker +COPY --chmod=0755 bin/hostexec /usr/local/bin/hostexec WORKDIR /workspace USER node diff --git a/docker/bin/hostexec b/docker/bin/hostexec new file mode 100755 index 0000000..f70cc61 --- /dev/null +++ b/docker/bin/hostexec @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat >&2 <<'EOF' +Usage: hostexec + +Run a command on the Docker host by starting a temporary privileged helper +container through the mounted Docker socket and entering the host namespaces. +EOF +} + +if [ "$#" -eq 0 ]; then + usage + exit 64 +fi + +if ! command -v docker >/dev/null 2>&1; then + echo "hostexec: docker CLI not found in this container" >&2 + exit 127 +fi + +docker_host="${DOCKER_HOST:-unix:///var/run/docker.sock}" +if [[ "$docker_host" == unix://* ]]; then + socket_path="${docker_host#unix://}" + if [ ! -S "$socket_path" ]; then + echo "hostexec: Docker socket is not accessible as a Unix socket at $socket_path" >&2 + exit 69 + fi +fi + +helper_image="${HOSTEXEC_IMAGE:-alpine:3.22}" +tty_args=(--interactive) +if [ -t 0 ] && [ -t 1 ]; then + tty_args+=(--tty) +fi + +exec docker run \ + --rm \ + "${tty_args[@]}" \ + --pull=missing \ + --privileged \ + --security-opt label=disable \ + --pid=host \ + --network=host \ + --volume /:/host:rw \ + "$helper_image" \ + nsenter -t 1 -m -u -i -n -p -- "$@" diff --git a/docker/compose.yml b/docker/compose.yml index 7fd5975..b98ac1c 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -14,6 +14,7 @@ x-pi-web-environment: &pi-web-environment PI_WEB_DATA_DIR: /data/pi-web PI_WEB_SESSIOND_SOCKET: /data/pi-web/sessiond.sock PI_CODING_AGENT_DIR: /data/pi-agent + HOSTEXEC_IMAGE: ${HOSTEXEC_IMAGE:-alpine:3.22} PI_WEB_MAX_UPLOAD_BYTES: ${PI_WEB_MAX_UPLOAD_BYTES:-67108864} x-pi-web-volumes: &pi-web-volumes @@ -46,6 +47,8 @@ services: user: "${PI_WEB_UID:-1000}:${PI_WEB_GID:-1000}" group_add: - "${DOCKER_GID:-0}" + security_opt: + - label=disable environment: *pi-web-environment volumes: *pi-web-volumes healthcheck: @@ -66,6 +69,8 @@ services: user: "${PI_WEB_UID:-1000}:${PI_WEB_GID:-1000}" group_add: - "${DOCKER_GID:-0}" + security_opt: + - label=disable environment: <<: *pi-web-environment PI_WEB_HOST: 0.0.0.0