Skip to content

Dev Container improvements#17348

Open
sgraband wants to merge 11 commits intomasterfrom
feat/dev-container
Open

Dev Container improvements#17348
sgraband wants to merge 11 commits intomasterfrom
feat/dev-container

Conversation

@sgraband
Copy link
Copy Markdown
Contributor

@sgraband sgraband commented Apr 14, 2026

What it does

Contributes to #14294

This PR adds several new features, bug fixes, and quality improvements to the dev container support in Theia:

New features:

  • Variable resolver fixes: Handle variables embedded in strings in devcontainer.json (e.g. ${localEnv:HOME}/path)
  • MountsContribution fix: Pass through all mount options (source, target, type) correctly instead of silently dropping them
  • SettingsContribution fix: Fix JSON quoting when injecting preferences through the shell by base64-encoding complex objects
  • portsAttributes support: Implement portsAttributes from devcontainer.json, allowing port labels, protocols, and auto-forward behavior to be configured
  • Attach to Running Container: Add a command to attach to an already-running Docker container without a devcontainer.json
  • SSH credential injection, git identity sharing, and shell detection: Automatically set up an isolated SSH directory with key forwarding, inject host .gitconfig (with SSH signing key rewriting), configure a login shell with SSH agent, and share host credentials into the container
  • Auto-reopen in last active dev container after restart: On application restart, automatically reconnect to the previously used dev container
  • Suggest to reopen workspace in dev container: When opening a workspace that contains a devcontainer.json, show a notification suggesting to reopen in a container
  • Rebuild Container command: Add a command to rebuild the dev container from scratch (removing the old container) while preserving the devcontainer file context
  • Ports view and port labels: Expose the Ports view in the remote package and display port labels from portsAttributes

How to test

Prerequisites: Docker must be installed and running.

  1. Reopen in Container

    • Open a workspace that contains a .devcontainer/devcontainer.json
    • Run Dev Container: Reopen in Container from the command palette
    • Verify Theia connects to the container and the workspace is available
  2. Suggestion notification

    • Open a workspace with a devcontainer.json from a local (non-remote) session
    • Verify a notification appears suggesting to reopen in a container
    • Click "Reopen in Container" and verify it works
  3. Rebuild Container

    • While connected to a dev container, run Dev Container: Rebuild Container
    • Verify the old container is removed and a new one is created
    • Verify the workspace reconnects automatically
  4. Attach to Running Container

    • Start a Docker container manually (e.g. docker run -it ubuntu bash)
    • Run Dev Container: Attach to Running Container
    • Select the running container and verify Theia connects
  5. SSH credentials and git identity

    • Ensure you have ~/.ssh/config and ~/.gitconfig on the host
    • Reopen in a container and verify:
      • git config user.name and user.email are inherited
      • ssh -T git@github.com works (key forwarding)
      • SSH agent is running (echo $SSH_AUTH_SOCK)
  6. Port forwarding with labels

    • Use a devcontainer.json with forwardPorts and portsAttributes (e.g. label, protocol)
    • Verify ports are forwarded and labels appear in the Ports view
  7. Variable resolver with embedded variables

    • Use a devcontainer.json with variables embedded in strings, e.g.:
      { "remoteEnv": { "MY_VAR": "${localEnv:HOME}/projects" } }
    • Reopen in the container and verify echo $MY_VAR resolves to the host's $HOME/projects (not the literal ${localEnv:HOME} string)
  8. Mount options pass-through

    • Add a mounts entry in devcontainer.json using the string form with extra options, e.g.:
      { "mounts": ["type=bind,source=${localEnv:HOME}/.config,target=/home/vscode/.config,readonly"] }
    • Reopen in the container and verify the mount exists (mount | grep .config), is read-only, and uses the correct source/target paths
  9. Settings with complex JSON values

    • Add a complex (object/array) setting in devcontainer.json, e.g.:
      { "settings": { "editor.tokenColorCustomizations": { "comments": "#FF0000" } } }
    • Reopen in the container and verify the preference is applied correctly in the running Theia instance (open Settings and search for the key)
    • Previously, object values were broken by shell quoting when passed as --set-preference CLI arguments
  10. Auto-reopen after restart

    • Connect to a dev container, then close and reopen Theia
    • Verify it automatically reconnects to the same container

