diff --git a/Dockerfile b/Dockerfile index 996e06faa..bed5e2002 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ gosu \ && rm -rf /var/lib/apt/lists/* +# Docker CLI (client only — daemon stays on the host via the +# /var/run/docker.sock mount). The Debian `docker.io` package ships +# dockerd but not the client binary on slim, so grab the static client +# tarball from download.docker.com instead. +ARG DOCKER_CLI_VERSION=27.5.1 +RUN ARCH="$(dpkg --print-architecture)" \ + && case "$ARCH" in \ + amd64) DARCH=x86_64 ;; \ + arm64) DARCH=aarch64 ;; \ + *) echo "unsupported arch $ARCH"; exit 1 ;; \ + esac \ + && curl -fsSL "https://download.docker.com/linux/static/stable/${DARCH}/docker-${DOCKER_CLI_VERSION}.tgz" \ + -o /tmp/docker.tgz \ + && tar -xzf /tmp/docker.tgz -C /tmp \ + && install -m 0755 /tmp/docker/docker /usr/local/bin/docker \ + && rm -rf /tmp/docker /tmp/docker.tgz + WORKDIR /app # Install Python deps first (layer cache). Optional extras (PyMuPDF AGPL, etc.) diff --git a/docker-compose.yml b/docker-compose.yml index 0b350c2e1..dd708303f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,16 @@ services: # land under /app/.local for the odysseus user. Persist them so a # container recreate does not silently remove installed serve engines. - ${APP_DATA_DIR:-./data}/local:/app/.local:z + # Docker socket — lets Cookbook launch commands like + # `docker exec ollama-rocm ollama show ` reach the host's + # Docker daemon (and sibling containers like ollama-rocm / + # ollama-test). The in-container user needs to be in the + # socket's owning group — see `group_add` below; the GID + # there must match the host's `docker` group (defaults to 963 + # on Debian, 999 on Ubuntu — override via env if yours differs). + - /var/run/docker.sock:/var/run/docker.sock + group_add: + - "${DOCKER_GID:-963}" extra_hosts: # Lets the container reach local services on the Docker host, including # Ollama at http://host.docker.internal:11434. diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 668018ac1..7d796d9ff 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -24,6 +24,31 @@ if ! getent passwd "$PUID" >/dev/null 2>&1; then useradd -u "$PUID" -g "$PGID" -M -s /bin/sh -d /app odysseus fi +# Docker-socket group plumbing. When /var/run/docker.sock is bind- +# mounted (cookbook uses `docker exec` to reach sibling containers +# like ollama-rocm), the socket is owned by root:docker on the host. +# We need the in-container odysseus user to be in the matching group +# so `gosu PUID:PGID` doesn't strip it. compose's `group_add` only +# applies to the initial root process — gosu drop resets supplementary +# groups — so detect the socket's GID here and add the user via the +# username form `gosu odysseus` below. +DOCKER_SOCK="${DOCKER_SOCK:-/var/run/docker.sock}" +if [ -S "$DOCKER_SOCK" ]; then + SOCK_GID="$(stat -c '%g' "$DOCKER_SOCK" 2>/dev/null || echo '')" + if [ -n "$SOCK_GID" ] && [ "$SOCK_GID" != "0" ]; then + # Create the group locally if missing, then add odysseus to it. + if ! getent group "$SOCK_GID" >/dev/null 2>&1; then + groupadd -g "$SOCK_GID" docker_host || true + fi + SOCK_GROUP="$(getent group "$SOCK_GID" | cut -d: -f1)" + if [ -n "$SOCK_GROUP" ]; then + ODY_USER="$(getent passwd "$PUID" | cut -d: -f1)" + [ -z "$ODY_USER" ] && ODY_USER=odysseus + usermod -aG "$SOCK_GROUP" "$ODY_USER" 2>/dev/null || true + fi + fi +fi + # Repair ownership on every writable path the app touches at runtime. # # Bind-mounted dirs (/app/data, /app/logs) are the obvious ones, but @@ -83,9 +108,13 @@ export PATH="/app/.local/bin:$PATH" # Run first-time setup as the app user so data/ files get the right ownership. # setup.py is idempotent — skips auth.json / .env if they already exist. # || true so a setup failure never prevents the container from starting. -gosu "$PUID:$PGID" python /app/setup.py || true +# Use the username form (no :GID) so supplementary groups from /etc/group +# (including the docker-socket group set above) flow through to the child. +ODY_USER="$(getent passwd "$PUID" | cut -d: -f1)" +[ -z "$ODY_USER" ] && ODY_USER="$PUID:$PGID" +gosu "$ODY_USER" python /app/setup.py || true # Drop root and run the actual app. `gosu` is preferred over `su` / # `sudo` because it cleans up the process tree (no extra shell layer) # so signals (SIGTERM from `docker stop`) reach uvicorn directly. -exec gosu "$PUID:$PGID" "$@" +exec gosu "$ODY_USER" "$@"