VSCode Dev Containers

Published: Sep 24, 2024 by Isaac Johnson

From time to time, I’ve had the need to develop in a more obscure language or complicated development environment. Recently I had to swap laptops for work and was reminded of all my nuanced dependencies that I had setup over time.

One way to accomplish having more rapid setup is to use Visual Studio Dev Containers.

Today we’ll set them up and show how we can use them to make sharing a development environment much easier.

Installation

To start, we go to extensions in VSCode

/content/images/2024/09/devenv-01.png

From there we can install or enable (in my case update) the “Dev Containers” extension

/content/images/2024/09/devenv-02.png

Let’s consider this somewhat complicated and dated Jekyll setup I use to blog. The local Github runner is defined as:

FROM summerwind/actions-runner:latest

RUN sudo apt update -y \
  && umask 0002 \
  && sudo apt install -y ca-certificates curl apt-transport-https lsb-release gnupg

# Install MS Key
RUN curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null

# Add MS Apt repo
RUN umask 0002 && echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ focal main" | sudo tee /etc/apt/sources.list.d/azure-cli.list

# Install Azure CLI
RUN sudo apt update -y \
  && umask 0002 \
  && sudo apt install -y azure-cli awscli ruby-full

# Install Pulumi
RUN curl -fsSL https://get.pulumi.com | sh

# Install Homebrew
RUN /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# OpenTF

# Install Golang 1.19
RUN eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" \
  && brew install go@1.19
#echo 'export PATH="/home/linuxbrew/.linuxbrew/opt/go@1.19/bin:$PATH"'

RUN eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" \
  && brew install opentofu

RUN sudo cp /home/linuxbrew/.linuxbrew/bin/tofu /usr/local/bin/

RUN sudo chown runner /usr/local/bin

RUN sudo chmod 777 /var/lib/gems/2.7.0

RUN sudo chown runner /var/lib/gems/2.7.0

# Install Expect and SSHPass

RUN sudo apt update -y \
  && umask 0002 \
  && sudo apt install -y sshpass expect

# save time per build
RUN umask 0002 \
  && gem install bundler -v 2.4.22

# Limitations in newer jekyll
RUN umask 0002 \
  && gem install jekyll --version="~> 4.2.0"
  
