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 by 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:
(--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.
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.
You can also install it by using the following link:
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
:
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:
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".
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.
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.
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:
"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.
Here's an example of customizations
with the extensions
property:
"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:
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:
"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.
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.
"postCreateCommand": "node --watch app.js"
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
.
"remoteUser": "node"
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
:
RUN adduser newuser
"remoteUser": "newuser"
Here are the final versions of Dockerfile
and devcontainer.json
:
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
:
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 theDockerfile
:
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 on Mac or Gpg4win on Windows. You can find the full guide here: 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
forgpg
to work in a Node.js dev container. Make sure to include the following instruction in yourDockerfile
:
USER node
If you still get an error, try installing pinentry
via Homebrew:
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.
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_).