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
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 .
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.0.1",
"version": "4.0.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 @@ -60,6 +60,11 @@
"type": "boolean",
"default": false,
"description": "Disable ip6tables (this option is only applicable for Docker versions 27 and greater)"
},
"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
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
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