Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
7 changes: 6 additions & 1 deletion src/docker-outside-of-docker/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down Expand Up @@ -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",
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 @@ -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
Expand Down Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions test/docker-outside-of-docker/ROOTLESS_DOCKER.md
Original file line number Diff line number Diff line change
@@ -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)
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
4 changes: 2 additions & 2 deletions test/docker-outside-of-docker/docker_dash_compose_v1.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
69 changes: 68 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,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"
}
}
}
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