# Contents

You've heard about containers and how useful they are for development environments, but haven't taken the plunge yet.

Don't worry, we've got you covered. Containers can revolutionize the way you code by giving you isolated and disposable environments that are ready to code in, straight out of the box. In this ultimate guide, we'll show you everything you need to get started with dev containers.

By the end, you'll be spinning up customized containers for your projects and wondering how you ever lived without them.

Let's dive in and see how dev containers can streamline your workflow and make you a happier, more productive developer.

TL;DR
  • Dev containers are isolated, lightweight environments that provide a pre-configured development environment inside your editor or IDE. They save time by eliminating the need for manual setup and ensure a clean environment every time. Dev containers offer benefits such as pre-configured build environments, isolated environments, reproducible builds, less setup time, and flexibility in choosing base images.

What Are Dev Containers?

Dev containers are isolated, lightweight environments that allow developers to work inside a containerized version of a build environment. Basically, dev containers give you a pre-configured development environment right inside your editor or IDE.

As a developer, dev containers can save you a ton of time setting up projects and ensure you have a clean environment every time you start working.

Some of the main benefits of using dev containers include:

  • Pre-configured build environments. Dev containers come with a base image that has all the software, tools, and dependencies pre-installed so you can get started coding right away.

  • Isolated environments. Each dev container has its own isolated filesystem, networking, memory, and CPU - so there are no conflicts with other projects or software on your local machine.

  • Reproducible builds. Dev containers provide the exact same environment every time they're launched, so you get the same build results each time. No more "it works on my machine!" issues.

  • Less setup time. Starting a new project with dev containers means you can skip the lengthy setup and configuration process. Just open your project in the container and everything is ready to go.

  • Flexibility. You have options to choose a base image with the software and tools you want or build your own custom base image. So dev containers can flexibly meet your specific needs.

Dev containers revolutionize the developer experience by providing pre-configured, isolated environments that can supercharge your productivity. If you haven't tried them yet, you owe it to yourself as a developer to give dev containers a shot.

They may just change the way you work! The future is containerized!

Getting Started With Dev Containers

Getting started with Dev Containers is pretty straightforward. To use them, you'll need:

A code editor that supports Dev Containers Dev Containers currently work with Visual Studio Code and JetBrains IDEs like WebStorm. For this guide, we'll focus on VS Code.

Docker Desktop installed Dev Containers use Docker to build and run containers. So you'll need Docker Desktop for your OS installed and running.

A devcontainer.json file: This file defines your container environment. It specifies things like:

  • The Docker image you want to use (e.g., node:12-alpine)

  • Folders to mount into the container

  • Environment variables

  • Post-create scripts to run

VS Code has snippets to help you generate a devcontainer.json file for popular tech stacks.

Step-by-Step Example:

Create a new project folder:

1mkdir my-dev-container-project
2cd my-dev-container-project

Open the project in VS Code:

1code .

Add a .devcontainer folder with a devcontainer.json file:

1mkdir .devcontainer
2touch .devcontainer/devcontainer.json

Edit the devcontainer.json file:

1{
2 "name": "Node.js Sample",
3 "dockerFile": "Dockerfile",
4 "settings": {
5 "terminal.integrated.shell.linux": "/bin/bash"
6 },
7 "extensions": [
8 "dbaeumer.vscode-eslint"
9 ],
10 "postCreateCommand": "npm install",
11 "remoteUser": "node"
12}

Add a Dockerfile to the .devcontainer folder:

1FROM node:12-alpine

Build and run the container:

  • Press F1 and select "Remote-Containers: Reopen in Container" to build the container image and reopen the folder in the container.

  • VS Code will build the container, install its dependencies, and start the container - all in the background.

  • Your VS Code window will reload, and you'll be working directly in the container with everything set up and ready to go!

Anytime you need to build a fresh instance of the container, just run the "Remote-Containers: Rebuild and Reopen in Container" command again.

Choosing a Base Image

Choosing a base image for your dev container is an important first step. This base image contains the basic Linux operating system and initial tools/settings that your container will build upon.

Language-Specific Images If you're building a container for a particular programming language like Python, JavaScript, or Go, start with an image tailored for that language.

These images will have the runtime and base packages pre-installed so you can get started coding right away. Some popular options include:

  • python: For Python development. Comes with Python, Pip, and other basics.

  • node: For JavaScript/Node.js projects. Includes Node.js and NPM.

  • golang: For Go development. Comes with the Go compiler, build tools, and common libraries.

