# Optimizing Your Coding Environment: A Beginner's Guide to Dev Containers in VSCode

Containers have become a popular method for packaging and deploying applications, offering a consistent and reproducible environment that can be easily moved from development to production. However, while containers are great for deployment, setting up a development environment that mirrors the production environment can be challenging.

# Prerequisites

Before diving into Dev Containers in VS Code, it's recommended to have a basic understanding of Docker. If you're new to Docker, I would recommend going through this [Docker Tutorial for Beginners](https://hashnode.com/post/docker-tutorial-for-beginners-cjrj2hg5001s2ufs1nker9he2) by @[Liron Navon](@liron_navon).

# Using Docker for Development

You can also use docker containers for development by bind mounting the source directory and simply using the `docker run` command.

Here's an example of the `Dockerfile` required for development:

%[https://gist.github.com/omgupta15/4f2b47d527028726987a4d74937c355d] 

(`--watch` option restarts the server every time the file changes.)

Note that you need to use `node --watch app.js` as the `CMD` instruction.

After creating the `Dockerfile`, we build an image and mount the directory containing the source code into the container. Mounting the source directory makes sure that the updated files are not lost after the container is shut down.

```bash
docker build -t my-app-image .
docker run -it --rm --mount type=bind,source=$(pwd),target=/home/node/server -p 8080:8000 my-app-image
```

### Limitations of this Approach

* Although running code in a container can ensure that it works correctly, it's possible that the local environment may not have all the necessary language or module installations required for IDE functionality, which can lead to issues such as missing autocomplete or incorrect error & warning displays in VS Code.
    
* To run shell commands within a container, you'll need to use a somewhat lengthier syntax, such as `docker exec -it <container-name> <command>`.
    
* Each developer's unique configuration may result in inconsistent functionality or unexpected errors. It's essential to establish a shared standard for VS Code settings to ensure that everyone is working with the same tools and parameters.
    

This is where Dev Containers come in. Dev Containers allow developers to create a fully isolated development environment that mirrors the production environment, complete with all the necessary tools and dependencies.

# What are Dev Containers?

Dev Containers let you use a container as a full-fledged development environment in VS Code. These enable you to use Visual Studio Code's full feature set by mounting any folder inside a container.

Dev containers allow you to configure your project's editor when someone opens it, making it easier to ensure consistency across different machines. You can adjust settings, install extensions, define debugging, and manage the container environment to match your specific needs.

# Getting Started

### Installing the Dev Containers extension

Go to the Extensions Marketplace. Search for `Dev Containers` extension and install it.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681417054862/7083cf22-5d15-4bb2-9e49-5b98e063d277.png align="center")

You can also install it by using the following link:

%[https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers] 

# Dev Container Setup

After the installation is complete, you can see the following options by hitting `⌘ + ⇧ + P` on Mac or `Ctrl + Shift + P` on Windows and searching for `dev containers`:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681417850882/9ee3f938-8f16-4586-b61f-66aa6f7122c3.png align="center")

You can either create a new container, add dev container configuration files to the current project, attach to a container that is running already or create a new container from a GitHub Repository.

We'll use the `Add Dev Container Configuration Files...` option to add dev container to our current project.

Select the `From 'Dockerfile'` option to use your existing configuration:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681418235439/94e7ae03-68cc-4fef-a3a6-40ec1bc58ade.png align="center")

This creates a `.devcontainer/devcontainer.json` file in your project directory.

Now click the Remote Window icon in the bottom left corner of the VS Code window and select "Reopen in Container".

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681447640437/814dfeba-03af-47ce-9f32-4ab2fd3b86f8.png align="center")

VS Code will build the container image (if necessary) and start the container. Once the container is running, your workspace will be configured to use it as the development environment.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681461819301/f6d3f4fb-9da9-447d-9798-132eebfa7254.png align="center")

When you execute commands in the integrated terminal, they will be executed within the container. To check the name of the operating system used by the container, you can run the command `cat /etc/issue.net` in the integrated terminal.

# Environment Setup

Once the environment loads, you will find that your extensions and settings (except the global ones) are not available inside the container.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681462413577/334ff7ec-d811-40c8-ac28-5984c2ffad80.png align="center")

## VS Code Extensions & Settings

You can set up the extensions and settings automatically in your new workspace by adding the `customizations` field to the `devcontainer.json` file.

### Settings

You can specify settings using the `settings` field inside the `customizations > vscode` field:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681465098904/c85d64ca-7574-428e-b357-7cc96a622ace.png align="center")

```json
"customizations": {
  "vscode": {
    "settings": {
      "workbench.colorTheme": "Default Dark+",
      "editor.formatOnSave": true,
      "editor.tabSize": 2
    }
  }
}
```

These settings will override the default settings of VS Code for the workspace and will be automatically applied when the dev container starts.

Options like `editor.formatOnSave` and `editor.tabSize` can be used to maintain consistency in the codebase when several collaborators are working on the project.

### Extensions

To set up extensions in your dev container, you need to use the `extensions` property. This property takes an array of one or more extension identifiers that you want to install in your dev container. These identifiers can be found in the extension's marketplace URL or in the extension's `publisher.extensionName` format.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681467218443/4b7e26d4-759b-480a-9246-a3822baef367.png align="center")

Here's an example of `customizations` with the `extensions` property:

```json
"customizations": {
  "vscode": {
    "settings": {...},
    "extensions": [
      "github.copilot",
      "ms-azuretools.vscode-docker",
      "redhat.vscode-yaml"
    ]
  }
}
```

After restarting the container, the extensions specified in the `devcontainer.json` file will be automatically installed:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681468308889/b65e39e4-b157-4b6d-8889-1ecc661ebb87.png align="center")

In `docker run`, we used the `--publish` or `-p` flag to publish a container's ports to the host system. How do we achieve a similar result in a dev container?

## Port Forwarding

For mapping the ports from a container to the host system, `appPort` property is used.

The `appPort` property specifies the port number(s) on which the application(s) running inside the Dev Container is/are listening.

Here's an example of the mapping:

```json
"appPort": [
  "3000",
  "8080:8000"
]
```

`"3000"` specifies both the container and host ports as `3000`.

`"8080:8000"` is specified in the format `"<host-port>:<container-port>"`. If your application is running on port `8000` inside the container, it will be available at `http://localhost:8080` on your host system.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681470138787/03e60487-9125-411e-9124-afbfdbefea1b.png align="center")

## Post Create Command

The `postCreateCommand` field allows us to specify a command as a string to be executed after the container has been successfully built and started. This can be particularly useful in situations where we need to run a server or a script during the development process.

```json
"postCreateCommand": "node --watch app.js"
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681474171760/fea0af50-213b-4bc5-bc6b-c56f5d26a17b.png align="center")

In this example, this command auto-restarts the `app.js` file every time it changes.

## Connecting as a non-root user

Connecting as a root user to a dev container can be a security risk and is generally not recommended. Due to this reason, official `node` docker images usually have a user `node` with fewer privileges.

You can connect as `node` user by using the `remoteUser` property of `devcontainer.json`.

```json
"remoteUser": "node"
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681475193692/e4d87c90-6879-417f-8add-52b9ada94127.png align="center")

> ### Custom Users
> 
> You can only connect as a user which already exists in the container. To use a custom username, create a user by adding the following instruction in the `Dockerfile` :
> 
> ```apache
> RUN adduser newuser
> ```
> 
> ```json
> "remoteUser": "newuser"
> ```
> 
> ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681475922603/2db7ec53-e73e-43ef-adb0-07c4976f3cfe.png align="center")

Here are the final versions of `Dockerfile` and `devcontainer.json`:

%[https://gist.github.com/omgupta15/2b78d38480cb8e1adff84ee8640a9be1] 

%[https://gist.github.com/omgupta15/46bfddaea31a9e29ec877e882f8f3008] 

We can use the same `Dockerfile` for both environments since the copied files are ignored when we mount our current directory as a volume. However, for better optimization and reduced data usage, you may want to consider creating separate Dockerfiles for each environment.

# Git inside Dev Containers

The Dev Containers extension provides support for using your local Git credentials within a container.

> If you get an error stating that git is not installed, you can install it by adding the following instruction into the `Dockerfile`:
> 
> ```apache
> RUN apt-get update && apt-get install git -y
> ```

It lets you use git commands inside the container exactly like on your local machine.

## GPG Signing

If you use GPG to sign your commits, you can also use your local keys inside the containers.

> If you get an error stating that `gpg` is not installed, you can install it by adding the following instruction into the `Dockerfile`:
> 
> ```apache
> RUN apt-get update && apt-get install gnupg2 -y
> ```

Try generating a signature, using `echo "test" | gpg --clearsign`.

If it doesn't work, you probably need to install [GPG Tools](https://gpgtools.org/) on Mac or [Gpg4win](https://www.gpg4win.org/) on Windows. You can find the full guide here: [https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials#\_sharing-gpg-keys](https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials#_sharing-gpg-keys)

After proper setup, it should be able to generate a signature.

> **Note:** You need to be using the container as the user `node` for `gpg` to work in a Node.js dev container. Make sure to include the following instruction in your `Dockerfile`:
> 
> ```apache
> USER node
> ```

If you still get an error, try installing `pinentry` via [Homebrew](https://brew.sh/):

```bash
brew install pinentry
which pinentry-mac

# Now, add the path given by `which` command to the `~/.gnupg/gpg-agent.conf` file
vi ~/.gnupg/gpg-agent.conf
# Press `i` to go into insert mode.
pinentry-program /opt/homebrew/bin/pinentry-mac # path given by `which` command

# Press `esc` key to go into command mode. Type the following command:
:wq # and press enter to save & quit.

# Restart gpg-agent
gpgconf --kill gpg-agent
```

Now run `echo "test" | gpg --clearsign` inside your container.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681504681766/f6361ec9-ccb9-48e5-8157-e87d0bf6cfa0.png align="center")

# Wrapping Up

That's it! By now, you should have a good understanding of what Dev Containers are, how they work, and how they can be used to streamline your development workflow. We covered a variety of topics, including how to create a dev container, how to use extensions, and how to set up port forwarding. We also highlighted best practices for using dev containers, such as avoiding running as the root user.

With Dev Containers, you can create a consistent and reproducible development environment that can be easily shared with your team members. This can save time, reduce errors, and increase productivity. So why not give Dev Containers a try today and see how they can benefit your development workflow?

Thanks for reading all the way to the end. I hope you found it informative and enjoyable.

If you find any mistakes or something missing in this article, have any doubts or suggestions, or just want to say hi, reach out to me on [Twitter (@omgupta15\_)](https://twitter.com/omgupta15_).