RUN sudo rm -rf /var/lib/apt/lists/*

#harbor.freshbrewed.science/freshbrewedprivate/myghrunner:1.1.16

I don’t need that day-to-day, but that container is rather hefty to build and rebuild which is why i have it preloaded in my local Harbor CR.

How might we translate to a Dev Container?

The basic setup for Dev Containers is just a folder with two files

builder@LuiGi:~/Workspaces/jekyll-blog$ tree ./.devcontainer/
./.devcontainer/
├── Dockerfile
└── devcontainer.json

0 directories, 2 files

The JSON should describe your dev env. In my case, I serve Jekyll on 4000

builder@LuiGi:~/Workspaces/jekyll-blog$ cat .devcontainer/devcontainer.json
{
  "name": "My Jekyll Runner",
  "hostRequirements": {
    "cpus": 4
  },
  "build": {
    "dockerfile": "Dockerfile"
  },
  "waitFor": "onCreateCommand",
  "updateContentCommand": "bundle exec jekyll serve",
  "forwardPorts": [4000],
  "customizations": {
    "codespaces": {
      "openFiles": ["index.html"]
    }
  },
  "portsAttributes": {
    "4000": {
      "label": "Serve",
      "onAutoForward": "openPreview"
    }
  }
}

I’m going to try a Slim 22 with Bookworm first

builder@LuiGi:~/Workspaces/jekyll-blog$ cat .devcontainer/Dockerfile
FROM bookworm-slim:latest

RUN sudo apt update -y \
  && umask 0002 \
  && sudo apt install -y ca-certificates curl apt-transport-https lsb-release gnupg

# Install Azure CLI
RUN sudo apt update -y \
  && umask 0002 \
  && sudo apt install -y ruby-full

RUN sudo chmod 777 /var/lib/gems/2.7.0

RUN sudo chown runner /var/lib/gems/2.7.0

# Install Expect and SSHPass

RUN sudo apt update -y \
  && umask 0002 \
  && sudo apt install -y sshpass expect

# save time per build
RUN umask 0002 \
  && gem install bundler -v 2.4.22

# Limitations in newer jekyll
RUN umask 0002 \
  && gem install jekyll --version="~> 4.2.0"

RUN sudo rm -rf /var/lib/apt/lists/*

I can now run this in the container

/content/images/2024/09/devenv-03.png

At first I was fighting many different errors such as a missing entrypoint.sh file (which I do not have in my Dockerfile)

/content/images/2024/09/devenv-04.png

Then apt installs of wget and curl which are also not in my Dockerfile

/content/images/2024/09/devenv-05.png

I figured out that .devcontainers was using the Dockerfile at my root, not the one in .devcontainers. Here I had been assuming we used .devcontainers/Dockerfile.

I updated my root one and after considerable time it booted.

I went to forward ports but nothing was being served. When I sent to launch directly, I saw some missing Ruby packages

/content/images/2024/09/devenv-06.png

I installed with bundle install

/content/images/2024/09/devenv-07.png

Then discovered a minor issue with Jekyll and the newer Ruby 3.0 when I kept seeing

/var/lib/gems/3.0.0/gems/pathutil-0.16.2/lib/pathutil.rb:502:in `read': no implicit conversion of Hash into Integer (TypeError)

However, this StackOverflow clued me into a working fix which was to add a line for an updated pathutil

# frozen_string_literal: true

source "https://rubygems.org"
gemspec

gem "jekyll", "~> 3.9"
gem "pathutil", github: "sdogruyol/pathutil", ref: '6ab144a7706c2bc5fa0dfdfa498e94ff66e944c6'

group :jekyll_plugins do
    gem "jekyll-feed", "~> 0.6"
    gem "jekyll-sitemap"
    gem "jekyll-paginate"
    gem "jekyll-seo-tag"
end

Now I could bundle install then bundle exec jekyll server as I was accustomed to doing

/content/images/2024/09/devenv-08.png

I see two ports served

/content/images/2024/09/devenv-09.png

Which reminded me that livereload uses a port as I verified the contents

/content/images/2024/09/devenv-10.png

One thing that surprised me, which really shouldn’t, was that the devcontainer was mapped to my local workspace. So as I edited this blog entry, the git workspace in the devcontainer view showed the same staged files

/content/images/2024/09/devenv-11.png

We can see a live demo of all this in action

Other environments

There are a few more options we can see in Microsoft’s getting started. Just searching for ‘vscode-remote-try’ shows we have a few options including

Honestly, I’m a bit surprised there is no Ruby considering they have older languages like C++ and PHP.

/content/images/2024/09/devenv-13.png

They don’t really push devcontainers as much as Codespaces. We can see we can launch any one of these into the Cloud using Codespaces from the Code dropdown

/content/images/2024/09/devenv-14.png

However, if I wanted to use that example above (dotnet) locally with devcontainers, I could clone down the repo locally and launch into VSCode

builder@DESKTOP-QADGF36:~/Workspaces$ git clone https://github.com/microsoft/vscode-remote-try-dotnet.git
Cloning into 'vscode-remote-try-dotnet'...
remote: Enumerating objects: 360, done.
remote: Counting objects: 100% (140/140), done.
remote: Compressing objects: 100% (50/50), done.
remote: Total 360 (delta 108), reused 96 (delta 87), pack-reused 220 (from 1)
Receiving objects: 100% (360/360), 96.38 KiB | 1.48 MiB/s, done.
Resolving deltas: 100% (177/177), done.


builder@DESKTOP-QADGF36:~/Workspaces$ cd vscode-remote-try-dotnet
builder@DESKTOP-QADGF36:~/Workspaces/vscode-remote-try-dotnet$ code .

I could use the command menu to find and launch, but I noticed VSCode prompted me to do that already in the lower right

/content/images/2024/09/devenv-15.png

As before, the first time is always a bit slower as it has to build or pull the container

/content/images/2024/09/devenv-16.png

But now I’m fully launched into a Dotnet container

/content/images/2024/09/devenv-17.png

I can press F5 or just use the menu to run without debugging

/content/images/2024/09/devenv-18.png

And in just a moment it has compiled and started the .net app and is serving it on Port 5000

/content/images/2024/09/devenv-19.png

Which is just a simple hello world app

/content/images/2024/09/devenv-20.png

Summary

Dev Containers are not new. I can recall them from some time back but had some troubles sourcing the original release date.

Essentially they were “Remote Containers” and I could see some articles about running Blazor in them in 2020. Around VS Code 17.4, we see “Remote - Containers” were renamed “Dev Containers” and some notes about adding C++ in this Sept 2022 article.

I’ve been meaning to check them out for some time. While they aren’t supported in ProjectIDX from Google, I do see a similar “DevPod containers” that is someone’s pet project.

/content/images/2024/09/devenv-21.png

I think the power of Devcontainers, over say just running in Docker, is that we have native port-forwarding. I can build and test and the workspace is still rooted locally (in my WSL, in my case) and I can see the ports in Windows.

I would need to launch a Docker image with ports pre-determined and a mapping to a local dir to test it that way. Then I would need to decide if I live in vim in Docker or just VS Code locally but see the changes reflected in the image. In either case, it would solve the problem of dependencies and quick development environment setup using containers.

If there is interest, I might explore some Podman options and using Docker locally as a follow-up article. Hopefully you got use out of this - I know every time I rebuild a WSL the setup of Ruby for Jekyll is a big pain so I’ll likely use this Devcontainer for future blog work on new machines.

DevContainers VSCode Ruby Jekyll

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes