Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/docker-in-docker-stress-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ jobs:
- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli

- name: "Generating tests for 'docker-in-docker' which validates if docker daemon is running"
run: devcontainer features test --skip-scenarios -f docker-in-docker -i mcr.microsoft.com/devcontainers/base:noble .
- name: "Generating tests for 'docker-in-docker' which validates if docker daemon is running (with iptablesSwitchAtRuntime=true)"
run: devcontainer features test -f docker-in-docker --skip-autogenerated --filter "docker_stress_iptables_runtime" .

test-onCreate:
strategy:
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/test-pr-arm64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,14 @@ jobs:
- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli

- name: "Exclude iptables-isolation scenarios from docker-in-docker"
if: matrix.features == 'docker-in-docker'
run: |
sudo apt-get update && sudo apt-get install -y jq
sed 's://.*$::' test/docker-in-docker/scenarios.json \
| jq 'del(.docker_without_iptables, .docker_without_iptables_ubuntu)' \
> test/docker-in-docker/scenarios.json.tmp
mv test/docker-in-docker/scenarios.json.tmp test/docker-in-docker/scenarios.json

- name: "Testing '${{ matrix.features }}' scenarios"
run: devcontainer features test -f ${{ matrix.features }} --skip-autogenerated .
37 changes: 37 additions & 0 deletions .github/workflows/test-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,42 @@ jobs:
- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli

- name: "Exclude iptables-isolation scenarios from docker-in-docker"
if: matrix.features == 'docker-in-docker'
run: |
sudo apt-get update && sudo apt-get install -y jq
sed 's://.*$::' test/docker-in-docker/scenarios.json \
| jq 'del(.docker_without_iptables, .docker_without_iptables_ubuntu)' \
> test/docker-in-docker/scenarios.json.tmp
mv test/docker-in-docker/scenarios.json.tmp test/docker-in-docker/scenarios.json

- name: "Testing '${{ matrix.features }}' scenarios"
run: devcontainer features test -f ${{ matrix.features }} --skip-autogenerated .

iptables-isolation:
needs: [detect-changes]
if: contains(fromJSON(needs.detect-changes.outputs.features), 'docker-in-docker')
runs-on: ubuntu-latest
continue-on-error: true
strategy:
fail-fast: false
matrix:
scenario:
- docker_without_iptables
- docker_without_iptables_ubuntu
steps:
- uses: actions/checkout@v6

- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli

- name: "Isolate scenario '${{ matrix.scenario }}'"
run: |
sudo apt-get update && sudo apt-get install -y jq
sed 's://.*$::' test/docker-in-docker/scenarios.json \
| jq '{ "${{ matrix.scenario }}": .["${{ matrix.scenario }}"] }' \
> test/docker-in-docker/scenarios.json.tmp
mv test/docker-in-docker/scenarios.json.tmp test/docker-in-docker/scenarios.json

- name: "Testing docker-in-docker scenario '${{ matrix.scenario }}'"
run: devcontainer features test --features docker-in-docker --filter ${{ matrix.scenario }} --skip-autogenerated .
3 changes: 2 additions & 1 deletion src/docker-in-docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ Create child containers *inside* a container, independent from the host's docker
| version | Select or enter a Docker/Moby Engine version. (Availability can vary by OS version.) | string | latest |
| moby | Install OSS Moby build instead of Docker CE | boolean | true |
| mobyBuildxVersion | Install a specific version of moby-buildx when using Moby | string | latest |
| dockerDashComposeVersion | Default version of Docker Compose (v1, v2 or none) | string | v2 |
| dockerDashComposeVersion | Default version of Docker Compose (v1, v2, latest or none) | string | latest |
| azureDnsAutoDetection | Allow automatically setting the dockerd DNS server when the installation script detects it is running in Azure | boolean | true |
| dockerDefaultAddressPool | Define default address pools for Docker networks. e.g. base=192.168.0.0/16,size=24 | string | - |
| 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 | false |
| disableIp6tables | Disable ip6tables (this option is only applicable for Docker versions 27 and greater) | boolean | false |
| iptablesSwitchAtRuntime | If true, the iptables alternative is selected at container start (inside docker-init.sh) instead of at image build time. Useful when the desired iptables backend depends on the host kernel at runtime rather than at build time. | boolean | false |

