Using VS Code Remote Containers without Docker Desktop

Docker Desktop's licensing or footprint may rule it out, but the Dev Containers extension doesn't require it — it needs a Docker-compatible engine. This page points the extension at a rootless Docker Engine, Podman, or Colima, so you get the full devcontainer experience without Docker Desktop.

The reason this works at all is that the extension never talks to Docker Desktop directly. It shells out to a Docker-compatible CLI and speaks the Docker Engine API over a Unix socket. Desktop is simply one packaging of that CLI plus a VM plus a GUI. Once you understand that the extension only cares about two things — a binary it can invoke as docker (or a substitute you name) and a socket that answers the Engine API — the whole task reduces to installing an engine that provides both and telling VS Code where to find them. Everything else in your devcontainer.json, from features to lifecycle hooks to forwarded ports, is engine-agnostic and stays untouched.

You reach for this when Desktop's per-seat licensing is a budget or compliance problem, when its background VM is too heavy for an older laptop, or when your organisation has standardised on Podman for its daemonless, rootless security model. The mental model to hold is a three-link chain: an engine that runs containers, a socket that exposes its API, and a VS Code setting that names the CLI. Break any link and the extension reports that it cannot find Docker; keep all three consistent and the reopen-in-container flow behaves exactly as documented, because the extension cannot tell the difference.

Prerequisites

You need a Docker-compatible engine installed and its socket reachable by VS Code.

  • A rootless Docker Engine, Podman, or Colima installed.
  • The engine's socket path known (e.g. the rootless or Podman socket).
  • The Dev Containers extension installed in VS Code.

Engine prerequisitesYou need an alternative engine, its socket, and VS Code pointed at it.Enginepodman / colima /rootlessSocketpath knowndockerPathpoint VS CodeAttachreopen in container

The detail people most often get wrong is treating the socket path as a constant. It is not: a rootless Docker Engine listens under $XDG_RUNTIME_DIR/docker.sock, Podman exposes its API through a socket you often have to enable with systemctl --user start podman.socket, and Colima writes to ~/.colima/default/docker.sock. Copying a DOCKER_HOST value from a blog post that assumed a different engine is the fastest way to a "cannot connect to the Docker daemon" error. Confirm your own path with docker context inspect or by listing the runtime directory before you wire anything into VS Code.

It is also worth confirming the CLI is genuinely on the PATH that VS Code sees, which is not always the login shell's PATH. The Dev Containers extension launches its Docker calls from the environment VS Code inherited when it started, so a docker or podman binary installed only in a shell-specific location may be invisible until you either restart VS Code from that shell or set the path explicitly. Verifying which podman and docker info from a terminal before touching settings saves a round of confusing failures.

Step-by-Step Implementation

  1. Install an alternative engine (example: Colima on macOS, or Podman).
# macOS
brew install colima docker && colima start
# or Podman
podman machine init && podman machine start