Follow-ups

Breaking changes

  • This PR introduces breaking changes and requires careful review. If yes, the breaking changes section in the changelog has been updated.

Attribution

Contributed on behalf of STMicroelectronics and TypeFox

Review checklist

Reminder for reviewers

sgraband and others added 7 commits April 14, 2026 15:12
…d in strings

The variable resolver regex used `^` and `$` anchors (`/^\$\{(.+?)(?::(.+))?\}$/`),
requiring the entire string to be a variable reference. Variables inside
larger strings like `source=${localEnv:HOME}/.gitconfig,target=...` were
never resolved.

Remove the anchors, make the regex global and the inner group non-greedy,
and rewrite `resolveVariable()` to use `String.replace()` with a callback
so variables are resolved wherever they appear in the string.

Contributes to #14294

Co-authored-by: Nina Doschek <ndoschek@eclipsesource.com>
`parseMountString()` only extracted `source`, `target`, and `type`,
silently dropping options like `consistency=cached` and `readonly`.
Additionally, values containing `=` (e.g. from resolved variables) were
truncated by `split('=')[1]`.

Use `substring(indexOf('=') + 1)` instead of `split('=')[1]` to handle
values containing `=`. Extract `readonly`/`ro` into `ReadOnly: true` and
`consistency=<value>` into `BindOptions.Propagation`.

Contributes to #14294

Co-authored-by: Nina Doschek <ndoschek@eclipsesource.com>
`--set-preference=key={"nested":"value"}` was passed through `sh -c` in
the container's exec, where the shell interpreted braces and quotes,
producing invalid JSON.

On the sender side (SettingsContribution.enhanceArgs), object/array
values are now base64-encoded with a `base64:` prefix. Scalar values
(strings, numbers, booleans) pass through unchanged.

On the consumer side (PreferenceCliContribution.setArguments), values
with a `base64:` prefix are decoded before `JSON.parse()`.

Contributes to #14294

Co-authored-by: Nina Doschek <ndoschek@eclipsesource.com>
`portsAttributes` was defined in the devcontainer type schema but
`ForwardPortsContribution` only read `forwardPorts` and ignored port
attributes entirely.

Extend `ForwardedPort` interface with optional `label`, `protocol`, and
`onAutoForward` fields. `ForwardPortsContribution.handlePostConnect()`
now merges matching `portsAttributes` into forwarded port objects via a
new `getPortAttributes()` method that supports exact port match and
range patterns (e.g. `"8000-9000"`).

Contributes to #14294

Co-authored-by: Nina Doschek <ndoschek@eclipsesource.com>
Previously only "Reopen in Container" (from a devcontainer.json) was
supported. There was no way to connect to an already-running Docker
container.

Add `RunningContainerInfo` interface, `listRunningContainers()` and
`attachToContainer(containerId)` methods to the RPC interface. The
backend calls `docker.listContainers()` and creates a
`RemoteDockerContainerConnection` with `RemoteSetupService.setup()`.

On the frontend, a new `ATTACH_TO_CONTAINER` command shows a QuickPick
of running containers (name, image, status), then connects and opens the
remote workspace. Workspace path is inferred from container mounts or
working directory.

Contributes to #14294

Co-authored-by: Nina Doschek <ndoschek@eclipsesource.com>
…and shell detection

Three issues addressed: SSH didn't work in containers, git identity
wasn't carried over, and terminals defaulted to sh.

