Skip to main content

Container Startup Lifecycle

How DCT initializes a devcontainer from first start to ready-to-develop, and why the startup is split across three stages.


The Three Stages​


Stage 1: initializeCommand (runs on host)​

File: Defined in devcontainer.json

"initializeCommand": "mkdir -p .devcontainer.secrets/env-vars && hostname -s > .devcontainer.secrets/env-vars/.host-hostname 2>/dev/null || hostname > .devcontainer.secrets/env-vars/.host-hostname 2>/dev/null || true"

Runs on the host machine before the container starts. This is the only stage that executes outside the container.

Purpose: Capture the host's real hostname. This is needed because:

  • macOS (zsh) doesn't export HOSTNAME as an environment variable
  • remoteEnv can only pass variables that exist — if HOSTNAME is empty, DEV_HOST_HOSTNAME is empty
  • hostname -s works on Mac, Linux, and Windows (WSL2)

Output: .devcontainer.secrets/env-vars/.host-hostname containing the short hostname (e.g., MBP-J4G0G066W2).

Cross-platform behavior:

Platformhostname -s returns
macOSMachine name (e.g., MBP-J4G0G066W2)
LinuxMachine hostname (e.g., terje-desktop)
Windows (WSL2)WSL hostname
Windows (PowerShell)Falls back to hostname without -s

Stage 2: ENTRYPOINT (runs in container, no remoteEnv)​

File: image/entrypoint.sh

The ENTRYPOINT is a Docker-level construct. It runs as PID 1 before any IDE connects. remoteEnv variables are NOT available at this stage — VS Code hasn't connected yet.

Every start​

StepWhat it doesFile
Git configSet safe directory, file mode, hidden filesentrypoint.sh
GitignoreEnsure .devcontainer.secrets/ is gitignoredlib/ensure-gitignore.sh
VS Code extensionsEnsure Dev Containers extension is recommendedlib/ensure-vscode-extensions.sh
CredentialsSymlink Claude Code + GitHub CLI credentialslib/claude-credential-sync.sh, lib/gh-credential-sync.sh
Git identityApply host-captured git name/emailentrypoint.sh
ServicesStart supervisord, OTel monitoring if enabledentrypoint.sh
Version checkCheck if newer DCT image available (5s timeout)entrypoint.sh

First start only (inside INIT_MARKER check)​

StepWhat it does
Create .devcontainer.extend/Generate enabled-tools.conf, enabled-services.conf, project-installs.sh with templates
Restore configsRun --verify on all config scripts (git, Azure DevOps, etc.) — except config-host-info.sh which needs remoteEnv
Install toolsRead enabled-tools.conf, run install scripts for each enabled tool
Project installsRun .devcontainer.extend/project-installs.sh for custom setup

Output​

All ENTRYPOINT output is redirected to /tmp/.dct-startup.log. The welcome script (dev-welcome.sh) streams this to the user's first terminal.


Stage 3: postStartCommand (runs in container, remoteEnv available)​

File: Defined in devcontainer.json, calls config-host-info.sh

"postStartCommand": "bash /opt/devcontainer-toolbox/additions/config-host-info.sh --verify 2>/dev/null || true"

Runs after VS Code connects and injects remoteEnv. This is the first point where DEV_HOST_* variables are available.

Purpose: Detect the host platform and save the results to .host-info.


Host Info Detection (config-host-info.sh)​

Hostname resolution chain​

config-host-info.sh resolves the host's hostname using a 4-step fallback:

SourceWorks onHow it gets there
DEV_HOST_HOSTNAMELinux${localEnv:HOSTNAME} in remoteEnv — bash exports it
DEV_HOST_COMPUTERNAMEWindows${localEnv:COMPUTERNAME} in remoteEnv
.host-hostname fileMac, Linux, WindowsinitializeCommand runs hostname -s on host
Fallback devcontainerAllWhen nothing else is available

Platform detection logic​

if [ "${DEV_HOST_OS}" = "Windows_NT" ]; then
HOST_OS="Windows"
elif [[ "${DEV_HOST_HOME}" == /Users/* ]]; then
HOST_OS="macOS"
elif [ -n "${DEV_HOST_USER}" ]; then
HOST_OS="Linux"
else
HOST_OS="unknown"
fi

Docker engine detection​

config-host-info.sh also captures Docker host info via docker info:

FieldExampleWhat it reveals
DOCKER_CONTAINER_NAMEmusing_sandersonVS Code's container name
DOCKER_HOST_NAMElima-rancher-desktopDocker runtime (Rancher Desktop, Docker Desktop, OrbStack)
DOCKER_HOST_CPUS8Host CPU count
DOCKER_HOST_MEMORY15.6GiBHost RAM available to Docker
DOCKER_HOST_ARCHaarch64Host CPU architecture
DOCKER_HOST_OSAlpine Linux v3.23Docker VM's OS

Output: .host-info​

All detected values are saved to .devcontainer.secrets/env-vars/.host-info:

export HOST_OS="macOS"
export HOST_USER="terje.christensen"
export HOST_HOSTNAME="MBP-J4G0G066W2"
export HOST_DOMAIN="none"
export HOST_CPU_ARCH="arm64"
export DOCKER_CONTAINER_NAME="musing_sanderson"
export DOCKER_HOST_NAME="lima-rancher-desktop"
export DOCKER_HOST_CPUS="8"
export DOCKER_HOST_MEMORY="15.6GiB"
export DOCKER_HOST_ARCH="aarch64"
export DOCKER_HOST_OS="Alpine Linux v3.23"

This file is:

  • Sourced by OTel configs for telemetry resource attributes
  • Read by dev-env to display host information
  • Persists across container restarts (workspace mount)
  • Overwritten on every container start by postStartCommand

Commands​

CommandWhat it does
config-host-info --envShow all DEV_HOST_* environment variables
config-host-info --refreshRe-detect and show results
config-host-info --verifySilent re-detect and save (used by postStartCommand)
config-host-info --showDisplay saved .host-info contents

Why Three Stages?​

The split exists because of a fundamental constraint: remoteEnv is injected by VS Code after the container starts, not before.

StageRunsremoteEnv availableCan write to workspace
initializeCommandOn hostN/A (host env)Yes (host filesystem)
ENTRYPOINTIn containerNoYes
postStartCommandIn containerYesYes

If VS Code injected remoteEnv before the ENTRYPOINT, all of this could be a single stage. But the devcontainer spec doesn't work that way — the ENTRYPOINT is a Docker concept, and VS Code layers its features on top after the container is running.


Known Issue: Permission Handling in CI​

In CI (ci-tests.yml), the workspace is mounted with the GitHub Actions runner user, but the container runs as vscode. This causes "Permission denied" when scripts try to write to workspace files (like .gitignore).

ensure-gitignore.sh handles this by wrapping all writes with 2>/dev/null || true. Any script that writes to workspace files during startup should follow the same pattern to avoid breaking CI tests.


File Reference​

FilePurpose
image/entrypoint.shMain startup script (ENTRYPOINT)
.devcontainer/additions/config-host-info.shHost detection + Docker engine info
.devcontainer/additions/lib/ensure-gitignore.shGitignore management (sourced by logging.sh)
.devcontainer/additions/lib/ensure-vscode-extensions.shVS Code extension recommendations
.devcontainer/manage/dev-welcome.shWelcome message + startup log display
.devcontainer.secrets/env-vars/.host-infoSaved host detection results
.devcontainer.secrets/env-vars/.host-hostnameHost hostname (from initializeCommand)
/tmp/.dct-startup.logStartup log (streamed to first terminal)
/tmp/.dct-initializedInit marker (prevents re-running first-start block)