Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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_iptables_switch_at_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_with_default_iptables, .docker_with_default_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 (run in separate 'iptables-isolation' job)"
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_with_default_iptables, .docker_with_default_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_with_default_iptables
- docker_with_default_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 .
1 change: 1 addition & 0 deletions src/docker-in-docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Create child containers *inside* a container, independent from the host's docker
| 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
7 changes: 6 additions & 1 deletion 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.1.0",
"version": "3.2.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 @@ -61,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
35 changes: 32 additions & 3 deletions src/docker-in-docker/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
abdurriq marked this conversation as resolved.
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
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
33 changes: 33 additions & 0 deletions test/docker-in-docker/docker_with_default_iptables.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/bash
Comment thread
Kaniska244 marked this conversation as resolved.

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 "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_with_iptables.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash
Comment thread
Kaniska244 marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: docker_with_legacy_iptables would be a more accurate name


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

1 change: 1 addition & 0 deletions test/docker-in-docker/docker_with_iptables_ubuntu.sh
63 changes: 62 additions & 1 deletion test/docker-in-docker/scenarios.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,64 @@
{
"docker_iptables_switch_at_install": {
"image": "mcr.microsoft.com/devcontainers/base:debian",
"features": {
"docker-in-docker": {
"moby": "false"
}
},
"initializeCommand": "sudo modprobe ip_tables"
},
// DO NOT REMOVE: This scenario is used by the docker-in-docker-stress-test workflow
"docker_iptables_switch_at_runtime": {
"image": "mcr.microsoft.com/devcontainers/base:debian",
"features": {
"docker-in-docker": {
"moby": "false",
"iptablesSwitchAtRuntime": true
}
},
"initializeCommand": "sudo modprobe ip_tables"
},
"docker_with_default_iptables": {
"image": "mcr.microsoft.com/devcontainers/base:debian",
"features": {
"docker-in-docker": {
"moby": "false",
"iptablesSwitchAtRuntime": true
}
},
"initializeCommand": "sudo modprobe --remove --remove-holders --wait 1000 ip_tables"
},
"docker_with_iptables": {
"image": "mcr.microsoft.com/devcontainers/base:debian",
"features": {
"docker-in-docker": {
"moby": "false",
"iptablesSwitchAtRuntime": true
}
},
"initializeCommand": "sudo modprobe ip_tables"
},
"docker_with_default_iptables_ubuntu": {
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"docker-in-docker": {
"moby": "false",
"iptablesSwitchAtRuntime": true
}
},
"initializeCommand": "sudo modprobe --remove --remove-holders --wait 1000 ip_tables"
},
"docker_with_iptables_ubuntu": {
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"docker-in-docker": {
"moby": "false",
"iptablesSwitchAtRuntime": true
}
},
"initializeCommand": "sudo modprobe ip_tables"
},
"overlayfs_containerd_root": {
"image": "mcr.microsoft.com/devcontainers/base:noble",
"features": {
Expand Down Expand Up @@ -221,7 +281,8 @@
"features": {
"docker-in-docker": {
"version": "latest",
"moby": "false"
"moby": "false",
"iptablesSwitchAtRuntime": true
}
},
"remoteUser": "vscode",
Expand Down
Loading