SSH credentials (Fixes #14926):
- Create an isolated SSH directory at `~/.theia/dev-container/ssh/`
  instead of exposing the real `~/.ssh`
- Copy `known_hosts` and `config` from the host for host verification
- Generate a dedicated ed25519 keypair for container use (printed to
  console for the user to register with their Git provider)
- Detect SSH-based commit signing (`gpg.format = ssh`) and copy the
  signing key into the isolated directory
- Bind-mount the isolated dir read-only; fix permissions in post-create
  (700 dir, 600 private keys, 644 public keys/known_hosts)
- Start a shared ssh-agent via `/etc/profile.d/ssh-agent.sh` so the
  passphrase is only prompted once per container session

Git identity:
- Bind-mount host `~/.gitconfig` to `/tmp/host_gitconfig` (read-only)
- In post-create, copy to the container user's `$HOME/.gitconfig` so
  the container user owns it
- For SSH signing: rewrite `user.signingkey` to point to the
  container's SSH directory instead of the host-absolute path
- For GPG signing: disable (keys aren't available in containers)
- SSH signing is left enabled when the key is available

Terminal shell (Fixes #14293):
- Set `THEIA_SHELL=/bin/bash`, `SHELL=/bin/bash`, and
  `THEIA_SHELL_ARGS=-l` as container env vars at creation time
- The `-l` flag starts bash as a login shell, which sources
  `/etc/profile.d/` (needed for ssh-agent env) and `~/.bashrc`
  (needed for the user's prompt customization)
- Also set `TERM=xterm-256color` and `COLORTERM=truecolor`

Contributes to #14294

Co-authored-by: Nina Doschek <ndoschek@eclipsesource.com>
…start

When Theia restarted with a `devcontainer://` URI as the most recently
used workspace, `WorkspaceService.doInit()` called `toFileStat()` which
failed for non-file schemes, resulting in an empty workspace instead of
reconnecting to the container.

Check for non-`file://` URI schemes before calling `toFileStat()`. When
detected, route through the registered `WorkspaceOpenHandlerContribution`
handlers. The existing `ContainerConnectionContribution` (already
registered as handler) handles `devcontainer://` URIs and triggers the
container reconnection flow.

Fixes #14292
Contributes to #14294

Co-authored-by: Nina Doschek <ndoschek@eclipsesource.com>
@github-project-automation github-project-automation Bot moved this to Waiting on reviewers in PR Backlog Apr 14, 2026
@sgraband sgraband force-pushed the feat/dev-container branch from 1a2d73a to 993502d Compare April 14, 2026 13:30
sgraband and others added 3 commits April 15, 2026 08:59
When opening a workspace that contains a devcontainer.json file, there
was no prompt to the user about the available dev container configuration.

Add `DevContainerSuggestionContribution` (FrontendApplicationContribution)
that on startup checks if already connected to a remote container (skips
if so), waits for the workspace to be ready, then checks for
devcontainer.json files. If found, shows an info notification offering
"Reopen in Container" and "Don't Show Again" actions.

Contributes to #14294

Co-authored-by: Nina Doschek <ndoschek@eclipsesource.com>
Add a command that stops and removes the current dev container, clears
the cached container info, and recreates it from the devcontainer.json.

When inside a remote container, the command reads the stored connection
context (devcontainer file path, host workspace path, container ID)
from localStorage — scanning the filesystem won't work because the RPC
goes to the local backend which doesn't have the container's paths.

The connection context is stored by `doOpenInContainer` at connect time
so it's available for rebuild regardless of session type.

Command visibility:
- "Reopen in Container" is shown only in local sessions
- "Rebuild Container" is shown only in remote sessions

Also fixes workspace path inference: `inferWorkspacePath()` skips
injected mounts (.ssh, .gitconfig) that were being picked as
`Mounts[0]` when `workspaceFolder` was not set in devcontainer.json.

Contributes to #14294, #15121

Co-authored-by: Nina Doschek <ndoschek@eclipsesource.com>
The Ports view widget existed but was inaccessible — no toggle command
was registered. Add `toggleCommandId` to `PortForwardingContribution` so
the view can be opened via the command palette ("Ports: Focus on Ports
View").

Also add a "Label" column to the Ports table and pass through the
`label` field from `ForwardedPort` so that `portsAttributes` labels
from devcontainer.json are displayed.
@sgraband sgraband force-pushed the feat/dev-container branch from 993502d to d5e7233 Compare April 15, 2026 07:37
Copy link
Copy Markdown
Member

@ndoschek ndoschek left a comment

Choose a reason for hiding this comment

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

Thanks a lot for this PR @sgraband!
I tested it and things work well on my end 🎉
I've added a few inline comments, please have a look.

One bit of housekeeping: the commits reference several issues this PR fixes/contributes to, but they're not linked from the PR description yet. Could you add/link them?

On the bigger picture: a natural next step would be to make "open folder in dev container" smoother by having Theia determine the correct workspace folder automatically from the devcontainer metadata (workspaceFolder, mounts...) instead of relying on the user to navigate there. But of course fine as a follow-up.

try {
// openWorkspace reloads the window on success, so if we
// reach the timeout the connection attempt is hanging.
await Promise.race([
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The 15-second timeout may be too short for dev container reconnection. openWorkspace() calls doOpenInContainer()connectToContainer(), which can pull Docker images, create containers, and set up remote connections which can easily exceed 15 seconds on first run or slow networks. When the timeout fires, it falls back to a local workspace while the container creation continues in the background, leading to a confusing state. Consider increasing the timeout or making it configurable.

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.

I feel like increasing the timeout to 2 minutes should be fine. Without configuration (for now). This should only really be relevant, when the user deleted the docker image and then restarts Theia and the dev container is the most recent workspace if i am not missing something. So this does not really happen that often i believe, therefore a setting is not really necessary imho.

const keyPath = path.join(isolatedDir, 'id_ed25519');
if (!await fs.pathExists(keyPath)) {
await new Promise<void>((resolve, reject) => {
cp.exec(`ssh-keygen -t ed25519 -f "${keyPath}" -N "" -C "theia-dev-container"`, err => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ensureIsolatedSshDir() silently generates a new ed25519 keypair at ~/.theia/dev-container/ssh/id_ed25519 if none exists. While it logs a message asking the user to register the public key, auto-generating cryptographic material on the user's machine could be surprising. Consider making this opt-in or using a more prominent notification (e.g., via MessageService).

Comment thread packages/dev-container/src/electron-browser/container-connection-contribution.ts Outdated
createOptions.Env!.push(`${key}=${value}`);
}
};
setIfMissing('THEIA_SHELL', '/bin/bash');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hardcodes /bin/bash as the default shell. Some container images (e.g., Alpine-based) don't include bash. If /bin/bash doesn't exist, terminal sessions will fail to start. Consider detecting the available shell inside the container (e.g., which bash || which sh) in handlePostCreate, or at least falling back to /bin/sh.

Comment thread packages/dev-container/src/electron-node/remote-container-connection-provider.ts Outdated
Comment thread packages/workspace/src/browser/workspace-service.ts
@sgraband sgraband linked an issue Apr 16, 2026 that may be closed by this pull request
3 tasks
- Localize user-facing strings (error messages, progress reports)
- Persist "Don't Show Again" for reopen-in-container suggestion
- Revert ILogger back to console per Theia logging guidelines
- Fix shutdownAction operator precedence and guard missing compose file
- Add DevContainerConfiguration.empty() factory for attach flow
- Preserve remote MRU workspace across in-container saves/unloads
- Increase remote workspace reconnection timeout to 120s
- Add unit tests for cli-enhancing-creation-contributions
@sgraband sgraband requested a review from ndoschek April 22, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Waiting on reviewers

Development

Successfully merging this pull request may close these issues.

Improve handling of dev container workspaces

2 participants