Embercode
✕
VS CodeLesson 7 of 10

50 min read

Dev containers and WSL

Why this matters

A project often works on one computer and fails on another. The code is the same, but the tools around it differ: a different Python version, a missing library, a different operating system. A common phrase for this is "it works on my machine". It describes a real problem, and it wastes hours for beginners and teams alike.

This lesson covers two ways to fix it inside VS Code. A dev container runs the project inside a sealed package that defines its tools. WSL (Windows Subsystem for Linux) runs a Linux system on a Windows computer, so the project uses Linux tools. Both let you open a project in an environment that you can describe in a file and share with others.

What an environment is

An environment is everything a program runs on, apart from the program itself. It includes:

  • the operating system, such as Windows, macOS or a Linux distribution
  • the language runtime, such as the Python or Node.js program that runs your code
  • the libraries, which are ready-made code packages your project imports
  • the tools and settings, such as environment variables, which are named values a program can read

A small example shows how much this matters. Here is a file that prints the Python version and the operating system it runs on:

import platform
import sys
 
print("Python", sys.version.split()[0], "on", platform.system())

Run on the author's laptop, it prints:

Python 3.14.4 on Linux

Run inside a container built from the python:3.12-slim image, the same file prints:

Python 3.12.16 on Linux

The code did not change. Only the environment did. A teammate whose computer has Python 3.12 gets the second result. If the project depends on a feature from one version and not the other, the program may crash for one person and run for another. A dev container removes that difference, because the version is written in a file that everyone uses.

Containers in plain words

A container is a lightweight package that runs a program with its own set of files and tools, separated from the rest of your computer. Containers come from Docker, a program that runs them. Three terms are used throughout this lesson.

  • An image is a saved template. It holds a complete set of files, such as a Linux system with Python 3.12 installed. Images are downloaded once and reused.
  • A container is a running copy of an image. You can start several containers from one image, and stopping one does not change the image.
  • A bind mount connects a folder on your computer to a folder inside the container. Changes made in one place appear in the other at once.

You can try these ideas from a terminal. This command starts a container from the python:3.12-slim image and runs a file inside it. The folder of the project is mounted at /workspace inside the container:

docker run --rm -v "$PWD":/workspace -w /workspace python:3.12-slim python check.py

Each part does one job:

  • docker run starts a new container.
  • --rm deletes the container when the command ends. The image is kept.
  • -v "$PWD":/workspace mounts the current folder at /workspace. $PWD is the shell's name for the current folder.
  • -w /workspace makes /workspace the starting folder inside the container.
  • python:3.12-slim is the image name. The part after the colon is the tag, which names the version.
  • python check.py is the command that runs inside the container.

The output is the one shown earlier, from inside the container:

Python 3.12.16 on Linux

If you see an error that says Docker cannot connect, the Docker program is not running. The next section covers the check.

The dev container file

VS Code reads a dev container from a folder called .devcontainer at the top of the project. Inside that folder is a file called devcontainer.json. The file is plain text in JSON format, which is a common way to write structured settings. JSON uses curly braces for groups of settings, square brackets for lists, double quotes around names and text, and commas between items.

Here is a small example for a Python project:

{
  "name": "ForgeStack Python",
  "image": "python:3.12-slim",
  "postCreateCommand": "python3 --version",
  "customizations": {
    "vscode": {
      "extensions": ["ms-python.python"]
    }
  }
}

Each setting does one thing:

  • name is a label that VS Code shows. It can be any text.
  • image names the image the container starts from. This is the setting that fixes the Python version.
  • postCreateCommand runs one command after the container is created. Here it prints the Python version, so you can see it in the log.
  • customizations.vscode.extensions lists VS Code extensions to install inside the container. ms-python.python is the Python extension.

Save the file as .devcontainer/devcontainer.json, so its path from the project's top folder is exactly that. Then commit the .devcontainer folder to Git, as covered in the Git module. A teammate who clones the project gets the same file, and so gets the same environment.

Checking the file before you use it

A JSON file with a small mistake will not open. You can check the file from the terminal with Node.js, a program that runs JavaScript. Node has a function that reads JSON and reports the first error it finds. This command reads the file and prints the name and the image:

node -e 'const fs = require("fs"); const o = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); console.log("ok:", o.name, o.image)' .devcontainer/devcontainer.json

The output for the file above is:

ok: ForgeStack Python python:3.12-slim

Now look at a broken version of the same file, with a comma after the last item in a group. JSON does not allow that comma:

{
  "name": "Broken",
  "image": "python:3.12-slim",
}

The same check reports:

SyntaxError: Expected double-quoted property name in JSON at position 53 (line 4 column 1)

The line number tells you where to look. Line 4 is the closing brace, and the comma before it is the problem. VS Code also draws a red underline under this kind of error when the file is open.

VS Code accepts comments in this file, but the check above does not. Keep the file as plain JSON with no comments, so that every tool you use can read it.

Reopening a project in a container

Before VS Code can use a dev container, your computer needs two things.

  1. Docker running. Check it from the terminal. This command asks Docker for its version and its operating system:

    docker info --format '{{.ServerVersion}} {{.OperatingSystem}}'

    On the machine used to write this lesson, it printed:

    29.1.3 Ubuntu 26.04.1 LTS

    Your version and system name will differ. What matters is that the command prints a version. If it prints a connection error, Docker is not running. On Windows and macOS, start Docker Desktop and wait until it says it is running.

  2. The Dev Containers extension. It has the identifier ms-vscode-remote.remote-containers. Search for "Dev Containers" in the Extensions view, which is Ctrl+Shift+X, and install the one from Microsoft.

With both in place, open the project folder, the one that contains .devcontainer. VS Code usually shows a notice with a Reopen in Container button. You can also press Ctrl+Shift+P to open the command palette and type Dev Containers: Reopen in Container. Choose it.

VS Code then does the following:

  1. It reads devcontainer.json.
  2. It downloads the image if it is not already on your computer. The first time can take several minutes.
  3. It starts a container from the image and mounts your project folder into it.
  4. It installs the extensions listed in the file.
  5. It runs the postCreateCommand.
  6. It reconnects the window to the container.

After this, the terminal in VS Code runs inside the container, and so do the Python or Node programs you start from the editor. The bottom-left corner of the window shows that the window is connected to a dev container. That corner is the quickest way to tell which environment the window uses. To leave, use the same corner's menu or the command palette, and choose the option to reopen the folder locally.

Editing a file in the window edits the file in your project folder, because of the bind mount. You can close the window and reopen it later, and the container starts again from the same image.

Worked example 1: the check file on two environments

This is the example from the start of the lesson, now run by hand. It shows the environment is different, not the code.

Put the check.py file in a folder. Run it on your computer:

python3 check.py

On the machine used for this lesson the output was:

Python 3.14.4 on Linux

Now run it through the container command from earlier:

docker run --rm -v "$PWD":/workspace -w /workspace python:3.12-slim python check.py

The output is:

Python 3.12.16 on Linux

Two lines of output show that the same file ran on two different Python versions. The version came from the image name, not from anything you typed inside the file. Changing the image name to a different version changes the result without touching the code.

Worked example 2: a Node.js project in a container

The next example is a project that needs Node.js 22, not Python. Node.js runs JavaScript outside the browser. Its dev container file names an image that has Node 22:

{
  "name": "ForgeStack Node",
  "image": "node:22-slim",
  "postCreateCommand": "node --version",
  "customizations": {
    "vscode": {
      "extensions": ["dbaeumer.vscode-eslint"]
    }
  }
}

The extension in this file is ESLint, the linter introduced earlier in the course. The file is different from the Python one in three places: the image, the command, and the extension. The structure is the same.

Check the file with the same command as before, run from the project folder. The output is:

ok: ForgeStack Node node:22-slim

To confirm the image gives you the version you expect, run the version command in a container:

docker run --rm node:22-slim node --version

The output is:

v22.23.3

The version numbers in these outputs are from the time of writing. Image owners can update an image, so the patch number after 22. may change. The major version, 22 here, is what the file asks for.

The postCreateCommand in this file prints the same version when the container is created, so a teammate sees it in the log without running anything by hand.

Worked example 3: who owns the files the container writes

The third example is a case that catches many people. A container can create files inside the project folder, because of the bind mount. The owner of those files depends on the user that ran the command inside the container.

This command writes a file from inside a container:

docker run --rm -v "$PWD":/workspace -w /workspace python:3.12-slim sh -c 'echo made-in-container > from-container.txt'

Then list the folder on your computer:

ls -l

The output in one run was:

-rw-r--r-- 1 chiranjeet chiranjeet  93 Nov  5 10:31 check.py
-rw-r--r-- 1 root       root        18 Nov  5 10:31 from-container.txt

The first file belongs to the user chiranjeet, which is the user on the host computer. The second file belongs to root. The image runs commands as root by default. root is the Linux administrator account. Inside the container, root can do anything, so the file is created with root as its owner. Back on the host, the file is owned by root, and your normal user may not be able to edit it without changing the owner or using sudo, which runs one command with administrator rights.