General Purpose Images For a more flexible dev container base, you can choose a general purpose image like:

  • Ubuntu: A popular Linux distribution with a lightweight footprint. Easy to install any languages/tools on top.

  • Debian: Another popular, open-source Linux OS. Stable and reliable.

  • Alpine: A tiny Linux distribution perfect for containers. Only a 5MB image but you can still install whatever you need.

These general images give you more control to fully customize your container's contents. However, it also means more work upfront to get your programming environment set up. It depends if you prefer convenience or flexibility!

In the end, choose an image that has:

  1. The minimum tools/packages you need to get started

  2. A small footprint for fast builds and load times

  3. Active maintenance to keep the image secure and up-to-date

Your dev container's base image establishes a solid foundation. From there, you can install specific tools, sync in source code, and fully configure your development environment. The possibilities are endless!

Adding Tools and Runtimes

Adding tools and runtimes to your dev container gives you a lot of flexibility. You have a few options for how to do this:

Install Tools/Runtimes When You Build the Image

This is good if you know exactly what you need for your project ahead of time. You can install tools/runtimes using the Dockerfile with RUN apt-get install.

1# Dockerfile
2FROM node:14
3
4# Install Python
5RUN apt-get update && apt-get install -y python3 python3-pip

Install Tools/Runtimes When You Start the Container

This is useful if you want to install things at runtime or if your tooling needs change from project to project. You can install tools/runtimes using the devcontainer.json postCreateCommand.

1{
2 "name": "Node.js Sample",
3 "image": "mcr.microsoft.com/vscode/devcontainers/javascript-node:0-14",
4 "postCreateCommand": "npm install && pip install requests"
5}

Use devcontainer.json build.args to Pass in Tools/Runtimes as Build Arguments

This lets you build once but start the container multiple times with different tools and runtimes. You pass the build args when you start the container, and the Dockerfile uses the build args to conditionally install tools/runtimes.

1# Dockerfile
2ARG PYTHON_VERSION=3.8
3FROM python:${PYTHON_VERSION}
4
5RUN apt-get update && apt-get install -y curl
1{
2 "name": "Python Sample",
3 "build": {
4 "dockerfile": "Dockerfile",
5 "args": {
6 "PYTHON_VERSION": "3.9"
7 }
8 }
9}

You can refer to this documentation for understanding the syntax and usage of build arguments: Docker ARG documentation

Create a devcontainer.json with Multiple Builds for Different Stacks

This allows you to pick a "stack" when starting the container and get a set of predetermined tools. You define multiple build objects in devcontainer.json, each with a Dockerfile to install a particular set of tools. The devcontainer.json reference guide explains the available options and settings you can use to customize your development containers.

Install a Common Set of Tools in the Dockerfile, Then Use Install-Specific Tools in devcontainer.json

This gives you a base set of tools on start, then you can install additional tooling as needed for your particular project in the postCreateCommand.

Use Docker Compose to Start Additional "Sidecar" Containers with Other Tools/Runtimes

This lets you start up other containers with tooling/runtimes that your main dev container can access. You define the sidecar services in a docker-compose.yml file referenced by devcontainer.json. The Docker Compose documentation explains how to define and manage multi-container applications using Docker Compose YAML files.

1# docker-compose.yml
2version: '3'
3services:
4 dev:
5 build: .
6 volumes:
7 - .:/workspace
8 command: npm start
9 db:
10 image: postgres
11 environment:
12 POSTGRES_PASSWORD: example

Using a combination of these techniques, you can craft a dev container with a robust set of tools for any development needs.

The options are plentiful - you just have to choose what works best for your particular project!

Mounting Source Code

To get started developing in a container, you'll first need to mount your source code as a volume. This allows you to edit your code in your IDE of choice on your host machine, while building and running it in the container. There are a couple ways to mount your source code:

Bind Mounts The easiest approach is to use bind mounts. This mounts a directory on your host machine into the container. Anything you change on the host will be reflected in the container and vice versa. To set up a bind mount, you specify the -v flag when running your container, like this:

1docker run -v /path/on/host:/path/in/container ...

So if your code was in /home/user/code on your host machine, and you wanted to mount it to /app in your container, you'd run:

1docker run -v /home/user/code:/app ...

Now your container will have your source code in the /app directory, and any changes you make on either the host or in the container will be mirrored in the other.

Volume Drivers

Bind mounts are simple but have some downsides, like permissions issues. An alternative is to use a volume driver. Volume drivers handle more advanced storage options for Docker volumes. Some free options are:

  • Local: The default driver. Uses bind mounts.

  • CIFS: Allows you to mount Windows file shares.

  • NFS: For *nix file shares.

  • OverlayFS: An advanced option with better performance than bind mounts.

To use a volume driver, you specify it after the -v flag, in the format:

1docker run -v :: ...

So to use the overlayFS driver to mount your code, it would be:

1docker run -v overlayFS:/path/on/host:/path/in/container ...

Volume drivers open up more advanced options for managing your Docker volumes and avoiding some of the issues with bind mounts. I'd recommend starting with bind mounts for simplicity, but exploring volume drivers as your needs evolve.

Setting Up Container Entrypoints

When setting up Dev Containers, you need to define entrypoints which specify what commands should be run when the container starts up.

Entrypoints allow you to bootstrap your development environment by installing dependencies, setting up your codebase, starting servers, and anything else you need to do to get your project running.

There are a few options for defining entrypoints:

  • Dockerfile entrypoint - You can define an ENTRYPOINT in your Dockerfile that will run when the container starts. This is good for simple entrypoints, but isn't flexible if you need to override it. Here is the doc to learn more.

  • docker-compose.yml entrypoint - You can define an entrypoint in your docker-compose.yml file. This gives you more flexibility, as you can override it when launching the container. Refer the doc to read further.

  • Shell script - You can write a shell script, mark it as executable, and use it as your entrypoint. This is a great option if you have a complex entrypoint with conditional logic. You can pass arguments to the shell script when launching the container to control its behavior.

  • Rebuild container - You can build a new image with a different ENTRYPOINT to redefine your entrypoint. This isn't ideal, as it requires rebuilding your image. It's better to use one of the other options for a flexible entrypoint.

A good entrypoint should:

  • Install any dependencies (npm install, bundle install, apt-get install, etc.)

  • Set up your codebase (migrate databases, compile assets, etc.)

  • Start any required servers (npm start, rails s, etc.)

  • Run in the foreground and tail the logs

  • Pass through any signals so Ctrl+C will stop the container

Defining a solid container entrypoint is key to having a smooth dev container experience. Put in the effort to get it right, and your dev container will start up with everything you need to get coding!

You'll be able to dive right into your work without having to deal with installation or configuration.

Use Cases of Dev Containers

Dev Containers have tons of useful applications for developers. Here are a few of the major use cases.

Local Development Environments

You're working on a Python project that requires specific versions of Python and several libraries. With a dev container, you can create a Dockerfile and a devcontainer.json file that installs the necessary Python version and libraries. Whenever you open the project in VS Code, it automatically sets up the environment, and you can start coding immediately.

1{
2 "name": "Python Dev Container",
3 "dockerFile": "Dockerfile",
4 "settings": {
5 "python.pythonPath": "/usr/local/bin/python3"
6 },
7 "extensions": [
8 "ms-python.python"
9 ],
10 "postCreateCommand": "pip install -r requirements.txt"
11}
1FROM python:3.8-slim
2
3COPY requirements.txt .
4RUN pip install --no-cache-dir -r requirements.txt

Onboarding New Team Members

Your team is working on a JavaScript application. Instead of having new team members manually install Node.js, npm, and other dependencies, you provide them with a devcontainer.json file. They clone the repository, open it in VS Code, and the dev container sets up everything for them automatically.

1{
2 "name": "Node.js Dev Container",
3 "image": "mcr.microsoft.com/vscode/devcontainers/javascript-node:0-14",
4 "postCreateCommand": "npm install"
5}

Isolated Environments

You want to test a new version of a library without affecting your current setup. You create a new branch and modify the Dockerfile in your dev container to use the new version of the library. If the new version causes issues, your main environment remains unaffected.

1FROM node:14
2
3RUN npm install -g new-library@beta

Reproducible Setups

Your project uses a specific set of tools and configurations. By committing your devcontainer.json and Dockerfile to the repository, every team member and CI/CD pipeline can use the exact same environment.

1{
2 "name": "Consistent Dev Environment",
3 "dockerFile": "Dockerfile",
4 "settings": {
5 "editor.formatOnSave": true
6 },
7 "extensions": [
8 "dbaeumer.vscode-eslint",
9 "esbenp.prettier-vscode"
10 ],
11 "postCreateCommand": "npm install"
12}
1FROM node:14-alpine
2
3RUN npm install -g eslint prettier

Tooling Experiments

You want to experiment with the latest version of Go for a new project. You create a dev container that installs the latest Go version and sets up a sample project. If you decide not to proceed, you can delete the container without any impact on your local environment.

1{
2 "name": "Go Dev Container",
3 "image": "golang:latest",
4 "postCreateCommand": "go mod tidy"
5}

CI/CD Environments

Your CI/CD pipeline can use the Dockerfile and devcontainer.json to create the same environment as your local development. This ensures that tests run in the same environment as development, reducing discrepancies between local and CI builds.

1# .github/workflows/ci.yml
2name: CI
3
4on: [push, pull_request]
5
6jobs:
7 build:
8 runs-on: ubuntu-latest
9 container:
10 image: ghcr.io/owner/repo:devcontainer
11 steps:
12 - uses: actions/checkout@v2
13 - name: Set up Node.js
14 uses: actions/setup-node@v2
15 with:
16 node-version: '14'
17 - run: npm install
18 - run: npm test
  • Do I need Docker installed? Yes, Dev Containers utilize Docker containerization technology. You'll need Docker Community Edition (CE) or higher installed.

  • Do Dev Containers replace my local development environment? No, Dev Containers run within your local environment and development tools. They simply contain the runtimes and dependencies for your project.

  • Can I commit and push from within a Dev Container? Yes, Dev Containers mount your local source code into the container. You can freely commit, push, pull, and work with Git as needed.

  • Do I need an internet connection to use Dev Containers? Dev Containers do require an internet connection the first time they're built to pull dependencies. After the initial build, no internet connection is needed to use a Dev Container. However, if your project has an npm install or similar, an internet connection would be needed for those package installations.

  • Are Dev Containers platform agnostic? Yes, Dev Containers can be used on Windows, macOS, and Linux since they utilize Docker containerization. The experience may slightly differ between operating systems but the end result is the same.

  • Can I use any code editor/IDE with Dev Containers? Dev Containers work with any editor that supports the Remote - Containers extension including VS Code, Visual Studio, Atom, Sublime Text, and more.

Dev Containers Best Practices

Keep your dev container lightweight The more you pack into your dev container, the larger its image size grows. This can impact build and startup times. Only include tools, packages and software specifically needed for your project. Don't install extras "just in case" you might need them someday. You can always add more later if needed.

You can refer to this guide about creating a lightweight windows container environment without docker or this on creating creating lightweight container in 5 steps.

Use multi-stage builds Multi-stage Docker builds allow you to separate your dev environment from artifacts you want to ship. You can have one stage for your dev environment, and another to build your application. The second stage can copy only the built artifact from the first stage, keeping your final image lean.

Cache dependencies Caching dependencies and packages between builds can significantly speed up dev container startup times. Docker's cache directive was made for this. Add a line like RUN npm install --cache /tmp/cache to your Dockerfile, and Docker will reuse the cached /tmp/cache directory on subsequent builds.

Keep containers ephemeral Treat your dev containers as ephemeral. Don't store any important files or data on the container itself. Commit all your source code and work to your host machine or a Docker volume. This allows you to freely rebuild your container image without worrying about losing work.

Use Docker Compose Docker Compose allows you to define multiple services - like a database container, cache container and your dev container - in a single YAML file. With Docker Compose, you can spin up your entire dev environment with a single command. No need to start containers individually and wire them up.

Refer this official guide by Containers.dev using docker compse.

Bind to host networking For the easiest development experience, bind your dev container to your host machine's network. This will give the container access to services on your host like databases or API servers. It will appear as just another process on your network.

Keep images up to date As with any software, the components in your dev container like Ubuntu, Node, etc release updates to patch security issues and bugs. It's a good practice to periodically rebuild your dev container image to pull in the latest updates. This helps keep your environment secure and working as expected.

Dev Containers aim to simplify development environments and boost productivity. Give them a try and see how they can improve your workflow!

Dev Container FAQs: Common Questions Answered

Here are some frequently asked questions (FAQs) about dev containers:

General Questions

Do I Need Docker Installed?

  • Yes, Dev Containers utilize Docker containerization technology. You'll need Docker Community Edition (CE) or higher installed.

