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
From there we can install or enable (in my case update) the “Dev Containers” extension
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
At first I was fighting many different errors such as a missing entrypoint.sh file (which I do not have in my Dockerfile)
Then apt installs of wget and curl which are also not in my Dockerfile
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
I installed with bundle install
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
I see two ports served
Which reminded me that livereload uses a port as I verified the contents
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
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.
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
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
As before, the first time is always a bit slower as it has to build or pull the container
But now I’m fully launched into a Dotnet container
I can press F5 or just use the menu to run without debugging
And in just a moment it has compiled and started the .net app and is serving it on Port 5000
Which is just a simple hello world app
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.
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.