The colima start line does two jobs at once: brew install colima docker gives you the Colima VM manager and the vanilla Docker CLI (without Desktop's proprietary bits), and colima start boots a lightweight Linux VM that runs the actual Docker daemon and publishes a socket back to the host. On Linux the podman machine commands are equivalent scaffolding — init provisions the backing VM and start brings up the Podman API service. Installing the plain docker CLI here matters because the extension needs a client binary to invoke; the engine alone is not enough, and skipping the CLI is why a bare colima start sometimes leaves the extension complaining it cannot find Docker.

  1. Point VS Code at the engine via the Dev Containers docker path/socket setting.
{
  "dev.containers.dockerPath": "podman",
  "dev.containers.dockerComposePath": "podman-compose"
}

These two settings are the substitution the extension performs on every operation. dev.containers.dockerPath overrides the binary it calls in place of docker, so setting it to podman means every build, inspect, and exec routes through Podman instead. dev.containers.dockerComposePath does the same for multi-container projects, because the extension shells out to a separate compose binary when your devcontainer.json references a compose file. If you set only the first and leave compose pointing at the default, single-container projects build fine while compose-based ones fail with a missing-binary error — a confusing split that traces directly to setting one path but not the other. For a plain Colima setup you can often leave both at their defaults, since Colima ships the standard docker CLI; the overrides matter most for Podman.

  1. Set the Docker host if the socket isn't the default location.
export DOCKER_HOST="unix://$HOME/.colima/default/docker.sock"

The unix:// scheme tells the Docker client to connect to a local socket file rather than a TCP endpoint, and the path that follows is where Colima placed its socket. You need this line only when the socket is not at the conventional /var/run/docker.sock, which is precisely the situation with a rootless or per-user engine. Exporting DOCKER_HOST in the shell that launches VS Code, or setting it in the VS Code integrated terminal profile, ensures both the CLI and the extension resolve to the same daemon. The classic failure this prevents is a mismatch where your terminal's docker works because it found the socket, but the extension — started from a different environment — falls back to the default path, finds nothing listening, and errors out.

  1. Reopen in container and confirm it builds against the alternative engine.
docker context show && docker info | grep -i 'server version'

This verification pair confirms the two things that must be true before a reopen will succeed. docker context show prints the active context, letting you see whether the CLI is aimed at Colima or Podman rather than a stale Desktop context left over from a previous install. docker info | grep -i 'server version' then proves the client actually reached a running daemon: a server version only appears when the socket answered, so an empty result means the connection failed and you should revisit DOCKER_HOST before wasting a build. Running this from the VS Code integrated terminal, rather than a separate window, checks the exact environment the extension will inherit, which is the whole point — if docker info reports a server here, the reopen-in-container command will find the same engine.

Engine wiring layersAn alternative engine, its socket, and the VS Code docker-path setting wire the extension without Desktop.Alternative enginerootless Docker / Podman / ColimaSocket / DOCKER_HOSTwhere the engine listensdev.containers.dockerPathVS Code CLI to useAttachextension builds via that engine

Common Pitfalls

Failures come from a wrong socket path, an unset docker path, or rootless permission quirks.

The permission angle is the subtlest because rootless engines deliberately remap user IDs. When a rootless daemon builds and runs a container, the "root" inside the container is your unprivileged host user viewed through a user-namespace mapping, so files a container writes to a bind-mounted workspace land on the host owned by a high, shifted UID rather than your own account. This surfaces as files you cannot edit or delete after the container exits, or as a build that fails with permission denied when it tries to write into a mounted cache. The fix is to keep the socket owned by the same user that runs VS Code and to let the rootless containers.conf idmap settings, or a remoteUser in devcontainer.json, line the container UID up with your host UID rather than fighting the mapping after the fact.

The other topic-specific trap is port forwarding. Rootless engines cannot bind privileged ports below 1024 without extra capability grants, so a devcontainer that maps 80 or 443 silently fails to forward even though the container itself started cleanly. VS Code's forwarded-ports panel will show the port as active while nothing actually answers on the host, which makes the problem look like an application bug rather than an engine limitation. Map your services to ports above 1024 — 8080 instead of 80 is the usual move — or, if a low port is unavoidable, raise the rootless port range via net.ipv4.ip_unprivileged_port_start so the engine is permitted to publish it.

Engine triageA triage path from an unreachable engine to a working Desktop-free setup.Does docker info reach the engine?NOFix DOCKER_HOST / socketIs dev.containers.dockerPath set?NOPoint it at podman/dockerExtension builds without Desktop

SymptomRoot CauseRemediation
Extension can't find DockerdockerPath/socket not setSet dev.containers.dockerPath + DOCKER_HOST
Build fails with permission deniedRootless socket permsUse the rootless socket + correct user
Compose commands failCompose path not set for engineSet dev.containers.dockerComposePath
Ports not forwardingRootless port mapping limitsUse ports >1024 or configure rootless port range

Conclusion

The extension only needs a Docker-compatible engine, not Docker Desktop. Install a rootless Docker Engine, Podman, or Colima, tell VS Code the docker path and socket, and reopen in container — the experience is identical, only the engine underneath changes. Rootless setups just need attention to socket permissions and port ranges.

The strategic payoff is that decoupling the extension from Desktop makes your development environment more portable, not less. Because the wiring lives in a handful of named settings — dev.containers.dockerPath, dev.containers.dockerComposePath, and DOCKER_HOST — a teammate on Podman and a teammate on Colima can share the identical devcontainer.json and each override only the engine binding on their own machine. The reproducible part of the setup, the image and the features it pins, stays engine-neutral, which is the same pin-and-cache discipline that keeps builds deterministic elsewhere: pin what the container is, and treat the engine that runs it as swappable infrastructure.

That separation also future-proofs the project against licensing and tooling churn. If a Desktop licence lapses, an engine gets deprecated, or a security team mandates a rootless-only policy, you change the binding, not the container definition, and the CI pipeline that builds the same image needs no edits at all. Keeping the engine choice at the edge of the configuration — a per-developer detail rather than a committed dependency — is what lets the devcontainer stay the single source of truth for what the environment contains while the question of who runs it stays flexible.

Engines and wiringAny Docker-compatible engine works; three settings point VS Code at it.Engine optionsRootless DockerPodmanColimaPoint VS Code withdockerPathDOCKER_HOSTcomposePath

FAQ

Does the Dev Containers extension require Docker Desktop? No. It requires a Docker-compatible engine and CLI, which Docker Desktop provides but so do rootless Docker Engine, Podman, and Colima. Point the extension at your chosen engine via dev.containers.dockerPath and the Docker host, and it builds and attaches exactly as it would with Desktop. The extension has no dependency on Desktop's GUI or its background helper process; it communicates purely over the Engine API through whatever socket you name. If a page or plugin claims Desktop is mandatory, it is conflating the most common packaging with an actual requirement.

What's different about a rootless engine? Rootless engines run without root privileges, which is more secure but adds two wrinkles: the socket lives at a non-default path (set DOCKER_HOST), and low ports (<1024) can't be mapped without extra configuration. Use higher ports or configure the rootless port range, and set the socket path explicitly. The other consequence is user-namespace ID mapping, which shifts the UIDs of files a container writes back to the host; align remoteUser with your host account to avoid ending up with workspace files you cannot edit. None of this changes how you author devcontainer.json — it only changes the runtime wiring around it.

Can I use Podman as a drop-in for Docker here? Largely yes — set dev.containers.dockerPath to podman and dev.containers.dockerComposePath to podman-compose. Most devcontainer configs work unchanged. Watch for rootless permission and networking differences, which are the usual sources of friction when switching. Podman's daemonless model also means there is no long-running background service to restart; the API service is started on demand, so if the extension cannot connect, check that podman.socket is enabled for your user rather than looking for a daemon to bounce. Compose support depends on having podman-compose or Podman's compose provider installed, which is a separate package on most distributions.

How do I confirm which engine a build actually used? Run docker context show to see the active context and docker info to read the reported server version and storage driver from inside the same terminal VS Code inherits. A Colima server will identify differently from a Podman one, so the output tells you unambiguously which engine answered. If the values do not match the engine you intended, a stale context or an unexpected DOCKER_HOST in your shell profile is usually to blame.

Do I need to change devcontainer.json when switching off Docker Desktop? Generally no. The image, features, mounts, and lifecycle hooks are engine-neutral and stay as they are; the engine choice lives entirely in VS Code settings and the DOCKER_HOST environment variable. The exception is behaviour that leaned on a Desktop-specific convenience, such as an assumed /var/run/docker.sock bind for docker-in-docker, which you may need to repoint at your engine's real socket path.

Why does the extension say it cannot find Docker even though my terminal works? Because VS Code launches its Docker calls from the environment it started with, not from your interactive shell. If you set DOCKER_HOST in a shell profile after VS Code was already open, the running window never saw it. Restart VS Code from a shell where docker info succeeds, or set the path in the VS Code settings and terminal profile so both the extension and the integrated terminal resolve the same socket.