Do Dev Containers Replace My Local Development Environment?

  • No, Dev Containers run within your local environment and development tools. They simply contain the runtimes and dependencies for your project.

Can I Commit and Push from Within a Dev Container?

  • Yes, Dev Containers mount your local source code into the container. You can freely commit, push, pull, and work with Git as needed.

Do I Need an Internet Connection to Use Dev Containers?

  • Dev Containers do require an internet connection the first time they're built to pull dependencies. After the initial build, no internet connection is needed to use a Dev Container. However, if your project has an npm install or similar, an internet connection would be needed for those package installations.

Are Dev Containers Platform Agnostic?

  • Yes, Dev Containers can be used on Windows, macOS, and Linux since they utilize Docker containerization. The experience may slightly differ between operating systems, but the end result is the same.

Can I Use Any Code Editor/IDE with Dev Containers?

  • Dev Containers work with any editor that supports the Remote - Containers extension, including VS Code, Visual Studio, Atom, Sublime Text, and more.

What are dev containers?

  • Dev containers are isolated, lightweight environments that provide a pre-configured development environment inside your editor or IDE. They use Docker containers to encapsulate your development environment, ensuring consistency and eliminating setup issues.

Why should I use dev containers?

  • Dev containers simplify the setup of your development environment, provide consistency across different machines, eliminate "it works on my machine" problems, and ensure reproducibility. They also allow you to experiment with different configurations without affecting your local setup.

What do I need to get started with dev containers?
To get started with dev containers, you'll need:

  • A code editor that supports dev containers (e.g., Visual Studio Code or JetBrains IDEs).

  • Docker Desktop installed and running.

  • A devcontainer.json file to define your container environment.

Setup and Configuration

How do I create a devcontainer.json file?

  • You can create a devcontainer.json file manually or use the snippets provided by VS Code. The file defines the Docker image to use, the folders to mount, environment variables, and any post-create scripts to run.

How do I choose a base image for my dev container?

  • Choose a base image that matches the programming language or tech stack you're using. For example, use node for Node.js projects, python for Python projects, or general-purpose images like ubuntu or alpine for more flexibility.

How can I add additional tools and runtimes to my dev container?
You can add tools and runtimes in several ways:

  • Install them in the Dockerfile using RUN commands.

  • Use the postCreateCommand in the devcontainer.json file.

  • Use build arguments to conditionally install tools.

  • Combine multiple Dockerfiles in devcontainer.json for different setups.

  • Use Docker Compose to add sidecar containers with additional tools.

Working with Dev Containers

How do I mount my source code into a dev container?

  • You can mount your source code using bind mounts by specifying the -v flag in the Docker run command or by defining mounts in the devcontainer.json file. This allows you to edit code on your host machine while running it inside the container.

How do I start debugging inside a dev container?

  • Configure the debugging settings in the devcontainer.json file under the customizations section. Then, use the debug panel in your code editor to start the debugger, set breakpoints, and step through your code.

How can I share my dev container setup with my team?

  • Share the devcontainer.json file within your project's repository. Alternatively, you can create a Docker image and push it to a container registry, or use Docker Compose for more complex setups and reference it in your devcontainer.json.

Troubleshooting

What should I do if my container fails to start?

  • Check the container logs using docker logs <container-id>, ensure your configuration files are correct, and verify that all dependencies are properly installed.

How can I resolve slow performance issues?

  • Ensure your container has enough CPU and memory allocated, optimize your code to reduce disk I/O, and consider using faster storage options if disk access is a bottleneck.

What can I do if I encounter network issues?

  • Ensure there are no port conflicts, and if your container cannot resolve domain names, try using a custom DNS server by adding --dns to your Docker run command.

How do I handle dependency conflicts?

  • Ensure that the dependencies in your container match the required versions for your project. Use a package manager to manage dependencies and specify exact versions to avoid conflicts.

By addressing these FAQs, you can get a better understanding of dev containers and how to effectively use them in your development workflow.

Conclusion

So there you have it, everything you need to know about Dev Containers to make your life as a developer so much easier.

With the power of Docker and Visual Studio Code, you now have a portable, isolated development environment that can replicate your production environment locally. No more "it works on my machine!" excuses.

You'll be coding with confidence and shipping better software in no time. What are you waiting for? Spin up a Dev Container, install your dependencies, and get to work. Your future self will thank you. Happy coding!

Tags::
  • devcontainer.json
  • productivity