# Contents

Dev Containers provide an excellent way to create portable, consistent development environments for your engineering teams. By defining the required languages, tools, services, and configurations in a Dockerfile, developers can spin up sandboxed environments that contain everything they need to be productive. This eliminates many of the "works on my machine" problems that can waste time and breed frustration when working on shared projects.

However, creating well-configured Dev Containers from scratch requires expertise in several domains - Docker, Linux, infrastructure automation, VS Code extensions, and more. The configuration is done via Bash scripts specified in the Dev Container format, which can be complex and tedious to write manually.

In this article, we will explore both the benefits and challenges of using Dev Containers, including a hands-on walkthrough of configuring a Python Flask app. We will also discuss the need for Dev Container helper libraries to simplify the process of authoring robust and reusable configurations.

Grab a GitHub repo here: https://github.com/metcalfc/simple-flask

Dev Container Benefits

Dev Containers provide several key benefits:

  • Portability - The environment travels with the code repository, enabling a consistent experience across different machines.

  • Encapsulation - Dependencies and configurations are encapsulated within the container, avoiding conflicts with the host or between projects.

  • Isolation - Containers provide a sandboxed environment isolated from the rest of the system.

  • Speed - Containers utilize layers and caching to initialize faster than virtual machines.

  • Flexibility - Different components like languages and tools can be compose into the desired environment.

  • Sharing - Dev Containers can be shared and reused across teams and organizations.

For example, a data science team could create a Dev Container with Python, Jupyter Lab, and common ML libraries baked in. This stable environment can then be reused by all data scientists in the organization, ensuring consistency and avoiding dependency conflicts.

Walkthrough: Python Flask App

Let's walk through configuring one for a simple Python Flask web application to see Dev Containers in action.

  1. Create Flask app: We'll start by creating a simple app.py that just returns "Hello World" and some Daytona metadata:

1from flask import Flask
2import socket
3
4app = Flask(__name__)
5
6@app.route('/')
7def hello():
8 return 'Hello, World!' + '\n' + str(socket.gethostname()) + '\n' + 'Daytona Rocks!'
9
10if __name__ == '__main__':
11 app.run(host='0.0.0.0')

2. Add Dockerfile: Next, we'll create a Dockerfile that installs Python and configures the container to run the app:

1FROM python:3.7
2WORKDIR /app
3COPY . .
4RUN pip install flask
5EXPOSE 5000
6CMD ["python", "app.py"]

3. Convert to Dev Container: We'll convert this to a Dev Container format with a few tweaks:

  • Use a development-focused Python image

  • Install needed apt packages like git

  • Set VS Code Server as the default shell

1{
2 "dockerFile": "Dockerfile",
3 "customizations": {
4 "vscode": {
5 "settings": {},
6 "extensions": []
7 }
8 },
9 "containerEnv": {
10 "FOO": "bar"
11 },
12 "postCreateCommand": "pip install -r requirements.txt",
13 "remoteUser": "vscode",
14
15 "features": {
16 "ghcr.io/devcontainers/features/github-cli:1": {}
17 }
18}

Rebuild on changes: Now when we make a change like installing a new package, we can rebuild the container to test it:

1devcontainer> git add .
2devcontainer> git commit -m "Install cowsay"
3devcontainer> git push
4
5# Rebuild container
6devcontainer> devcontainer rebuild

The container will rebuild with our changes and reconnect automatically, without having to start over!

Dev Container Challenges

However, while Dev Containers provide an excellent end-user experience, creating them still poses some challenges:

  • Bash scripting expertise required: All configuration is done through Bash scripts specified in the template. This requires Linux, Docker, Bash proficiency.

  • IDE integration complexity: Enabling full VS Code/IDE functionality requires special configuration like shell overrides, which can be tricky.

  • Lots of boilerplate: Common tasks like package installs, directory setup have to be reimplemented per container.

  • Idempotence issues: Scripts may be rerun and need to handle that gracefully.

  • Documentation dysync: Outdated docs and images lead to headaches.

  • Sharp edges: Care must be taken with paths, exit codes, permissions, and more.

While an experienced DevOps engineer may be comfortable navigating these complexities, they pose a significant barrier to rapid Dev Container authoring for many developers.

Need for Helper Libraries

To simplify and streamline Dev Container creation, we need to provide developers with helper libraries that abstract away common implementation details.

These libraries should handle concerns like:

  • Multi-OS support - Debian, Ubuntu, Alpine

  • Multi-architecture - x86, ARM

  • Idempotence guarantees

  • Permission and path management

  • Package installation and management

  • Bash scripting best practices enforcement

  • VS Code/IDE integration

  • Documentation and implementation alignment

With robust helper libraries, developers could create Dev Containers by composing high-level constructs representing the needed components. The complexity would be handled under the hood.

For example, we could install node.js with:

1devcontainer-helpers install_nodejs {
2 version: "14"
3}

Rather than hundreds of lines of complex Bash scripting!

Initial Exploration

As a starting point, we created an example Dev Container from scratch to build intuition. We initialized a Git repository and added the configuration files to spin up a Ubuntu-based container with Python, pip, and VS Code:

1# Initialize Git repo
2git init
3
4# Add Dev Container files
5devcontainer init
6
7# Open in container
8devcontainer rebuild

To experiment with testing helper functions, we added the BATS testing framework and created a simple test for a check_file_contains function:

1# Helper function
2function check_file_contains() {
3 if ! grep -q "$1" "$2"; then
4 return 1
5 fi
6}
7
8# BATS test
9@test "Check for foo in file" {
10 check_file_contains "foo" "/path/to/file"
11}

We successfully executed the test against our function within the Dev Container environment.

Conclusion

In closing, Dev Containers provide teams with a powerful way to standardize development environments and eliminate issues caused by gaps between environments. However, complexity in implementation details like Bash scripting makes creating custom configurations time-consuming.

Helper libraries that abstract these details and provide easy-to-use functions for common tasks could dramatically improve the authoring experience. Our initial example shows the potential for testing simple utilities as a starting point before building up more advanced helpers.

There is a significant opportunity for tools that bridge the gap between the simplicity of the Dev Container format itself and the underlying mechanics of bringing configurations to life. By raising the level of abstraction, we can help more developers take advantage of this impactful technology.

You can also watch the live stream I did around this exploration: