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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ jobs:
"build-essential",
"claude-code",
"cypress-deps",
"docker-in",
"docker-out",
"dotnet",
"eclipse-deps",
Expand Down
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"files.associations": {
"**/features/src/docker-in/dind-init.sh.tmpl": "shellscript",
"**/features/src/docker-out/docker-init.sh.tmpl": "shellscript"
},
"json.schemaDownload.trustedDomains": {
"https://docs.renovatebot.com/renovate-schema.json": true
}
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Below is a list with included features, click on the link for more details.
| [build-essential](./features/src/build-essential/README.md) | Installs build essentials like gcc. |
| [claude-code](./features/src/claude-code/README.md) | Installs Claude Code, Anthropic's AI coding assistant CLI. |
| [cypress-deps](./features/src/cypress-deps/README.md) | Installs all dependencies required to run Cypress. |
| [docker-in](./features/src/docker-in/README.md) | Installs and runs a full Docker daemon. |
| [docker-out](./features/src/docker-out/README.md) | Installs a Docker client which re-uses the host Docker socket. |
| [dotnet](./features/src/dotnet/README.md) | A package which installs .NET SDKs, runtimes and workloads. |
| [eclipse-deps](./features/src/eclipse-deps/README.md) | Installs all dependencies required to run the Eclipse IDE. |
Expand Down
11 changes: 10 additions & 1 deletion build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ func init() {
gotaskr.Task("Feature:cypress-deps:Package", func() error { return packageFeature("cypress-deps") })
gotaskr.Task("Feature:cypress-deps:Test", func() error { return testFeature("cypress-deps") })

////////// docker-in
gotaskr.Task("Feature:docker-in:Package", func() error { return packageFeature("docker-in") })
gotaskr.Task("Feature:docker-in:Test", func() error { return testFeature("docker-in") })

////////// docker-out
gotaskr.Task("Feature:docker-out:Package", func() error { return packageFeature("docker-out") })
gotaskr.Task("Feature:docker-out:Test", func() error { return testFeature("docker-out") })
Expand Down Expand Up @@ -411,7 +415,12 @@ func testFeature(featureName string) error {
}

// Run the check in the container
checkError := goext.CmdRunners.Console.Run("docker", "run", "-t", "--rm", "-v", "/var/run/docker.sock:/var/run/docker.sock", imageName, "sh", "-c", "/tmp/check.sh")
runArgs := []string{"run", "-t", "--rm"}
if featureName == "docker-out" {
runArgs = append(runArgs, "-v", "/var/run/docker.sock:/var/run/docker.sock")
}
runArgs = append(runArgs, imageName, "sh", "-c", "/tmp/check.sh")
checkError := goext.CmdRunners.Console.Run("docker", runArgs...)
if checkError != nil {
return fmt.Errorf("check failed: %w", checkError)
}
Expand Down
16 changes: 4 additions & 12 deletions features/src/apm/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"os"
"path/filepath"
"regexp"
"strconv"

"github.com/roemer/gover"
)
Expand All @@ -31,17 +30,10 @@ func main() {

func runMain() error {
// Check Preconditions
osInfo, err := installer.Tools.System.GetOsInfo()
if err != nil {
return fmt.Errorf("failed getting os info: %v", err)
}
if osInfo.Vendor == "debian" {
versionId, err := strconv.Atoi(osInfo.VersionId)
if err != nil {
return fmt.Errorf("failed parsing the version number from %s: %v", osInfo.Vendor, err)
}
if versionId < 13 {
return fmt.Errorf("unsupported debian version: %d", versionId)
osInfo := installer.Tools.System.OsInfo()
if osInfo.IsDebian() {
if osInfo.MajorVersion() < 13 {
return fmt.Errorf("unsupported debian version: %s", osInfo.VersionId)
}
}

Expand Down
6 changes: 1 addition & 5 deletions features/src/claude-code/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,8 @@ func (c *claudeCodeComponent) InstallVersion(version *gover.Version) error {
}

// Use musl variant for Alpine (musl libc)
osInfo, err := installer.Tools.System.GetOsInfo()
if err != nil {
return err
}
var fileName string
if osInfo.IsAlpine() {
if installer.Tools.System.OsInfo().IsAlpine() {
fileName = fmt.Sprintf("claude-linux-%s-musl.tar.gz", archPart)
} else {
fileName = fmt.Sprintf("claude-linux-%s.tar.gz", archPart)
Expand Down
5 changes: 1 addition & 4 deletions features/src/cypress-deps/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ func main() {

func runMain() error {
fmt.Println("Installing Cypress Dependencies")
osInfo, err := installer.Tools.System.GetOsInfo()
if err != nil {
return fmt.Errorf("failed to get OS info: %w", err)
}
osInfo := installer.Tools.System.OsInfo()

// Common dependencies for all supported distros
commonDeps := []string{
Expand Down
39 changes: 39 additions & 0 deletions features/src/docker-in/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Docker inside Docker (docker-in)

Installs and runs a full Docker daemon.

## Example Usage

```json
"features": {
"ghcr.io/postfinance/devcontainer-features/docker-in:1.0.0": {
"version": "latest",
"composeVersion": "latest",
"buildxVersion": "latest",
"configPath": "",
"downloadUrl": "",
"versionsUrl": "",
"composeDownloadUrl": "",
"buildxDownloadUrl": ""
}
}
```

## Options

| Option | Description | Type | Default Value | Proposals |
|-----|-----|-----|-----|-----|
| version | The version of the Docker Engine and CLI to install. | string | latest | latest, 28.3.3, 20.10 |
| composeVersion | The version of the Compose plugin to install. | string | latest | latest, none, 2.39.1, 2.29 |
| buildxVersion | The version of the buildx plugin to install. | string | latest | latest, none, 0.26.1, 0.10 |
| configPath | Path or URL to a custom Docker client config.json file to copy into the container. | string | &lt;empty&gt; | /home/user/.docker/config.json, https://raw.githubusercontent.com/devcontainers/features/main/src/docker-in/config.json, none |
| downloadUrl | The download URL to use for Docker binaries (engine, CLI, containerd). | string | &lt;empty&gt; | |
| versionsUrl | The URL to use for checking available versions. | string | &lt;empty&gt; | |
| composeDownloadUrl | The download URL to use for Docker Compose binaries. | string | &lt;empty&gt; | |
| buildxDownloadUrl | The download URL to use for Docker Buildx binaries. | string | &lt;empty&gt; | |

## Customizations

### VS Code Extensions

- `ms-azuretools.vscode-containers`
91 changes: 91 additions & 0 deletions features/src/docker-in/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
"id": "docker-in",
"version": "1.0.0",
"name": "Docker inside Docker",
"description": "Installs and runs a full Docker daemon.",
"options": {
"version": {
"type": "string",
"proposals": [
"latest",
"28.3.3",
"20.10"
],
"default": "latest",
"description": "The version of the Docker Engine and CLI to install."
},
"composeVersion": {
"type": "string",
"proposals": [
"latest",
"none",
"2.39.1",
"2.29"
],
"default": "latest",
"description": "The version of the Compose plugin to install."
},
"buildxVersion": {
"type": "string",
"proposals": [
"latest",
"none",
"0.26.1",
"0.10"
],
"default": "latest",
"description": "The version of the buildx plugin to install."
},
"configPath": {
"type": "string",
"default": "",
"description": "Path or URL to a custom Docker client config.json file to copy into the container.",
"proposals": [
"/home/user/.docker/config.json",
"https://raw.githubusercontent.com/devcontainers/features/main/src/docker-in/config.json",
"none"
]
},
"downloadUrl": {
"type": "string",
"default": "",
"description": "The download URL to use for Docker binaries (engine, CLI, containerd)."
},
"versionsUrl": {
"type": "string",
"default": "",
"description": "The URL to use for checking available versions."
},
"composeDownloadUrl": {
"type": "string",
"default": "",
"description": "The download URL to use for Docker Compose binaries."
},
"buildxDownloadUrl": {
"type": "string",
"default": "",
"description": "The download URL to use for Docker Buildx binaries."
}
},
"customizations": {
"vscode": {
"extensions": [
"ms-azuretools.vscode-containers"
]
}
},
"mounts": [
{
"source": "dind-var-lib-docker-${devcontainerId}",
"target": "/var/lib/docker",
"type": "volume"
},
{
"source": "dind-var-lib-containerd-${devcontainerId}",
"target": "/var/lib/containerd",
"type": "volume"
}
],
"entrypoint": "/usr/local/share/dind-init.sh",
"privileged": true
}
98 changes: 98 additions & 0 deletions features/src/docker-in/dind-init.sh.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -e

# Wrapper function to only use sudo if not already root
sudoIf() {
if [ "$(id -u)" -ne 0 ]; then
sudo "$@"
else
"$@"
fi
}

# Setup logging
LOG_DIR=/var/log/feat-docker-in
LOG_FILE=$LOG_DIR/entrypoint.log
sudoIf mkdir -p "$LOG_DIR"
sudoIf chmod 755 "$LOG_DIR"

# Redirect all stdout/stderr to log
exec > >(sudoIf tee -a "$LOG_FILE") 2>&1

echo "=== Entrypoint Start $(date -Is) ==="
echo "[entrypoint] Running with args: $*"

# Determine the correct user
USERNAME="{{ .User }}"
if [ "${USERNAME}" = "" ]; then
USERNAME=$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)
fi

echo "[entrypoint] Running for user: ${USERNAME}"

# Ensure the docker group exists and the user is a member before starting the
# daemon, so dockerd creates the socket with the correct group ownership
getent group docker >/dev/null 2>&1 || sudoIf groupadd docker
sudoIf usermod -aG docker "${USERNAME}" || true

# Ensure runtime directories exist
sudoIf mkdir -p /run/docker /var/lib/docker /var/lib/containerd
sudoIf rm -f /var/run/docker.pid

# Start the Docker daemon
LOG_LEVEL="${DOCKERD_LOG_LEVEL:-info}"
echo "[entrypoint] Starting dockerd (log-level=${LOG_LEVEL})..."
sudoIf dockerd \
--host=unix:///var/run/docker.sock \
--group=docker \
--storage-driver=overlay2 \
--log-level="${LOG_LEVEL}" &

DOCKERD_PID=$!
echo "[entrypoint] dockerd pid $DOCKERD_PID"

# Forward termination signals so dockerd gets a chance to shut down cleanly
shutdown() {
echo "[entrypoint] Received signal, stopping dockerd (pid $DOCKERD_PID)..."
sudoIf kill -TERM "$DOCKERD_PID" 2>/dev/null || true
wait "$DOCKERD_PID" 2>/dev/null || true
exit 0
}
trap shutdown TERM INT

# Wait for daemon to become ready
TRIES=0
until sudoIf docker version >/dev/null 2>&1; do
if ! sudoIf kill -0 "$DOCKERD_PID" 2>/dev/null; then
echo "[entrypoint] ERROR: dockerd exited during startup!"
sudoIf tail -n 50 "$LOG_FILE" || true
exit 1
fi
if [ $TRIES -ge 60 ]; then
echo "[entrypoint] ERROR: Docker daemon did not become ready in time!"
sudoIf tail -n 50 "$LOG_FILE" || true
exit 1
fi
sleep 1
TRIES=$((TRIES+1))
echo "[entrypoint] waiting for dockerd... ($TRIES)"
done

echo "[entrypoint] Docker daemon is ready"

# Ensure the socket is group-accessible even if it predated the group.
sudoIf chown root:docker /var/run/docker.sock 2>/dev/null || true
sudoIf chmod 660 /var/run/docker.sock 2>/dev/null || true

# Finish up
echo "=== Entrypoint Complete $(date -Is) ==="

# Execute whatever commands were passed in. This allows us to set this script as
# ENTRYPOINT while still executing the default CMD. When no CMD is given, keep
# the container (and daemon) alive and wait on dockerd so the signal trap works.
set +e
if [ "$#" -gt 0 ]; then
exec "$@"
else
wait "$DOCKERD_PID"
fi
11 changes: 11 additions & 0 deletions features/src/docker-in/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
. ./functions.sh

"./installer_$(detect_arch)" \
-version="${VERSION:-"latest"}" \
-composeVersion="${COMPOSEVERSION:-"latest"}" \
-buildxVersion="${BUILDXVERSION:-"latest"}" \
-configPath="${CONFIGPATH:-""}" \
-downloadUrl="${DOWNLOADURL:-""}" \
-versionsUrl="${VERSIONSURL:-""}" \
-composeDownloadUrl="${COMPOSEDOWNLOADURL:-""}" \
-buildxDownloadUrl="${BUILDXDOWNLOADURL:-""}"
Loading
Loading