Skip to content
Draft
25 changes: 25 additions & 0 deletions src/docker-outside-of-docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
97 changes: 51 additions & 46 deletions src/docker-outside-of-docker/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,51 +1,56 @@
{
"id": "docker-outside-of-docker",
"version": "1.8.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.",
"options": {
"version": {
"type": "string",
"proposals": [
"latest",
"none",
"20.10"
],
"default": "latest",
"description": "Select or enter a Docker/Moby CLI version. (Availability can vary by OS version.)"
},
"moby": {
"type": "boolean",
"default": true,
"description": "Install OSS Moby build instead of Docker CE"
},
"mobyBuildxVersion": {
"type": "string",
"default": "latest",
"description": "Install a specific version of moby-buildx when using Moby"
},
"dockerDashComposeVersion": {
"type": "string",
"enum": [
"none",
"v1",
"v2"
],
"default": "v2",
"description": "Compose version to use for docker-compose (v1 or v2 or none)"
},
"installDockerBuildx": {
"type": "boolean",
"default": true,
"description": "Install Docker Buildx"
"id": "docker-outside-of-docker",
"version": "1.9.1",
"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.",
"options": {
"version": {
"type": "string",
"proposals": [
"latest",
"none",
"20.10"
],
"default": "latest",
"description": "Select or enter a Docker/Moby CLI version. (Availability can vary by OS version.)"
},
"moby": {
"type": "boolean",
"default": true,
"description": "Install OSS Moby build instead of Docker CE"
},
"mobyBuildxVersion": {
"type": "string",
"default": "latest",
"description": "Install a specific version of moby-buildx when using Moby"
},
"dockerDashComposeVersion": {
"type": "string",
"enum": [
"none",
"v1",
"v2"
],
"default": "v2",
"description": "Compose version to use for docker-compose (v1 or v2 or none)"
},
"installDockerBuildx": {
"type": "boolean",
"default": true,
"description": "Install Docker Buildx"
},
"installDockerComposeSwitch": {
"type": "boolean",
"default": false,
"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."
}
},
"installDockerComposeSwitch": {
"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."
}
},
"entrypoint": "/usr/local/share/docker-init.sh",
"customizations": {
"vscode": {
Expand Down
15 changes: 12 additions & 3 deletions src/docker-outside-of-docker/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"}"
Expand Down Expand Up @@ -318,13 +319,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
Expand Down Expand Up @@ -438,6 +445,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
Expand Down
29 changes: 29 additions & 0 deletions test/docker-outside-of-docker/custom_rootless_socket_path.sh
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions test/docker-outside-of-docker/docker_dash_compose_v1.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ source dev-container-features-test-lib

# Definition specific tests
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
16 changes: 16 additions & 0 deletions test/docker-outside-of-docker/root_docker_socket.sh
Original file line number Diff line number Diff line change
@@ -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}}'
27 changes: 27 additions & 0 deletions test/docker-outside-of-docker/rootless_docker_socket.sh
Original file line number Diff line number Diff line change
@@ -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
62 changes: 61 additions & 1 deletion test/docker-outside-of-docker/scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,5 +180,65 @@
"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
}
},
"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": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we really need this test when it appears to be quite similar to rootless_docker_socket scenario?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just another variant of using custom socket path. If it appears redundant I will remove this.

"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"
}
}
}
26 changes: 26 additions & 0 deletions test/docker-outside-of-docker/xdg_runtime_dir_socket.sh
Original file line number Diff line number Diff line change
@@ -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
Loading