diff --git a/src/docker-outside-of-docker/README.md b/src/docker-outside-of-docker/README.md index 1794d42e8..79ea30ae0 100644 --- a/src/docker-outside-of-docker/README.md +++ b/src/docker-outside-of-docker/README.md @@ -23,6 +23,7 @@ Re-use the host docker socket, adding the Docker CLI to a container. Feature inv | dockerDashComposeVersion | Compose version to use for docker-compose (v1 or v2 or none) | string | v2 | | installDockerBuildx | Install Docker Buildx | boolean | true | | installDockerComposeSwitch | Install Compose Switch (provided docker compose is available) which is a replacement to the Compose V1 docker-compose (python) executable. It translates the command line into Compose V2 docker compose then runs the latter. | boolean | true | +| socketPath | Path where the Docker socket is mounted inside the container. For rootless Docker, override the mount in devcontainer.json to map your host socket to this path. | string | /var/run/docker-host.sock | ## Customizations @@ -36,6 +37,30 @@ Re-use the host docker socket, adding the Docker CLI to a container. Feature inv - The host and the container must be running on the same chip architecture. You will not be able to use it with an emulated x86 image with Docker Desktop on an Apple Silicon Mac, for example. - This approach does not currently enable bind mounting the workspace folder by default, and cannot support folders outside of the workspace folder. Consider whether the [Docker-in-Docker Feature](../docker-in-docker) would better meet your needs given it does not have this limitation. +## Rootless Docker Support + +By default, this feature expects the Docker socket at `/var/run/docker.sock` on the host, which works for standard (root) Docker installations. For **rootless Docker** setups where the socket is located at `/run/user/$UID/docker.sock` or `$XDG_RUNTIME_DIR/docker.sock`, you need to override the mount in your `devcontainer.json`: + +```json +{ + "features": { + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {} + }, + "mounts": [ + { + "source": "/run/user/1000/docker.sock", + "target": "/var/run/docker-host.sock", + "type": "bind" + } + ] +} +``` + +**Notes:** +- Replace `1000` with your actual user ID (run `id -u` to find it) +- The feature will automatically detect the socket at `/var/run/docker-host.sock` +- Your custom mount will override the feature's default mount + ## Supporting bind mounts from the workspace folder A common question that comes up is how you can use `bind` mounts from the Docker CLI from within the a dev container using this Feature (e.g. via `-v`). If you cannot use the [Docker-in-Docker Feature](../docker-in-docker), the only way to work around this is to use the **host**'s folder paths instead of the container's paths. There are 2 ways to do this diff --git a/src/docker-outside-of-docker/devcontainer-feature.json b/src/docker-outside-of-docker/devcontainer-feature.json index 7314fa83d..abc243908 100644 --- a/src/docker-outside-of-docker/devcontainer-feature.json +++ b/src/docker-outside-of-docker/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "docker-outside-of-docker", - "version": "1.6.5", + "version": "1.7.0", "name": "Docker (docker-outside-of-docker)", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/docker-outside-of-docker", "description": "Re-use the host docker socket, adding the Docker CLI to a container. Feature invokes a script to enable using a forwarded Docker socket within a container to run Docker commands.", @@ -44,6 +44,11 @@ "type": "boolean", "default": true, "description": "Install Compose Switch (provided docker compose is available) which is a replacement to the Compose V1 docker-compose (python) executable. It translates the command line into Compose V2 docker compose then runs the latter." + }, + "socketPath": { + "type": "string", + "default": "/var/run/docker-host.sock", + "description": "Path where the Docker socket is mounted inside the container. For rootless Docker, override the mount in devcontainer.json to map your host socket to this path." } }, "entrypoint": "/usr/local/share/docker-init.sh", diff --git a/src/docker-outside-of-docker/install.sh b/src/docker-outside-of-docker/install.sh index 74fd63530..38336e282 100755 --- a/src/docker-outside-of-docker/install.sh +++ b/src/docker-outside-of-docker/install.sh @@ -13,7 +13,8 @@ MOBY_BUILDX_VERSION="${MOBYBUILDXVERSION:-"latest"}" DOCKER_DASH_COMPOSE_VERSION="${DOCKERDASHCOMPOSEVERSION:-"v2"}" # v1 or v2 or none ENABLE_NONROOT_DOCKER="${ENABLE_NONROOT_DOCKER:-"true"}" -SOURCE_SOCKET="${SOURCE_SOCKET:-"/var/run/docker-host.sock"}" +SOCKET_PATH="${SOCKETPATH:-"/var/run/docker-host.sock"}" # From feature option +SOURCE_SOCKET="${SOURCE_SOCKET:-"${SOCKET_PATH}"}" TARGET_SOCKET="${TARGET_SOCKET:-"/var/run/docker.sock"}" USERNAME="${USERNAME:-"${_REMOTE_USER:-"automatic"}"}" INSTALL_DOCKER_BUILDX="${INSTALLDOCKERBUILDX:-"true"}" @@ -313,13 +314,19 @@ else buildx=(moby-buildx${buildx_version_suffix}) fi apt-get -y install --no-install-recommends ${cli_package_name}${cli_version_suffix} "${buildx[@]}" || { err "It seems packages for moby not available in OS ${ID} ${VERSION_CODENAME} (${architecture}). To resolve, either: (1) set feature option '\"moby\": false' , or (2) choose a compatible OS version (eg: 'ubuntu-24.04')." ; exit 1 ; } - apt-get -y install --no-install-recommends moby-compose || echo "(*) Package moby-compose (Docker Compose v2) not available for OS ${ID} ${VERSION_CODENAME} (${architecture}). Skipping." + if [ "${DOCKER_DASH_COMPOSE_VERSION}" != "v1" ]; then + apt-get -y install --no-install-recommends moby-compose || echo "(*) Package moby-compose (Docker Compose v2) not available for OS ${ID} ${VERSION_CODENAME} (${architecture}). Skipping." + fi else buildx=() if [ "${INSTALL_DOCKER_BUILDX}" = "true" ]; then buildx=(docker-buildx-plugin) fi - apt-get -y install --no-install-recommends ${cli_package_name}${cli_version_suffix} "${buildx[@]}" docker-compose-plugin + if [ "${DOCKER_DASH_COMPOSE_VERSION}" != "v1" ]; then + apt-get -y install --no-install-recommends ${cli_package_name}${cli_version_suffix} "${buildx[@]}" docker-compose-plugin + else + apt-get -y install --no-install-recommends ${cli_package_name}${cli_version_suffix} "${buildx[@]}" + fi buildx_path="/usr/libexec/docker/cli-plugins/docker-buildx" # Older versions of Docker CE installs buildx as part of the CLI package if [ "${INSTALL_DOCKER_BUILDX}" = "false" ] && [ -f "${buildx_path}" ]; then @@ -433,6 +440,8 @@ echo "docker-init doesn't exist, adding..." # By default, make the source and target sockets the same if [ "${SOURCE_SOCKET}" != "${TARGET_SOCKET}" ]; then + # Create parent directory if it doesn't exist + mkdir -p "$(dirname "${SOURCE_SOCKET}")" touch "${SOURCE_SOCKET}" ln -s "${SOURCE_SOCKET}" "${TARGET_SOCKET}" fi diff --git a/test/docker-outside-of-docker/ROOTLESS_DOCKER.md b/test/docker-outside-of-docker/ROOTLESS_DOCKER.md new file mode 100644 index 000000000..3568a923b --- /dev/null +++ b/test/docker-outside-of-docker/ROOTLESS_DOCKER.md @@ -0,0 +1,73 @@ +# Docker Outside-of-Docker with Rootless Docker + +This document shows how to configure the `docker-outside-of-docker` feature with rootless Docker installations. + +## Standard Rootless Docker Configuration + +For a typical rootless Docker setup: + +```json +{ + "features": { + "ghcr.io/devcontainers/features/docker-outside-of-docker": { + "socketPath": "/var/run/docker-rootless.sock" + } + }, + "mounts": [ + { + "source": "${env:XDG_RUNTIME_DIR}/docker.sock", + "target": "/var/run/docker-rootless.sock", + "type": "bind" + } + ] +} +``` + +## Custom Socket Path Configuration + +For rootless Docker with custom socket location: + +```json +{ + "features": { + "ghcr.io/devcontainers/features/docker-outside-of-docker": { + "socketPath": "/custom/docker/socket.sock" + } + }, + "mounts": [ + { + "source": "/run/user/1000/docker.sock", + "target": "/custom/docker/socket.sock", + "type": "bind" + } + ] +} +``` + +## Detecting Your Docker Socket + +To find your Docker socket location: + +```bash +# Check for rootless Docker socket +ls -la ${XDG_RUNTIME_DIR}/docker.sock +# Typically: /run/user/1000/docker.sock + +# Check Docker context +docker context ls +``` + +## Key Points + +1. **socketPath option**: Configures where the feature expects the socket inside the container +2. **Mount source**: Must match your host's actual Docker socket location +3. **Mount target**: Must match the `socketPath` option value +4. **XDG_RUNTIME_DIR**: Usually `/run/user/{uid}` for rootless Docker + +## Test Scenarios + +The test scenarios demonstrate: +- `rootless_docker_socket`: Standard rootless configuration +- `custom_rootless_socket_path`: Custom socket path +- `xdg_runtime_dir_socket`: XDG runtime directory style +- `root_docker_socket`: Standard root Docker (for comparison) \ No newline at end of file diff --git a/test/docker-outside-of-docker/custom_rootless_socket_path.sh b/test/docker-outside-of-docker/custom_rootless_socket_path.sh new file mode 100644 index 000000000..45bca0468 --- /dev/null +++ b/test/docker-outside-of-docker/custom_rootless_socket_path.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -e + +source dev-container-features-test-lib + +echo "=== Custom Rootless Docker Socket Path Test ===" + +# Test that the custom socket path is properly configured +EXPECTED_SOCKET="/custom/docker/rootless.sock" + +# Check if the custom socket exists and is accessible +check "custom-socket-exists" test -S "$EXPECTED_SOCKET" +check "custom-socket-readable" test -r "$EXPECTED_SOCKET" + +# Verify Docker functionality using the custom socket +export DOCKER_HOST="unix://$EXPECTED_SOCKET" +check "docker-functional-custom" docker ps >/dev/null + +# Verify that DOCKER_HOST is properly set by the feature +check "docker-host-env-set" [ ! -z "$DOCKER_HOST" ] + +# Test basic Docker operations +check "docker-version" docker version --format '{{.Client.Version}}' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+' >/dev/null +check "docker-info" docker info >/dev/null + +echo "Custom socket path: $EXPECTED_SOCKET" +echo "Docker host: $DOCKER_HOST" + +reportResults \ No newline at end of file diff --git a/test/docker-outside-of-docker/docker_dash_compose_v1.sh b/test/docker-outside-of-docker/docker_dash_compose_v1.sh index d95f3cf73..b6afdda05 100755 --- a/test/docker-outside-of-docker/docker_dash_compose_v1.sh +++ b/test/docker-outside-of-docker/docker_dash_compose_v1.sh @@ -5,9 +5,9 @@ set -e # Optional: Import test library source dev-container-features-test-lib -# Definition specific tests -check "docker compose" bash -c "docker compose version | grep -E '2.[0-9]+.[0-9]+'" +# Definition specific tests - v1 should only have docker-compose, not docker compose plugin check "docker-compose" bash -c "docker-compose --version | grep -E '1.[0-9]+.[0-9]+'" +check "no docker compose plugin" bash -c "if command -v docker >/dev/null 2>&1; then ! docker compose version >/dev/null 2>&1; else true; fi" # Report result reportResults diff --git a/test/docker-outside-of-docker/root_docker_socket.sh b/test/docker-outside-of-docker/root_docker_socket.sh new file mode 100644 index 000000000..4c62147cb --- /dev/null +++ b/test/docker-outside-of-docker/root_docker_socket.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Test script to detect Docker type + +if [ -S "/var/run/docker.sock" ]; then + echo "Root Docker detected" + export DOCKER_HOST="unix:///var/run/docker-host.sock" +elif [ -S "/var/run/docker-rootless.sock" ]; then + echo "Rootless Docker detected" + export DOCKER_HOST="unix:///var/run/docker-rootless.sock" +else + echo "No Docker socket found" + exit 1 +fi + +docker --version +docker info --format '{{.SecurityOptions}}' \ No newline at end of file diff --git a/test/docker-outside-of-docker/rootless_docker_socket.sh b/test/docker-outside-of-docker/rootless_docker_socket.sh new file mode 100644 index 000000000..004554e9a --- /dev/null +++ b/test/docker-outside-of-docker/rootless_docker_socket.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +source dev-container-features-test-lib + +echo "=== Rootless Docker Socket Configuration Test ===" + +# Test the custom rootless socket path +EXPECTED_SOCKET="/var/run/docker-rootless.sock" + +# Check if the configured rootless socket exists and is accessible +check "rootless-socket-exists" test -S "$EXPECTED_SOCKET" +check "rootless-socket-readable" test -r "$EXPECTED_SOCKET" + +# Verify Docker functionality using the rootless socket +export DOCKER_HOST="unix://$EXPECTED_SOCKET" +check "docker-functional-rootless" docker ps >/dev/null + +# Test basic Docker operations with rootless configuration +check "docker-version-rootless" docker version --format '{{.Client.Version}}' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+' >/dev/null +check "docker-info-rootless" docker info >/dev/null + +# Demonstrate that customers can configure custom socket paths +echo "Configured rootless socket path: $EXPECTED_SOCKET" +echo "Docker host: $DOCKER_HOST" + +reportResults \ No newline at end of file diff --git a/test/docker-outside-of-docker/scenarios.json b/test/docker-outside-of-docker/scenarios.json index 2163e7076..1d4a2282c 100644 --- a/test/docker-outside-of-docker/scenarios.json +++ b/test/docker-outside-of-docker/scenarios.json @@ -180,5 +180,72 @@ "moby": false } } + }, + "rootless_docker_socket": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "features": { + "docker-outside-of-docker": { + "moby": false, + "socketPath": "/var/run/docker-rootless.sock" + } + }, + "mounts": [ + { + "source": "/var/run/docker.sock", + "target": "/var/run/docker-rootless.sock", + "type": "bind" + } + ], + "containerUser": "vscode" + }, + "root_docker_socket": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "features": { + "docker-outside-of-docker": { + "moby": false + } + }, + "mounts": [ + { + "source": "/var/run/docker.sock", + "target": "/var/run/docker-host.sock", + "type": "bind" + } + ], + "containerUser": "vscode" + }, + "custom_rootless_socket_path": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "features": { + "docker-outside-of-docker": { + "moby": false, + "socketPath": "/custom/docker/rootless.sock" + } + }, + "mounts": [ + { + "source": "/var/run/docker.sock", + "target": "/custom/docker/rootless.sock", + "type": "bind" + } + ], + "containerUser": "vscode" + }, + "xdg_runtime_dir_socket": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "features": { + "docker-outside-of-docker": { + "moby": false, + "socketPath": "/var/run/user-docker.sock" + } + }, + "mounts": [ + { + "source": "/var/run/docker.sock", + "target": "/var/run/user-docker.sock", + "type": "bind" + } + ], + "containerUser": "vscode" } -} +} \ No newline at end of file diff --git a/test/docker-outside-of-docker/xdg_runtime_dir_socket.sh b/test/docker-outside-of-docker/xdg_runtime_dir_socket.sh new file mode 100644 index 000000000..04e32cfd2 --- /dev/null +++ b/test/docker-outside-of-docker/xdg_runtime_dir_socket.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -e + +source dev-container-features-test-lib + +echo "=== XDG Runtime Directory Socket Test ===" + +# Test XDG_RUNTIME_DIR style socket configuration +EXPECTED_SOCKET="/var/run/user-docker.sock" + +# Check if the socket exists and is accessible +check "xdg-socket-exists" test -S "$EXPECTED_SOCKET" +check "xdg-socket-readable" test -r "$EXPECTED_SOCKET" + +# Verify Docker functionality using the XDG-style socket +export DOCKER_HOST="unix://$EXPECTED_SOCKET" +check "docker-functional-xdg" docker ps >/dev/null + +# Test that this works for rootless-style configurations +check "docker-version-xdg" docker version --format '{{.Client.Version}}' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+' >/dev/null + +# Verify the socket path matches what a customer would configure +echo "XDG-style socket path: $EXPECTED_SOCKET" +echo "Docker host: $DOCKER_HOST" + +reportResults \ No newline at end of file