Local Remote Development with VS Code
Lately I’m experimenting a lot with different technologies and frameworks and less working on a single code base. To prevent my local machine to get bloated by the different dependencies I started using the Remote Development extension pack for VS Code. This was a very pleasant experience I would like to share.
Introduction
In the past, I used virtual machines to isolate different projects and to quickly get back up to speed after replacing my hardware. Initially, I used a full graphical environment, where I would log in to the virtual machine (e.g., using VNC).
Later, at least for some use cases, I discovered the ability to connect my VS Code development environment directly to the workspace folder inside the virtual machine through an SSH connection. This allowed me to work on my projects within the isolated virtual machine environment, without the need to log in to a full graphical interface.
The virtual machine approach helped me maintain separate, consistent development environments for my different projects, even as I replaced the underlying hardware. The ability to integrate VS Code with the virtual machine workspace provided a more seamless and efficient development experience, compared to the initial full graphical login method.
This hybrid approach, utilizing both virtual machines for isolation and VS Code for a familiar development workflow, enabled me to balance the benefits of virtual machine-based environments with the convenience of my preferred code editor.
However, virtual machines also come with an overhead of additional effort.
Local Development
When developing locally you actually have three options: use your local machine, use a virtual machine, or use containers. Each of the options has benefits and drawbacks:
- Developing on the local machine can be fast, but the different dependencies and runtime environments often lead to bloated and difficult-to-maintain situations, especially when different projects have varying version requirements.
- Virtual machines help isolate different projects and can be useful when changing or replacing hardware. They also enable quick onboarding of new developers to a project. However, virtual machines come with additional costs, as you need to maintain each operating system. They can also have a negative impact on hardware performance due to virtualization and require more storage space since a complete operating system is required for every virtual machine.
- Containers provide a great and lightweight way to isolate work environments without the maintenance effort or performance drawbacks of virtual machines. This is why they are a popular choice.
Another option worth mentioning, for the sake of completeness, is working on remote virtual machines or even inside a container in a remote virtual machine. This approach can provide a consistent development environment, independent of the local machine’s hardware or software configuration.
As my main code editor is VS Code, I’ll present the options it supports.
VS Code Extensions
The Remote Development extension pack for VS Code provides a set of powerful tools that allow you to work on projects hosted on remote machines, virtual machines, or in containers.
The key components of the Remote Development extension pack include:
- Remote - SSH: This extension allows you to connect to and work on projects hosted on remote machines over SSH. This is useful for accessing resources or development environments that are not available on your local machine.
- Dev Containers: This extension enables you to develop inside a container, ensuring a consistent and isolated development environment that matches the production setup.
- Remote - Tunnels: Works similar as the SSH extension but instead of using SSH, the connection is tunneled trough a remote VS Code instance running on the remote server. Due to the similarity with the Remote SSH extension, I will not explain it further.
- WSL: This extension allows you to seamlessly access your Windows Subsystem for Linux (WSL) distribution from within VS Code, providing a Linux-based development environment on your Windows machine. Since I am working on a Linux system, I will not dive deeper into this option.
Remote SSH
The Remote SSH extension enables you to open any folder on a remote machine using SSH and take advantage of VS Code’s full feature set. This means you can interact with files and folders anywhere on the remote filesystem as if they were local, without needing the source code on your machine.
VS Code runs commands and extensions directly on the remote machine, providing a local-quality development experience including full IntelliSense, code navigation, debugging, and more. It uses a small VS Code Server on the remote machine to handle commands from the local VS Code, making the experience feel seamless.
Setting it up involves installing VS Code locally, installing the extension, and setting up SSH access to a compatible remote machine (which most likely also means to setup the remote machine itself). You can then connect to the remote machine and open folders through simple VS Code commands.
Dev Containers
Similar to the Remote SSH extension also the Dev Containers extension allows to interact with files and folders that are mounted into a container as if they were local. A small VS Code server runs inside the container and handles commands from your local VS Code.
In addition, the Dev Containers extension copies your local Git config into the container and forwards your SSH agent, which helps make working with Git inside the containers as seamless as possible.
Development Containers
Development Containers, or Dev Containers, are a standardized approach to defining and managing development environments using containerization. While the concept was initially introduced and popularized by the VS Code Remote Development extension pack, the underlying principles and benefits of Dev Containers are applicable across various development tools and workflows.
The core idea behind Development Containers is to encapsulate the entire development environment, including the operating system, runtime, libraries, and tools, within a container. This provides several key advantages:
- Consistent Environments: By defining the development environment in a container, teams can ensure that all developers are working in the same, reproducible setup, regardless of their local machine configurations.
- Isolated Workspaces: The container-based approach creates an isolated workspace, preventing conflicts between project dependencies and the host system. This is particularly useful when working on multiple projects with different runtime requirements.
- Portable Development: Development Containers can be easily shared and reproduced, making it simple to onboard new team members or set up the development environment on a different machine.
- Alignment with Production: By basing the Development Container on the same container images used in production, developers can ensure that the local development environment closely matches the final deployment, reducing the risk of unexpected issues.
While the specific implementation and tooling may vary, the core principles of Development Containers remain the same. Many development tools and cloud-based development environments, now support the use of Dev Containers to provide a consistent, isolated, and portable development experience.
Anatomy of a Simple Project
Setting up a Development Container is like creating a recipe for your perfect development environment. Here’s a practical example of a devcontainer.json
file that showcases the elegance of this approach for a simple nodejs based project:
{
"name": "Node.js Development",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22-bookworm",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
},
"forwardPorts": [3000],
"postCreateCommand": "npm install"
}
With this recipe, we define which VS Code extensions are available, what command would be executed after creating the container image, and which port would be forwarded from the container to the local editor.
The Development Container specification also allows integrating custom “features” that provide additional functionality, such as additional tools or shells:
"features": {
"ghcr.io/jungaretti/features/vim:1": {}
},
Of course, you can build your own features, but there is also a public repository of existing features available.
A Development Container setup is not limited to an existing container image. It also supports providing your own Dockerfile
or applying a more complex container image composition defined by a docker-compose.yml
file. The next paragraphs will describe this approach.
Example: Web Application
Let’s take a classic web application that consists of a TypeScript API, a TypeScript UI, and PostgreSQL, using Bun as the runtime environment. Here’s how you can set it up with Development Containers:
/.devcontainer
/devcontainer.json
/docker-compose.yml
/Dockerfile
/...
The docker-compose.yml
file defines the services that make up your app:
version: "3.8"
services:
app:
build:
context: ../
dockerfile: ./.devcontainer/Dockerfile
volumes:
- ..:/workspace:cached
command: sleep infinity
db:
image: docker.io/postgres:17-alpine
environment:
POSTGRES_DATABASE: mydb
POSTGRES_USER: myuser
POSTGRES_PASSWORD: b88ba0768b514a619d92
network_mode: service:app
This sets up an app
service built from the Dockerfile in the .devcontainer
folder and a db
service using the official PostgreSQL image. The app’s working directory is mounted as a volume in the container.
The devcontainer.json
references the docker-compose.yml
and specifies which container is the main workspace container:
{
"name": "webapp",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"forwardPorts": [
8091,
8090,
"db:5432"
],
"customizations": {
"vscode": {
"extensions": [
"ms-azuretools.vscode-docker",
"EditorConfig.EditorConfig",
"dbaeumer.vscode-eslint",
"ZainChen.json",
"esbenp.prettier-vscode",
"mikestead.dotenv",
"mtxr.sqltools",
"mtxr.sqltools-driver-pg",
]
}
},
"features": {
"ghcr.io/jungaretti/features/vim:1": {},
"ghcr.io/audacioustux/devcontainers/bun:1": {}
},
"postCreateCommand": "bun install",
"postStartCommand": "nohup bash -c 'screen -dmS api bun watch:api &' > nohup.api; nohup bash -c 'screen -dmS ui bun watch:ui &' > nohup.ui",
"postAttachCommand": "bash -c 'screen -wipe'"
}
To add support for the screen
command, we need to install it in our Dockerfile
:
FROM mcr.microsoft.com/devcontainers/base:ubuntu
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive && \
apt-get -y install --no-install-recommends screen
With the devcontainer.json
and docker-compose.yml
in place, you can open the project in the Development Container using the “Dev Containers: Reopen in Container” command in VS Code.
VS Code will build the image and start the containers as defined in the configuration files. The postCreateCommand
will ensure dependencies are installed and the postStartCommand
will start the API and UI processes (in watch mode).
You can access the running processes using screen -r api
or screen -r ui
. The postAttachCommand
ensures any stale screen sessions are cleaned up.
Outlook
Development Containers are rapidly gaining adoption and support across the developer tool ecosystem. While Visual Studio Code popularized the concept, many other editors, IDEs, and platforms are now embracing the Development Containers specification.
Zed, a modern text editor built in Rust, has an open issue tracking the implementation of Development Container support. Users are actively discussing the best approaches to integrate the feature, taking into account the Zed architecture and extension system. There is strong community demand for this capability to enable Zed as a full-featured development environment.
JetBrains has also recently added support for devcontainer.json in their IDEs like IntelliJ and Rider. While there may be some initial deviation from the spec, it demonstrates the industry-wide shift towards standardizing development environments using containers.
On the cloud side, GitHub Codespaces, a popular cloud development environment, is built entirely around the concept of Development Containers. Each codespace is defined by a devcontainer.json file in the repository, allowing developers to spin up a pre-configured environment accessible from any device with a browser.
Other tools like DevPod provide a graphical interface for managing Development Containers, making it easier for developers to adopt the workflow without being tied to a specific editor. The containers.dev project maintains a CLI reference implementation of the Development Container spec, enabling the integration into any editor or tool.
Even in the Neovim community, there are active efforts to add Development Container support to the popular terminal-based text editor. This would bring the benefits of containerized development environments to those who prefer a keyboard-centric workflow.
As the Development Containers specification matures and stabilizes, we can expect to see even broader adoption across the industry. The benefits of consistent, reproducible environments are too significant to ignore, especially as software projects become increasingly complex with diverse dependencies.
Summary
Using Remote Development extensions and Development Containers provides a more lightweight, portable and consistent way to isolate project environments compared to virtual machines. It enables a seamless remote development workflow while avoiding the maintenance overhead and performance issues of full VMs.
By embracing VS Code’s remote development capabilities and dev containers, developers can focus on what matters most - writing code - while leaving the complexity of environment management behind. It’s like having a personal assistant who ensures your workspace is always perfectly set up, no matter where you choose to work.
References
- https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack
- https://code.visualstudio.com/docs/remote/remote-overview
- https://containers.dev/
- https://containers.dev/features
- https://bun.sh/
- https://zed.dev/
- https://blog.jetbrains.com/dotnet/2023/03/22/remote-development-with-jetbrains-rider/
- https://github.com/features/codespaces
- https://devpod.sh/
- https://github.com/amitds1997/remote-nvim.nvim