Programming inside a Developer Container
Table of Contents
Hello devs,
I’ve recently been exploring the synergy between Docker and VSCode for local development. The concept of consolidating code and dependencies into containers has caught my attention. This approach not only establishes a clean slate for each project but also promotes a stateless system, a goal that aligns seamlessly with my coding environment.
To explore these ideas further, let’s create a small project: containerizing an installation of Hugo. This way, we can run our own dev blog anywhere without worrying about configuration and compatibility issues. It sounds great, so let’s dive into the code and make it happen!
What are Dev Containers?#
Dev containers, a feature in VSCode, empower us to use local containers for programming, seamlessly configured with both our dependencies and code. This operates under the hood, allowing us to open our projects within these containers and easily push changes to platforms like GitHub.
In essence, we work as if we were coding locally, avoiding any impact on our real system. Once our work is complete, we can delete these containers from our system without any repercussions. The magic doesn’t stop there—having a reproducible environment enhances teamwork among developers. Now, we can ensure the same environment, regardless of the system in use, eliminating the classic “it works on my machine” scenario.
It’s truly magical.
Prerequisites#
To make this project, we only need two tools installed in our system:
- Docker (local or remote works, in this tutorial we assume it’s installed locally)
- Visual Studio Code
I assume you’re familiar with containers, how Docker operates, and have a basic understanding of using Hugo
Creating the Initial Configuration#
Let’s kick things off by establishing the working directory for our Hugo blog. Just as we would with any other project, we create a directory to store our code. This directory can be uploaded to GitHub, and you can manage it with Git as usual.
mkdir hugo_blog
code hugo_blog # opens our project in VSCode; if this doesn't work, add the 'code' command to your system
Once our directory is set up, let’s initialize Git to track our changes.
git init
Creating the Configuration for Our Dev Container#
After preparing our base project, let’s set up the configuration files for dev containers. These files will be stored in a hidden directory named .devcontainer
. Let’s start by creating it.
# Inside our project directory
mkdir .devcontainer
Now, let’s generate the JSON file that VSCode will utilize to configure and create the container for us. This file must be named devcontainer.json
and should reside within the .devcontainer
directory.
Configuring the Creation of the Container with Hugo#
Now, let’s dive into the exciting part: configuring the container creation process with the Hugo toolchain. This step automates the installation of all the necessary components for our project. By doing this, we eliminate the need to worry about configuration and can seamlessly dive into our actual work.
To begin, let’s select a base image for our container. No need to fret about writing any docker-compose
or Dockerfile
. Everything is taken care of by the dev container.
Microsoft already offers several base images for our dev containers, let’s use their image for our project.
With devcontainer.json
, we can specify the VSCode extensions we want installed in our container. Given that we are configuring a Hugo blog, let’s include a spell checker extension.
I’ve opted for alpine
, a compact Linux system ideal for containers due to its small memory footprint. Now, let’s incorporate this configuration into our devcontainer.json
.
{
// The name for our Dev Container; note that this is not the actual Docker container name
"name": "Entangled Dev Blog",
// We can use any other image as a base, but let's stick with Alpine for this tutorial
"image": "mcr.microsoft.com/devcontainers/base:alpine",
// Configure tool-specific properties.
"customizations": {
"vscode": {
"extensions": ["streetsidesoftware.code-spell-checker"]
}
},
// runArgs can execute various commands; here, we use --name to assign a name to our Docker container
"runArgs": ["--name", "entangleddev_devcontainer"],
}
Great job! Now, to run and work in our dev container, make sure you have the ms-vscode-remote.remote-containers
extension installed. Once installed, click on the small ><
icon at the bottom left of VSCode and select Reopen in container
.
While this approach works, setting up everything manually each time can be tedious. Let’s add some automation to streamline the process. Additionally, we don’t have Hugo installed yet, so there’s a bit more work to be done.
At this point, consider pushing your changes to GitHub. Now, every time you clone your project and open it with VSCode, you can replicate this environment on any computer with Docker.
Installing Hugo#
Our devcontainer.json
still has some cool tricks up its sleeve—it can run commands once the container is created. This comes in handy for installing the needed dependencies and configuring our environment. To achieve this, we use "postCreateCommand"
. Unfortunately, this command only accepts a string with all the commands we want to execute in the terminal. Therefore, we’ll create a script to handle all the necessary configurations for us.
Before proceeding, we need to incorporate instructions in our devcontainer.json
that will run once the container is created. To do this, replace the existing code in our JSON file with the following updated version:
{
// The name for our Dev Container; note that this is not the actual Docker container name
"name": "Entangled Dev Blog",
// We can use any other image as a base, but let's stick with Alpine for this tutorial
"image": "mcr.microsoft.com/devcontainers/base:alpine",
// Configure tool-specific properties.
"customizations": {
"vscode": {
"extensions": ["streetsidesoftware.code-spell-checker"]
}
},
// runArgs can execute various commands; here, we use --name to assign a name to our Docker container
"runArgs": ["--name", "entangleddev_devcontainer"],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "ash .devcontainer/configuration.sh"
}
To install Hugo on Linux, we have several options. For Ubuntu, we could use a Snap (though it might not work inside containers), but we’ll opt for getting a precompiled program from its official GitHub page and configuring it. We’ll be going with the latter.
Let’s start by creating a shell script to handle our configuration. Within the .devcontainer
directory, create a new file named configuration.sh
and insert the following code:
#!/bin/ash
echo "==> Starting container post-installation"
sudo apk update
sudo apk add --no-cache python3 py3-pip wget git tar
pip3 install requests
python3 .devcontainer/download.py
echo "==> Finished container post-installation"
Here’s a breakdown of what the code accomplishes:
- Alpine employs apk instead of apt.
- Given Alpine’s minimalist nature, we include the necessary dependencies for our container.
- To simplify data retrieval from the GitHub server, we’ll create a Python script named
download.py
.
Continuing, let’s create our Python script that will handle the download and unpacking of Hugo. Within the .devcontainer
directory, create a new file name download.py
and insert the following code:
import json
import requests
import os
class HugoInstaller:
def __init__(self):
self.response = None
self.asset_url = ""
self.keyword = "arm64.tar.gz"
self.package_name = ""
self.dir = "/usr/local/bin/hugo_data"
def start_installation(self):
self._configure_git()
self._download_hugo()
self._unzip_installer()
self._install_hugo_theme()
def _configure_git(self):
os.system('git config --global user.name "Docker Container"')
os.system('git config --global user.email "[email protected]"')
def _download_hugo(self):
url = "https://api.github.com/repos/gohugoio/hugo/releases/latest"
self.response = requests.get(url)
if self.response.status_code == 200:
json_data = self.response.json()
assets = json_data["assets"]
self.asset_url = ""
for asset in assets:
file_name = asset["name"]
if self.keyword in file_name:
self.asset_url = asset["browser_download_url"]
self.package_name = asset["name"]
break
os.system(f"sudo mkdir {self.dir}")
os.system(f"sudo wget -P {self.dir} {self.asset_url}")
else:
print(f"Failed to fetch data. Status code: {self.response.status_code}")
def _install_hugo_theme(self):
'''Skip this function if you don't have a theme selected for your Hugo site'''
os.system("sudo git submodule update --init --recursive")
def _unzip_installer(self):
hugo_file_path = f"{self.dir}/{self.package_name}"
os.system(f"sudo tar -xzvf {hugo_file_path} -C {self.dir}")
os.system(f"sudo mv {self.dir}/hugo /usr/local/bin")
os.system(f"sudo rm -r {self.dir}")
if __name__ == "__main__":
installer = HugoInstaller()
installer.start_installation()
Here’s a breakdown of what the code accomplishes:
-
HugoInstaller
Class Initialization:- Initializes the
HugoInstaller
class with default values for properties likeresponse
,asset_url
,keyword
,package_name
, anddir
.
- Initializes the
-
start_installation
Method:- Calls private methods in sequence to initiate the installation process.
-
_configure_git
Method:- Configures global Git settings within the container to set user name and email.
-
_download_hugo
Method:- Fetches the latest release information for Hugo from the GitHub API.
- Parses the JSON response to extract the URL of the Hugo release asset containing the keyword “arm64.tar.gz.”
- Creates a directory (
/usr/local/bin/hugo_data
) for storing downloaded Hugo assets. - Downloads the Hugo release asset and saves it in the designated directory.
-
_install_hugo_theme
Method: (warning: call this method with your own theme or skip it altogether)- Initializes and updates Git submodules, specifically used for installing Hugo themes.
-
_unzip_installer
Method:- Extracts the downloaded Hugo release asset from its tar.gz format.
- Moves the extracted
hugo
executable to/usr/local/bin
for system-wide access. - Removes the temporary directory used for the installation process.
-
__main__
Block:- Instantiates the
HugoInstaller
class. - Invokes the
start_installation
method to commence the installation process.
- Instantiates the
Now rebuild your container, and Hugo will be installed. Now, you can start working on your blog inside a container. Commit your changes, and you’re done.
Phew, this was a bit of code, but now you’ve containerized your blog with Hugo. You can clone and start your dev container on any machine you want and begin writing about your awesome development journey.
More Resources#
To get more pre-configured images for your dev containers you can check out this GitHub repo by Microsoft here.
Conclusion#
This straightforward example serves as a foundation for creating your custom development environment tailored to different purposes. The versatility extends to running docker-compose
files for more intricate configurations.
I firmly believe that this stateless approach to local programming will gain increasing significance in the future. It addresses common issues encountered in team collaborations and personal projects by providing a clean system free from pollution. This enables experimentation with diverse tools and languages without compromising our actual systems.
While it’s worth noting a potential drawback of being tied to using VSCode, given its widespread adoption, most developers may find this limitation negligible compared to the considerable benefits.
I hope this inspires you to reconsider your approach to the local programming environment, offering new ways to enhance efficiency and safety in your work or hobby.
Thanks for reading, and happy coding!