Skip to content

Latest commit

 

History

History
134 lines (86 loc) · 11.3 KB

devcontainer.md

File metadata and controls

134 lines (86 loc) · 11.3 KB

Devcontainer Setup

This repo ships no application toolchain. It keeps two per-language Dev Container definitions under catalog/snippets/devcontainer/ as a reference for fleet code repos, each carrying one toolchain, extension surface, and postCreateCommand. The mechanics below (SSH commit signing, bind mounts, and gh auth) apply to any repo that uses them.

Devcontainer Image Toolchain
catalog/snippets/devcontainer/dotnet/devcontainer.json mcr.microsoft.com/devcontainers/dotnet:2-10.0 .NET 10 SDK
catalog/snippets/devcontainer/python/devcontainer.json mcr.microsoft.com/devcontainers/python:3-3.14-bookworm Python 3.14 + version-pinned uv

Each tag pins the image's major version and its runtime version, so it takes that major's updates without an edit. A maintainer changes a tag by hand, because Dependabot's devcontainers ecosystem updates a definition's Features and not its image. After changing a tag, start the definition with its post-create step, since a tag can be absent from the registry or fail its feature build. Start it in a carrying repo or in a scratch tree holding it under .devcontainer/<language>/, the path its postCreateCommand names. A .NET scratch tree also needs a local tool manifest, since its post-create restores local tools.

In a repo that carries one of these definitions, install the Dev Containers extension and pick Reopen in Container.

Prerequisite: complete host setup first. Without git config, an SSH key, and the allowed-signers file on the host, the devcontainer cannot sign commits.

What's Inside (Both Containers)

Component Source Purpose
gh CLI ghcr.io/devcontainers/features/github-cli:1 Issue/PR/release management from inside the container
Common utilities ghcr.io/devcontainers/features/common-utils:2 bash, curl, wget, sudo, vscode user
VS Code extensions customizations.vscode.extensions in each devcontainer.json The standard set, the container's language defaults, and the Docker extension

The .NET container additionally ships the csharpier/dotnet-outdated local tools (restored by catalog/snippets/devcontainer/dotnet/post-create.sh). The Python container additionally ships uv (installed by catalog/snippets/devcontainer/python/post-create.sh from a version-pinned URL) and pre-syncs the Python package venv where one is present.

Each devcontainer's extension list is the workspace catalog's base.jsonc set plus that container's own language fragment and docker.jsonc. A repo carrying a devcontainer drops the Docker extension where it ships no Docker target.

Bind Mounts (Both Containers)

The host SSH key, allowed-signers file, and gh config directory are mounted into the container so commits sign correctly. The mounts also make gh pre-authenticated when the host stores its gh token in a file (~/.config/gh/hosts.yml). Hosts that store the token in macOS Keychain or Linux libsecret will need an in-container gh auth login instead. See gh credential store below for the full picture.

Host path Container path Mode Purpose
~/.ssh/id_ed25519.pub /home/vscode/.ssh/id_ed25519.pub read-only Public half of the SSH key. The private key never enters the container, since SSH agent forwarding handles signing.
~/.config/git/allowed_signers /home/vscode/.config/git/allowed_signers read-only Maps your email to your public key so git verify-commit and git log --show-signature work inside the container.
~/.config/gh /home/vscode/.config/gh read-write gh CLI auth state shared with the host. See gh credential store below.

Each mount uses the string form, which passes straight to Docker's --mount flag. The Dev Containers CLI drops a readonly key from the object form, which leaves the host file writable from the container.

Each mount's source finds the host home directory by joining ${localEnv:HOME}${localEnv:USERPROFILE}, so exactly one of those variables must be set. Each host path must also exist under that directory, since Docker refuses an absent one inside WSL2. See Troubleshooting for a native Windows launch.

VS Code Dev Containers automatically copies your host ~/.gitconfig into the container at startup. So user.name, user.email, user.signingkey, gpg.format, and commit.gpgsign propagate without an explicit mount.

The SSH agent is forwarded automatically by the Dev Containers extension via SSH_AUTH_SOCK. So signing works as long as the agent on the host has your key loaded.

Lifecycle Commands

Both devcontainer.json files run two scripts at well-defined points:

  • onCreateCommand - sudo install -d -m 700 -o vscode -g vscode /home/vscode/.ssh. On macOS hosts the bind-mount surfaces /home/vscode/.ssh as root-owned, which would block writes from inside the container (e.g. gh updating known_hosts). This chown fixes it. Idempotent on Linux and WSL2.
  • postCreateCommand - language-specific:
    • .NET: catalog/snippets/devcontainer/dotnet/post-create.sh, which runs dotnet tool restore (csharpier, dotnet-outdated).
    • Python: catalog/snippets/devcontainer/python/post-create.sh, which installs the pinned uv and pre-syncs the Python package if present.

Re-runs of either are idempotent. No git hooks are installed by default.

To force them to run again after editing a script: VS Code -> Command Palette -> Dev Containers: Rebuild Container.

gh Credential Store

gh auth login writes its token to either a file or an OS credential store. Which one depends on your host:

Host Default token storage
Linux libsecret (gnome-keyring) when available, otherwise file
WSL2 file (no native credential store)
macOS macOS Keychain

The bind-mount of ~/.config/gh covers the file case. If your host stores the token in Keychain or libsecret, the bind-mount carries the rest of gh config but not the token. So the container will report "no authentication" until you either:

  1. Re-run gh auth login inside the container (writes a file token to the mounted directory), or
  2. Skip in-container gh and run those commands on the host instead.

The file-token path is slightly less secure than Keychain/libsecret because it's plaintext on disk inside ~/.config/gh/hosts.yml. For most contributors that's an acceptable trade-off, and if it isn't, use option 2.

Verify the Devcontainer

After Reopen in Container finishes, run the language-appropriate checks.

Both containers. Verify SSH signing and gh:

gh auth status                                     # logged in as you
git -c gpg.format=ssh commit -S --allow-empty -m "verify-signing"
git log --show-signature -1                        # "Good 'git' signature for ..."

.NET container (in a repo that ships .NET):

dotnet --version                                   # 10.x
which uv                                           # nothing - uv intentionally absent
dotnet build                                       # 0 warnings, 0 errors
dotnet test                                        # tests pass

Python container (in a repo whose Python is a uv project):

uv --version                                       # uv 0.x
which dotnet                                       # nothing - dotnet intentionally absent
cd "<package>" && uv sync && uv run pytest         # tests pass

If git -c gpg.format=ssh commit -S errors with signing failed: no allowed signers, the bind-mount of allowed_signers is missing or the file on the host is empty. In that case, re-run the snippet in host setup.

Troubleshooting

Container fails to start on Windows with The filename, directory name, or volume label syntax is incorrect. A launch with both HOME and USERPROFILE set joins them into an invalid bind-mount path. VS Code launched from Git Bash can inherit HOME, and a HOME variable set in the Windows environment reaches a Start menu launch too. devcontainer.json substitution has no fallback that picks one of them. The supported route is opening the repo from inside a WSL2 distribution, where each host path must exist under the WSL2 home.

A native launch opens the repo from Windows, even with Docker Desktop's WSL2 backend, and is outside that route. It needs HOME absent from VS Code's environment. Close every VS Code window, then launch VS Code from the Start menu. Where a HOME is set in the Windows environment, run Remove-Item Env:HOME in PowerShell, then code . from that session instead. Each host path must also exist under %USERPROFILE%. On Docker Desktop's WSL2 backend, an absent one becomes an empty host directory and the container still starts, even for a key file. Such a directory blocks the host setup steps that write that file. To recover, remove the container, delete that directory, and create the file. Then run Dev Containers: Reopen in Container. Windows gh defaults to %AppData%\GitHub CLI rather than .config\gh, so that mount can start empty and share no gh login. Evidence for a native launch is a devcontainer CLI up on Docker Desktop's WSL2 backend. It used scratch copies of the snippets, a synthetic home, and replaced image tags. So the VS Code launch, its .gitconfig copy, the lifecycle commands, and commit signing on a native launch are untested.

Permission denied writing to ~/.ssh/known_hosts in the container. The onCreateCommand should have chowned ~/.ssh to vscode. Rebuild the container, and if it persists, open a shell and run the same sudo install -d -m 700 -o vscode -g vscode ~/.ssh manually.

git commit fails with "no SSH agent socket". VS Code Dev Containers forwards SSH_AUTH_SOCK automatically, but only if the host has ssh-agent running with at least one key. Run ssh-add -l on the host first, and if it says "could not open a connection to your authentication agent", start the agent (see host setup).

uv not on PATH after rebuild (Python container). The post-create installer adds ~/.local/bin to PATH via the user shell init scripts, which take effect on next shell. Either re-open the integrated terminal or source ~/.bashrc.

Container builds but extensions don't auto-install. Make sure VS Code is using the Dev Containers extension (not "Remote - SSH" or "Remote - Tunnels"). The extension auto-install is keyed on customizations.vscode.extensions and only Dev Containers honors that.

Wrong-language work in the wrong container. The .NET container has no uv and no Python extensions, and the Python container has no dotnet SDK and no C# extensions. This is intentional, so use the matching container rather than installing the missing toolchain ad hoc.