You started on a new project, went through all the onboarding process, met your team, discovered the details of your new challenge and is now able to start the coding. Sounds exciting right?
But you know that before you start to define any variable in your IDE you need to setup all the tools, languages and environments for the project to work. And now the first challenge of you project is not to develop a new useful feature for the client, but to debug the tools that are not working as expected in your machine because of some computer magic.
This all sounds familiar. Right?
What are a Development Containers (or DevContainers)?
Development Containers are a specification that allows you to use containers as a full-featured development environment (read more on containers.dev). This enables users and teams to create a consistent, shareable, reproducible and isolated development environment.
Some benefits of using DevContainers are:
- A faster onboarding for anyone that wants to join your project. Now a single command is sufficient to get people ready to contribute;
- A shareable and reproducible environment that reduces problems related to different versions of packages and tools between team members;
- An isolated environment from your OS that prevents your local OS from interfering with your project.
What you are going to learn here?
This tutorial will help you set a DevContainer for VSCode using the official extension by Microsoft. At the end you will be able to create your own custom container, use it on VSCode and personalize it without interfering in your team's environment.
Requirements
DevContainers work on Windows and Linux (including WSL2). For this tutorial to work you will need:
- VSCode: You can install following this link;
- Docker Engine: You can install following the steps on Docker Engine Install.
Setup your DevContainer
Step 01: Setup your .devcontainer folder
For VSCode to autodetect the container you should have at least a Dockerfile
with your container's specification and a devcontainer.json
file containing metadata and settings for the environment. All these files should be in a .devcontainer
folder on the root of your project.
├── .devcontainer
│ ├── ... <- Any other related file that you want
│ ├── Dockerfile <- Container Dockerfile definition
│ └── devcontainer.json <- DevContainer's metadata and settings
└── ...
Step 02: Creating the Dockerfile
Here you can create a common Dockerfile with any base image, ARGs, ENVs and commands that you want your container to do. The example below will create a Ubuntu container with a python environment using Pyenv and Poetry. We also use the root
user but if you want to use a non-root user you can follow this link.
FROM ubuntu:22.04
ARG PYENV_VERSION=2.3.18
ARG POETRY_VERSION=1.5.1
ARG PYTHON_VERSION=3.11.3
ENV DEBIAN_FRONTEND=noninteractive
# Copy custom scripts and set the required permissions.
COPY custom-scripts/ /tmp/scripts/
RUN chmod +x /tmp/scripts/*
# Basic packages and requirements
RUN apt update && apt install -y git curl ca-certificates gnupg lsb-release locales locales-all nano jq wget
# Setting Locales
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US.UTF-8
# Install Pyenv and Python
ENV PYENV_ROOT="/usr/local/pyenv" PYENV_GIT_TAG=v${PYENV_VERSION}
ENV PATH="${PATH}:${PYENV_ROOT}/bin"
RUN apt-get install -y build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev curl libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev \
&& curl https://pyenv.run | bash \
&& pyenv install ${PYTHON_VERSION} \
&& pyenv global ${PYTHON_VERSION}
# Install Poetry
RUN unset PYENV_VERSION && pyenv exec pip install poetry==${POETRY_VERSION}
Lifecycle Scripts
You might have noticed the snippet in the Dockerfile above
# Copy custom scripts and set the required permissions
COPY custom-scripts/ /tmp/scripts/
RUN chmod +x /tmp/scripts/*
What it does is to copy some files from the custom-scripts
folder located inside the .devcontainer
and give the necessary permissions for them to be executed. This will be useful later on where we are going to run these scripts in some phases of the Docker lifecycle (e.g. when it creates, every time it starts and so on).
Step 03: The devcontainer.json file
The devcontainer.json
file is where you specify configuration for VSCode to run your containers. You can see the complete specification of settings and metadata on Development Containers metadata reference.
{
"name": "DevContainer",
"build": {
"dockerfile": "Dockerfile",
"args": {
"PYENV_VERSION": "2.3.18",
"POETRY_VERSION": "1.5.1",
"PYTHON_VERSION": "3.11.3"
}
},
"mounts": [
"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind,consistency=default"
],
"postStartCommand": "/bin/bash /tmp/scripts/poststart.sh",
"customizations": {
"vscode": {
"extensions": [
"ms-python.vscode-pylance",
"njpwerner.autodocstring",
"bungcip.better-toml",
"ms-python.black-formatter"
],
"settings": {
"editor.rulers": [79, 110],
}
}
}
}
Build
The build
information is responsible to setup the information for VSCode to build your container. This includes args, target, caches, the path to the Dockerfile and so on. The snippet below defines the path of the Dockerfile and set the args. See more on Image or Dockerfile specific properties.
{
...
"build": {
"dockerfile": "Dockerfile",
"args": {
"PYENV_VERSION": "2.3.18",
"POETRY_VERSION": "1.5.1",
"PYTHON_VERSION": "3.11.3"
}
},
...
}
postStartCommand and Lifecycle Scripts
Lifecycle scripts is a feature by DevContainers where you can make some commands run in some phases of the Docker Lifecycle. For example, on our example we use the postStartCommand
that runs everytime we start the container. This is useful for example to always update your OS or to make all your python dependencies installed. But there are more, you can use for example onCreateCommand
that will execute only when the container is created. You can read more about this on Lifecycle scripts and Start a process when the container starts.
{
...
"postStartCommand": "/bin/bash /tmp/scripts/poststart.sh",
...
}
Customizations
The customizations property is where DevContainers define information specific for your IDE. For VSCode we have two properties vscode.settings
and vscode.extensions
that defines the default settings and installed extensions for the environment. The example devcontainer.json
adds some extensions such as pylance, autodocstring, better-toml and black-formatter to the container and sets two editor rules for 79 (PEP8) and 110 characters.
{
...
"customizations": {
"vscode": {
"extensions": [
"ms-python.vscode-pylance",
"njpwerner.autodocstring",
"bungcip.better-toml",
"ms-python.black-formatter"
],
"settings": {
"editor.rulers": [79, 110],
}
}
}
}
Some tips for making this configuration easier:
- For extensions, you can go to any extension in the tab, expand the options on the
...
(three dots) and click on the optionadd to devcontainer.json
that the file will be automatically managed. - For settings you can open the command pallet on
Ctrl + Shift + P
and go to any configuration that you wish to alter, click on theMore actions
represented by a gear icon,copy setting as json
and add this to the json file.
Mounts (and Docker-From-Docker)
Mounts is the metadata used to specify your container's volumes as in any docker container. A common example of mount that is used is when you want to run docker within your container. Here we are setting up the Docker-from-Docker (or Docker-outside-of-Docker) approach where we mount the host's docker socket into the container so any build or docker command will actually execute on the host. See more on General devcontainer.json properties
{
...
"mounts": [
"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind,consistency=default"
],
...
}
Personalizing your environment with dotfiles
Each Linux user has their own set of settings for their software, some use a personalized shell like zshell, other use Vim with some tweaks as their main editor. To maintain the productivity of everyone it is important to maintain these settings across environments. Many Linux software uses dotfiles as their main way to store their settings and this is what we are going to explore.
One of the features of the DevContainer extension on VSCode is that we can pull dotfiles directly from a Github repository (public or private). This can be done by adding the following settings on your VSCode Settings (JSON) (or looking for similar settings in the UI).
{
"dotfiles.repository": "your-github-id/your-dotfiles-repo",
"dotfiles.targetPath": "~/dotfiles",
"dotfiles.installCommand": "~/dotfiles/install.sh"
}
What this means is that VSCode will copy all the content of the dotfiles.repository
into the folder dotfiles.targetPath
inside the container and will execute the dotfiles.installCommand
. You can omit the installCommand
and it will default to a install.sh
script inside the targetpath
. You can read more on
Personalizing with dotfile repositories.
A real example of a install.sh
(used by me) is presented below. This script will install zshell, Powerlevel10K theme and create symbolic links of my personalized settings into the required ones in the system.
#!/usr/bin/env sh
set -e
DOTFILES_LOCATION="${HOME}/dotfiles"
export DOTFILES_LOCATION;
apt install -y zsh fonts-powerline fzf
if [ -d "${HOME}/.oh-my-zsh" ]; then
printf "oh-my-zsh is already installed\n"
else
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended # Zshell
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k
fi
echo "creating symlinks for zsh"
ln -sf "${DOTFILES_LOCATION}/zsh/.zshrc" "${HOME}/.zshrc"
ln -sf "${DOTFILES_LOCATION}/zsh/.p10k.zsh" "${HOME}/.p10k.zsh"
ln -sf "${DOTFILES_LOCATION}/zsh/.zprofile.zsh" "${HOME}/.zprofile.zsh"
Note that everything in this section is completely optional, and you can use the default configuration for each software.
Conclusions
Learn about DevContainers were a turning point in the projects that I worked with reducing problems related to manual configuration of a series of tools on different environments. This tutorial should help you achieve the same benefits as I had by setting your custom development environment.
Thanks for Reading! Please, feel free to drop any suggestions, discussions, tips on the section below!
Top comments (0)