The fix is to run the container as a normal user. A dev container file can set this with the remoteUser setting, which names the user that VS Code works as:

"remoteUser": "vscode"

The name vscode is only an example. Many images define a normal user under a different name, so check the image's documentation before you set it. If the name is wrong, VS Code reports that the user does not exist.

Reopening a project in WSL

WSL is a feature of Windows that runs a Linux system beside Windows. Most of the commands in this course, such as ls, grep and bash scripts, are written for Linux. On Windows, WSL lets you run them without a separate computer or a virtual machine that you manage yourself.

You can see whether a terminal runs inside WSL by checking the system. Inside a WSL terminal, the kernel, which is the core of the operating system, has the word microsoft in its name. This command prints the first part of the kernel description:

cut -c1-70 /proc/version

On the machine used for this lesson, the output was:

Linux version 6.18.33.2-microsoft-standard-WSL2 (root@f1bbfb02316b) (g

The word WSL2 at the end means version 2 of WSL, which runs a real Linux kernel. Version 1 of WSL used a translation layer instead, and it is no longer the default.

This command shows the Linux distribution, which is the packaged Linux system you installed:

grep -E '^(NAME|VERSION)=' /etc/os-release

The output was:

NAME="Ubuntu"
VERSION="26.04.1 LTS (Resolute Raccoon)"

To see every WSL distribution on the computer, run this from the WSL terminal. Windows programs can be called from WSL, and wsl.exe is the Windows program that manages WSL:

wsl.exe --list --verbose

The output was:

  NAME              STATE           VERSION
* Ubuntu            Running         2
  docker-desktop    Stopped         2

The asterisk marks the distribution this terminal runs in. The STATE column says whether it is running. Your list may have different names.

Where your files should live

A WSL system has two places to store files that matter for this lesson.

  • Your Linux home folder, such as /home/yourname. Files here belong to the Linux system. The shortcut ~ means this folder.
  • The Windows drives, which WSL shows under /mnt. The C drive is /mnt/c, and a folder such as Documents on it is /mnt/c/Users/yourname/Documents.

On this machine, ls /mnt printed:

c
d
e
wsl
wslg

Each letter is a Windows drive. The wsl and wslg folders belong to WSL itself. Files on those drives can be used from WSL, but they are slower to read and write from Linux tools than files in the Linux home folder. For a project that you run and test often, keep it in the Linux home folder. Keep Windows files for things that Windows programs need.

Opening a folder in WSL

There are two common routes.

The first route starts from the WSL terminal. Move into the project folder inside WSL, then run:

cd ~/projects/forgestack
code .

The code command starts VS Code and opens the current folder. The window edits the files inside WSL, and the corner of the window shows the name of the distribution. The first time, VS Code installs a small helper inside WSL, which takes a little time.

On this machine, command -v code printed /mnt/c/Users/.../Microsoft VS Code/bin/code. That means the code command inside WSL is the Windows copy of VS Code, reached through /mnt/c. It works, but if you install VS Code twice, check which copy the command finds.

The second route starts from VS Code on Windows. Install the WSL extension, which has the identifier ms-vscode-remote.remote-wsl. Open the command palette with Ctrl+Shift+P and choose the command that reopens the folder in WSL, named WSL: Reopen Folder in WSL. VS Code then restarts the window in WSL.

You can check which environment a window uses by reading the bottom-left corner. It shows the distribution name when the window is connected to WSL.

Why the environment should match on every machine

A dev container or a WSL setup is only useful if the environment is written down and shared. The reasons are practical.

Say three people work on the same project. One uses a Mac with Python 3.14, one uses Windows with Python 3.12, and one uses Linux with Python 3.13. Each person runs the tests and gets a different result for the same code. A bug report from one person is hard to reproduce for the others. Each person spends time asking which version the project needs, and nobody can answer for certain.

When the project includes a .devcontainer folder, the answer is in the repository. Every person opens the same image, so every person runs Python 3.12. A bug reproduced by one person can be reproduced by the others, because the environment is the same.

Two limits matter. A container does not fix bugs in your code, so the tests can still fail, and the failure will then be the same for everyone. A container also does not remove the need for Docker. Docker is the program that runs the container, and it must be installed and running on each computer. WSL is also per computer: it runs on Windows only, so a Mac user uses the dev container file without WSL.

Common mistakes

Docker is not running

If Docker is not running, the command line and VS Code both fail to start the container. The Docker command prints a message like this when it cannot reach its background service, which is called the daemon:

failed to connect to the docker API at unix:///nonexistent/docker.sock; check if the path is correct and if the daemon is running: dial unix /nonexistent/docker.sock: connect: no such file or directory

This text came from a socket path that was set on purpose for this example, so the wording on your machine may differ. In every case the fix is to start Docker and try again. Check with docker info before you open the folder.

A trailing comma in the file

A comma after the last item in a group is allowed in many programming languages but not in JSON. It is one of the most common reasons a dev container fails to open. Use the check from earlier in this lesson to find the line, then delete the comma.

The .devcontainer folder is not in Git

If you create the file on your computer and do not commit it, your teammates never receive it. They open the project and get no container, or a different one from their own settings. Check the status with git status after you create the folder and add it with git add .devcontainer.

Files in the container belong to root

This is the problem from worked example 3. It shows up later, when you try to edit or delete a file that the container created. Set remoteUser to a normal user, or change the file owner with sudo chown. The chown command changes the owner, and it needs sudo because it touches files that belong to root.

A WSL project on a Windows drive

A project kept under /mnt/c works, but it is slower for the tools that read many files, such as test runners and package installers. Move the project into the Linux home folder with a command such as mv, then open it from there. The mv command moves a folder, and it runs inside WSL, so use the paths shown in the WSL section.

Opening the wrong environment

A VS Code window can be on Windows, editing Windows files, while its terminal runs in WSL. The two worlds use different paths and different tools, so a command that works in one can fail in the other. Check the bottom-left corner of the window before you run a command, and check the path with pwd in the terminal.

The extension is missing

If the Reopen in Container notice does not appear and the command palette has no Dev Containers commands, the Dev Containers extension is not installed. Install it from the Extensions view, then reload the window.

Practice

Exercise 1. Your teammate sends a devcontainer.json file. VS Code will not open the container, and the check command reports a line-number error. The file has this content:

{
  "name": "Test",
  "image": "python:3.12-slim",
  "postCreateCommand": "python3 --version"
  "customizations": {}
}

What is wrong, and how do you fix it?

Show the answer

The line "postCreateCommand": "python3 --version" has no comma after it, but another item follows. JSON needs a comma between items in the same group. Add a comma at the end of that line, so it reads "postCreateCommand": "python3 --version",.

Exercise 2. Using the Python file from worked example 1, you want the project to run on Python 3.13 in a container. What do you change, and what do you leave alone?

Show the answer

Change the image name in devcontainer.json from python:3.12-slim to a 3.13 image, such as python:3.13-slim. Leave the Python code unchanged. The check command will then print a 3.13 version, which proves the change. The exact patch number depends on the image you pull.

Exercise 3. You run a command in a container that creates report.txt in the project folder. On your computer, you cannot edit report.txt. What is the likely cause, and what are two ways to fix it?

Show the answer

The container ran as root, so the file belongs to root on your computer. Two fixes are: set remoteUser in devcontainer.json to a normal user name that the image defines, and change the owner of the existing file with sudo chown. The first prevents the problem for new files, and the second fixes only the file that already exists.

Exercise 4. You are starting a new project that you will run and test often on Windows with WSL. Where should the project folder be, and why?

Show the answer

Put it in the Linux home folder, such as ~/projects/newproject. Tools that read many files run faster from the Linux file system than through /mnt/c, and the project then uses Linux file permissions as its tools expect. Keep the Windows drive for files that Windows programs need.

Exercise 5. Write a devcontainer.json file for a project that needs Node.js 22 and runs node --version after the container is created. Check that the file parses with the Node command from this lesson.

Show the answer

A correct file is:

{
  "name": "Node project",
  "image": "node:22-slim",
  "postCreateCommand": "node --version"
}

Run the check command with the path to the file. It should print ok: followed by the name and the image. If it prints a SyntaxError, look at the line number it gives and check for a missing or extra comma.

Summary

  • An environment is the operating system, the language runtime, the libraries and the settings a project runs on. Different environments can give different results for the same code.
  • A dev container is defined by .devcontainer/devcontainer.json. Its image setting fixes the tools, and committing the folder shares the setup with every teammate.
  • Reopen a project in a container with the Dev Containers extension and Docker running. Check Docker with docker info before you try.
  • WSL runs a Linux system on Windows. Keep projects you run often in the Linux home folder, not under /mnt/c.
  • Files a container writes can belong to root, which blocks editing on your computer. Set remoteUser to a normal user.
Check yourself: Environments (8 questions) →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← The Source Control panel