## Customizations

Expand Down
12 changes: 9 additions & 3 deletions src/docker-in-docker/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "docker-in-docker",
"version": "3.0.1",
"version": "3.1.0",
"name": "Docker (Docker-in-Docker)",
"documentationURL": "https://github.com/devcontainers/features/tree/main/src/docker-in-docker",
"description": "Create child containers *inside* a container, independent from the host's docker instance. Installs Docker extension in the container along with needed CLIs.",
Expand Down Expand Up @@ -29,11 +29,12 @@
"type": "string",
"enum": [
"none",
"latest",
"v1",
"v2"
],
"default": "v2",
"description": "Default version of Docker Compose (v1, v2 or none)"
"default": "latest",
"description": "Default version of Docker Compose (v1, v2, latest or none)"
},
"azureDnsAutoDetection": {
"type": "boolean",
Expand All @@ -60,6 +61,11 @@
"type": "boolean",
"default": false,
"description": "Disable ip6tables (this option is only applicable for Docker versions 27 and greater)"
},
"iptablesSwitchAtRuntime": {

@SrzStephen SrzStephen Jun 8, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Probably the only thing I'd try to make a case for, but I'm not overly familiar with this repos general policy here so if it tends to more conservative then that's ok:

If you're moving to a v4 release it might be worth making this a default: true since you're already cutting a major rev.

Reason: The only time it matters what iptables is present is at runtime so it's probably the correct approach, someone can always opt into the old behaviour if they wish (by setting it to false) but you'll probably see some "why doesn't docker run" issues similar to #1659.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. I would much rather put it back to version 3 as the flag could help doing this seamlessly.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'm wondering since 3.0.0 and 3.0.1 break on Ubuntu 24.04 devcontainer base images on a nUbuntu 26.04 host anyway and my testing indicates that the 3.1.0/4.0.0 feature works also on an Ubuntu 22.04 why not considering this a fix with a new compatible feature with the default of iptablesSwitchAtRuntime:true? This makes the 3.x feature working; are there any side effects of the iptablesSwitchAtRuntime?

"type": "boolean",
"default": false,
"description": "If true, the iptables alternative is selected at container start (inside docker-init.sh) instead of at image build time. Useful when the desired iptables backend depends on the host kernel at runtime rather than at build time."
}
},
"entrypoint": "/usr/local/share/docker-init.sh",
Expand Down
37 changes: 33 additions & 4 deletions src/docker-in-docker/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
DOCKER_VERSION="${VERSION:-"latest"}" # The Docker/Moby Engine + CLI should match in version
USE_MOBY="${MOBY:-"true"}"
MOBY_BUILDX_VERSION="${MOBYBUILDXVERSION:-"latest"}"
DOCKER_DASH_COMPOSE_VERSION="${DOCKERDASHCOMPOSEVERSION:-"v2"}" #v1, v2 or none
DOCKER_DASH_COMPOSE_VERSION="${DOCKERDASHCOMPOSEVERSION:-"latest"}" #v1, v2, latest or none
AZURE_DNS_AUTO_DETECTION="${AZUREDNSAUTODETECTION:-"true"}"
DOCKER_DEFAULT_ADDRESS_POOL="${DOCKERDEFAULTADDRESSPOOL:-""}"
USERNAME="${USERNAME:-"${_REMOTE_USER:-"automatic"}"}"
Expand All @@ -22,6 +22,7 @@ MICROSOFT_GPG_KEYS_ROLLING_URI="https://packages.microsoft.com/keys/microsoft-ro
DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES="trixie bookworm buster bullseye bionic focal jammy noble"
DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES="trixie bookworm buster bullseye bionic focal hirsute impish jammy noble resolute"
DISABLE_IP6_TABLES="${DISABLEIP6TABLES:-false}"
IPTABLES_SWITCH_AT_RUNTIME="${IPTABLESSWITCHATRUNTIME:-false}"

# Default: Exit on any failure.
set -e
Expand Down Expand Up @@ -313,8 +314,10 @@ if [ "${ADJUSTED_ID}" = "debian" ] && command -v update-ca-certificates > /dev/n
update-ca-certificates
fi

# Swap to legacy iptables for compatibility (Debian only)
if [ "${ADJUSTED_ID}" = "debian" ]; then
# Swap to legacy iptables for compatibility (Debian only) - install-time path.
# When IPTABLES_SWITCH_AT_RUNTIME=true the same logic is emitted into
# docker-init.sh and runs at container start instead.
if [ "${IPTABLES_SWITCH_AT_RUNTIME}" != "true" ] && [ "${ADJUSTED_ID}" = "debian" ]; then
# On distros where legacy iptables is no longer kernel-supported (e.g. Ubuntu 26.04 / resolute),
# prefer iptables-nft. Otherwise prefer legacy for backward compatibility.
use_nft=false
Expand All @@ -323,12 +326,15 @@ if [ "${ADJUSTED_ID}" = "debian" ]; then
esac

if [ "${use_nft}" = "true" ] && type iptables-nft > /dev/null 2>&1; then
echo "(*) Setting iptables alternatives to nft for better compatibility with newer kernels"
update-alternatives --set iptables /usr/sbin/iptables-nft || true
update-alternatives --set ip6tables /usr/sbin/ip6tables-nft || true
elif type iptables-legacy > /dev/null 2>&1; then
elif type iptables-legacy > /dev/null 2>&1 && iptables-legacy -L > /dev/null 2>&1; then
echo "(*) Setting iptables alternatives to legacy for better compatibility with Docker and older kernels"
update-alternatives --set iptables /usr/sbin/iptables-legacy || true
update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy || true
elif type iptables-nft > /dev/null 2>&1; then
echo "(*) Setting iptables alternatives to nft for better compatibility with newer kernels for non resolute"
update-alternatives --set iptables /usr/sbin/iptables-nft || true
update-alternatives --set ip6tables /usr/sbin/ip6tables-nft || true
fi
Expand Down Expand Up @@ -970,6 +976,29 @@ DOCKER_DEFAULT_ADDRESS_POOL=${DOCKER_DEFAULT_ADDRESS_POOL}
DOCKER_DEFAULT_IP6_TABLES=${DOCKER_DEFAULT_IP6_TABLES}
EOF

# On Debian-based images, re-assert the iptables alternative at container start
# (only when the user opted into runtime switching via iptablesSwitchAtRuntime=true).
if [ "${IPTABLES_SWITCH_AT_RUNTIME}" = "true" ] && [ "${ADJUSTED_ID}" = "debian" ]; then
tee -a /usr/local/share/docker-init.sh > /dev/null \
<< 'EOF'
# Prefer legacy only when the ip_tables kernel module is actually present.
# (Do NOT call `iptables-legacy -L/-nL` to test this — it auto-modprobes ip_tables
# and would defeat hosts/scenarios where the module is intentionally absent
# such as the newer kernels which leaves out ip_tables legacy.)
if type iptables-legacy > /dev/null 2>&1 \
&& { grep -qE '^(ip_tables)\b' /proc/modules \
|| [ -d /sys/module/ip_tables ]; } \
&& update-alternatives --list iptables 2>/dev/null | grep -q '/usr/sbin/iptables-legacy'; then
update-alternatives --set iptables /usr/sbin/iptables-legacy || true
update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy || true
elif type iptables-nft > /dev/null 2>&1 \
&& update-alternatives --list iptables 2>/dev/null | grep -q '/usr/sbin/iptables-nft'; then
update-alternatives --set iptables /usr/sbin/iptables-nft || true
update-alternatives --set ip6tables /usr/sbin/ip6tables-nft || true
fi
EOF
fi

tee -a /usr/local/share/docker-init.sh > /dev/null \
<< 'EOF'
dockerd_start="AZURE_DNS_AUTO_DETECTION=${AZURE_DNS_AUTO_DETECTION} DOCKER_DEFAULT_ADDRESS_POOL=${DOCKER_DEFAULT_ADDRESS_POOL} DOCKER_DEFAULT_IP6_TABLES=${DOCKER_DEFAULT_IP6_TABLES} $(cat << 'INNEREOF'
Expand Down
1 change: 1 addition & 0 deletions test/docker-in-docker/docker_compose_latest_moby.sh
14 changes: 14 additions & 0 deletions test/docker-in-docker/docker_compose_latest_no_moby.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash

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 '[0-9]+\.[0-9]+\.[0-9]+'"
check "docker-compose" bash -c "docker-compose --version | grep -E '[0-9]+\.[0-9]+\.[0-9]+'"
check "installs compose as docker-compose" bash -c "[[ -f /usr/local/bin/docker-compose ]]"

# Report result
reportResults
22 changes: 22 additions & 0 deletions test/docker-in-docker/docker_iptables_switch_at_install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash

set -e

# Optional: Import test library
source dev-container-features-test-lib

# Default behavior (iptablesSwitchAtRuntime omitted -> false): switching happens
# at image build time, so docker-init.sh should NOT contain the runtime block.
check "init-script-exists" bash -c "test -f /usr/local/share/docker-init.sh"
check "no-runtime-iptables-block" bash -c "! grep -q 'update-alternatives --set iptables' /usr/local/share/docker-init.sh"

# The build-time switch should have set /etc/alternatives/iptables to one of the
# known backends. With the ip_tables module loaded on the host, legacy is preferred.
check "iptables-alternative-set" bash -c "readlink /etc/alternatives/iptables | grep -E 'iptables-(legacy|nft)$'"
check "iptables works" sudo iptables -L

check "version" docker --version
check "docker-ps" bash -c "docker ps"

# Report result
reportResults
24 changes: 24 additions & 0 deletions test/docker-in-docker/docker_iptables_switch_at_runtime.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash

set -e

# Optional: Import test library
source dev-container-features-test-lib

# iptablesSwitchAtRuntime=true: switching is deferred to container start, so the
# runtime block MUST have been written into docker-init.sh by install.sh.
check "init-script-exists" bash -c "test -f /usr/local/share/docker-init.sh"
check "runtime-iptables-block-present" bash -c "grep -q 'update-alternatives --set iptables' /usr/local/share/docker-init.sh"
check "runtime-iptables-block-has-legacy-branch" bash -c "grep -q '/usr/sbin/iptables-legacy' /usr/local/share/docker-init.sh"
check "runtime-iptables-block-has-nft-branch" bash -c "grep -q '/usr/sbin/iptables-nft' /usr/local/share/docker-init.sh"

# The runtime block runs as part of docker-init.sh (the feature's entrypoint),
# so by the time these tests execute the alternative must already be set.
check "iptables-alternative-set" bash -c "readlink /etc/alternatives/iptables | grep -E 'iptables-(legacy|nft)$'"
check "iptables works" sudo iptables -L

check "version" docker --version
check "docker-ps" bash -c "docker ps"

# Report result
reportResults
20 changes: 20 additions & 0 deletions test/docker-in-docker/docker_stress_iptables_runtime.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash

set -e

# Optional: Import test library
source dev-container-features-test-lib

# Stress scenario: validates the docker daemon works when the iptables
# alternative switching is deferred to container start (iptablesSwitchAtRuntime=true).
check "init-script-exists" bash -c "test -f /usr/local/share/docker-init.sh"
check "runtime-iptables-block-present" bash -c "grep -q 'update-alternatives --set iptables' /usr/local/share/docker-init.sh"

check "version" docker --version
check "docker-ps" bash -c "docker ps"
check "log-exists" bash -c "ls /tmp/dockerd.log"
check "log-for-completion" bash -c "cat /tmp/dockerd.log | grep 'Daemon has completed initialization'"
check "log-contents" bash -c "cat /tmp/dockerd.log | grep 'API listen on /var/run/docker.sock'"

# Report result
reportResults
20 changes: 20 additions & 0 deletions test/docker-in-docker/docker_with_iptables.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash

set -e

# Optional: Import test library
source dev-container-features-test-lib

# Feature specific tests
check "iptables works" sudo iptables -L
check "iptables uses legacy" bash -c "iptables --version | grep legacy"

check "version" docker --version
check "docker-ps" bash -c "docker ps"
check "log-exists" bash -c "ls /tmp/dockerd.log"
check "log-for-completion" bash -c "cat /tmp/dockerd.log | grep 'Daemon has completed initialization'"
check "log-contents" bash -c "cat /tmp/dockerd.log | grep 'API listen on /var/run/docker.sock'"

# Report result
reportResults

20 changes: 20 additions & 0 deletions test/docker-in-docker/docker_with_iptables_ubuntu.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash

set -e

# Optional: Import test library
source dev-container-features-test-lib

# Feature specific tests
check "iptables works" sudo iptables -L
check "iptables uses legacy" bash -c "iptables --version | grep legacy"

check "version" docker --version
check "docker-ps" bash -c "docker ps"
check "log-exists" bash -c "ls /tmp/dockerd.log"
check "log-for-completion" bash -c "cat /tmp/dockerd.log | grep 'Daemon has completed initialization'"
check "log-contents" bash -c "cat /tmp/dockerd.log | grep 'API listen on /var/run/docker.sock'"

# Report result
reportResults

34 changes: 34 additions & 0 deletions test/docker-in-docker/docker_without_iptables.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash

set -e

# Optional: Import test library
source dev-container-features-test-lib

# Feature specific tests
check "docker-ps" bash -c "docker ps"
# Fail loudly if dockerd never finished initializing, printing the real error
check "dockerd-started-successfully" bash -c '
if ! grep -q "Daemon has completed initialization" /tmp/dockerd.log; then
echo "❌ Docker daemon failed to start. Last errors from /tmp/dockerd.log:"
echo "----- dockerd.log (tail) -----"
tail -n 100 /tmp/dockerd.log
echo "----- error/fatal lines -----"
grep -iE "error|fatal|failed|panic" /tmp/dockerd.log || true
exit 1
fi
'
check "log-for-completion" bash -c "cat /tmp/dockerd.log | grep 'Daemon has completed initialization'"

check "iptables works" sudo iptables -L
check "iptables uses nf_tables" bash -c "iptables --version | grep nf_tables"

check "version" docker --version
check "docker-ps" bash -c "docker ps"
check "log-exists" bash -c "ls /tmp/dockerd.log"
check "log-for-completion" bash -c "cat /tmp/dockerd.log | grep 'Daemon has completed initialization'"
check "log-contents" bash -c "cat /tmp/dockerd.log | grep 'API listen on /var/run/docker.sock'"

# Report result
reportResults

20 changes: 20 additions & 0 deletions test/docker-in-docker/docker_without_iptables_ubuntu.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash

set -e

# Optional: Import test library
source dev-container-features-test-lib

# Feature specific tests
check "iptables works" sudo iptables -L
check "iptables uses nf_tables" bash -c "iptables --version | grep nf_tables"

check "version" docker --version
check "docker-ps" bash -c "docker ps"
check "log-exists" bash -c "ls /tmp/dockerd.log"
check "log-for-completion" bash -c "cat /tmp/dockerd.log | grep 'Daemon has completed initialization'"
check "log-contents" bash -c "cat /tmp/dockerd.log | grep 'API listen on /var/run/docker.sock'"

# Report result
reportResults

Loading
Loading