---
title: "Why I use Dev containers for most of my projects"
date: 2023-02-09T23:14:05-05:00
toc: false
images:
tags:
  - dev
---

It is important to have a consistent and streamlined setup process for your
development environment. This saves time and minimizes frustration, both making
you more productive and also making it much easier to onboard new developers.
This is important, whether we're talking about a company who wants to onboard
new engineers or an open source project that needs more contributors, being able
to press one button to get a fully functional development environment is
incredibly valuable.

That's why I was thrilled when I discovered dev containers! Dev containers use
containers to automate the setup of your development environment. You can have
it install compilers, tools, and more. Set up a specific version of nodejs,
install AWS cli, or run a bash script to run a code generator. Anything you need
to set up your development environment. And you can also run services like
databases or message queues along with your development environment because it
has support for Docker compose.

For example, I use dev containers to spin up Redis and CouchDB instances for a
project I'm working on. It also installs pnpm, then uses it to install all the
dependencies for the codebase. The end result is that you can press one button
to have a fully functional development environment in under a minute.

This has many advantages. It ensures that everyone has the same version of any
services or tools needed, and isolates these tools from the base system. And if
you ship your code with containers, it also makes your development environment
very similar to your production environment. No more "well it works on my
machine" issues!

## Basic setup

I use dev containers with VSCode. It has pretty good support. I've also tried
the [dev container CLI](https://github.com/devcontainers/cli) which works fine
if you just want to keep everything in the CLI (although you could probably
stick with docker compose alone then!).

VSCode comes with commands to automatically generate a dev container
configuration for you by answering a few questions.

![A VSCode prompt window. "deb" is typed into the prompt, and the text "Simple debian container with git installed" is highlighted below.](/img/devcontainer-debian-example.png)

At the core of dev containers, what sets it apart from just using Docker is the
"features". These are pre-made recipes that install some tool or set up some
dependency within your dev container. There is a lot of these available,
installing everything from `pnpm` to `wget`. You can also set up commands to run
when the container is created --or even every time the container is started-- to
install or set up anything else that features didn't cover.

```json
{
  // ...
  "features": {
    "ghcr.io/devcontainers/features/node:1": {},
    "ghcr.io/devcontainers-contrib/features/pnpm:2": {}
  },
  "updateContentCommand": "pnpm install",
  // ...
}
```

Above is an excerpt from the dev container of a project I'm working on. I needed nodejs and pnpm,
and I then use pnpm to install the dependencies.

## Docker compose

But I honestly probably would not have used dev containers if this was all they
did. What I find even more impressive is that they can be set up to use docker
compose to bring up other services like I mentioned at the beginning.

To do that, you create your docker compose file with all the services you need,
but also add in the dev container.

```yml
version: '3.8'
services:
  devcontainer:
    image: mcr.microsoft.com/devcontainers/base:bullseye
    volumes:
      - ..:/workspaces/my-project:cached
    command: sleep infinity
    environment:
      - COUCHDB=http://test:test@couchdb:5984
      - S3=http://minio:9000

  couchdb:
    restart: unless-stopped
    image: couchdb:3.3
    volumes:
      - couchdb-data:/opt/couchdb/data
    environment:
      - COUCHDB_USER=test
      - COUCHDB_PASSWORD=test

  minio:
    restart: unless-stopped
    image: minio/minio
    volumes:
      - minio-data:/data
    command: server /data --console-address ":9001"

volumes:
  couchdb-data:
  minio-data:
```

In the example above, I'm setting up a CouchDB database and Minio S3-compatible
store. Docker gives containers access to each other using the container names. I
pass the endpoint URLs as environment variables to my dev container, where I can
read and use them.

Then, you just tell your dev container config to use the docker compose file.

```json
{
  "name": "my-project",
  "dockerComposeFile": "docker-compose.yml",
  "service": "devcontainer",
  "workspaceFolder": "/workspaces/my-project",

  // Adding the Rust compiler, plus the AWS cli so I can access the S3 API of minio from the CLI.
  "features": {
    "ghcr.io/devcontainers/features/rust:1": {},
    "ghcr.io/devcontainers/features/aws-cli:1": {}
  },

  // The project I'm working on exposes the port 8080.
  // I forward that out so I can look at it on my browser.
  "forwardPorts": [8080],

  // Set up the development AWS config and credentials with test values,
  "onCreateCommand": "mkdir -p ~/.aws/ && /bin/echo -e '[default]\nregion = local' > ~/.aws/config && /bin/echo -e '[default]\naws_access_key_id = minioadmin\naws_secret_access_key = minioadmin' > ~/.aws/credentials",

  // Create the S3 bucket
  "postCreateCommand": "aws s3 --endpoint-url $S3_ENDPOINT mb s3://my-bucket",

  // I found that I have to add this, but it's not the default. Not sure why.
  "remoteUser": "root",

  "customizations": {
    // You can even add in VSCode extensions that everyone working on the project
    // would need, without them having to install it on their own setup manually.
    "vscode": {
      "extensions": [
        "rust-lang.rust-analyzer",
        "streetsidesoftware.code-spell-checker"
      ]
    }
  }
}

```

That's it! Run the "Dev Containers: Reopen in Container" command in VSCode, give
it a few minutes, and you'll have your full development environment ready.
