IBM Developer

Tutorial

Create portable dev environments with Dev Containers

Learn how to set up and use Dev Containers for a consistent and portable development environment

When developing software, the first step before writing any code is to set up your development environment. Depending on your team, you may have multiple developers with different devices, operating systems, and versions of software already installed on their machines. This variation can make it challenging to keep your team’s environments in sync, especially if you are managing multiple applications using a variety of different languages and technologies. Dev Containers help create a uniform development environment that can be shared and managed via source control.

What is containerized development?

Containerized development involves developing your software applications entirely inside a container or a set of containers. This usually involves creating a shared volume between your host file system and the container so that changes you make to files persist on your host machine. If you are unfamiliar with container technology or how it works, check out Docker’s explanation, Use containers to Build, Share and Run your applications.

In this article, Dev Containers refers to the Visual Studio Code extension that helps you manage developing inside containers. For a more detailed explanation of containerized development and the features provided by the VS Code Dev Containers extension, check out their Developing inside a container guide.

Why develop inside containers?

Dev Containers offer several advantages:

  • Allow you and your team to share the same development environment and VS Code extensions.
  • Use the same container base images that your applications run on in production.
  • Help keep installed software on your machine to a minimum.
  • Eliminate the need to manage multiple versions of languages and dependencies using software like nvm or pyenv.
  • Make it easier to switch contexts when maintaining a suite of applications that use different technologies.
  • Reduce the time to onboard new developers to your projects.

Setting Up Your Project with Dev Containers

Prerequisites

Before starting this tutorial, ensure you have a basic understanding of containers and have the following software installed:

Steps

Step 1. Install the Visual Studio Code dev container extension and create a project directory

  1. Install the Dev Containers extension:

    • Open Visual Studio Code.
    • Click on the Extensions tab.
    • Search for the "Dev Containers" extension and install it.

    dev-containers-extension.png

Step 2. Configure the extension

If you are using Podman or another containerization software besides Docker, you may need to make some slight changes to the Dev Container configuration settings.

  1. Go to the Extensions tab in VS Code and click on the extension settings for the newly installed Dev Containers extension.

    extension-settings.png

  2. Scroll to the Docker Compose Path and Docker Path fields.

    configure-dev-containers.png

  3. Enter the path to your Podman and Podman Compose executables.

Step 3. Create Dev Container configuration files

  1. Open the Command Palette:

    • In VS Code, type Ctrl+Shift+P (Windows/Linux) or Cmd+Shift+P (Mac) to open the Command Palette.

    • Search for Add Dev Container Configuration Files.

    add-configuration-files.png

  2. Add Configuration:

    • Hit Enter and click Add Configuration to Workspace.

      add-configuration-to-workspace.png

    • Type Node.js & PostgreSQL and hit Enter again.

      add-node-postgres.png

    • Select Node 20 Bookworm (latest stable base image) and hit Enter one more time.

  3. This will create a new .devcontainer folder in your project with the following files:

    • devcontainer.json: A configuration file used by VS Code to help manage your dev container. You can find all the possible configuration values at Specification reference.

    • docker-compose.yml: A Docker Compose file that will be used to build out your development container stack. This includes the container you will be developing in and any supporting services like databases, caches, search engines, etc. For more information, see Docker Compose overview.

    • Dockerfile: The Dockerfile used to build your dev container image.

Step 4. Running the Dev Container

  1. Rebuild and Reopen in Container:

    • Pull up the Command Palette (Ctrl+Shift+P on Windows/Linux or Cmd+Shift+P on Mac).
    • Type Rebuild and Reopen in Container and hit Enter.

      rebuild-and-reopen-in-container.png

  2. Build and Spin Up Containers: This will begin to build your container image from your Dockerfile and spin up any other containers in your Docker Compose stack.

    You can find more information about Dev Container Command Palette and CLI commands at Dev Container CLI.

Step 5. Running commands in the Dev Container

Once the container spins up, you can begin developing in it.

  1. Open a Terminal:

    • Click the + button on the bottom right-hand side of your code editor or use Ctrl + backtick (`) to open a terminal.
  2. This terminal will execute commands directly inside your container, not on your host machine. You will only have access to commands and programs installed via your Dockerfile.

    If you need to run commands on your host machine, see the tips section below.

Step 6. Developing in the Container

Now that we have a working dev container, let's create a test application.

  1. Create a New Application: In your newly spawned terminal, run the command:

      npx create-next-app@latest
    

    This will guide you through the steps of creating a new Next.js application and installing any necessary dependencies.

  2. To navigate to your project folder, run

      cd <your-project-folder>
    
  3. To start your application, run:

      npm run dev
    

    This will spin up your Next.js app.

  4. Navigate to http://localhost:3000 to see your running application.

Step 7. Connect to Your Postgres Database

  1. In your docker-compose.yml file, add the following lines to your db service:

     expose:
     - 5432
     ports:
     - 5432:5432
    

    These lines will expose the port the database is running on and map it to your host machine.

  2. Rebuild the Container:

    • Open the Command Palette (Ctrl+Shift+P on Windows/Linux or Cmd+Shift+P on Mac).

    • Run the Rebuild Container command.

      rebuild-container.png

    • Anytime you make changes to one of the configuration files inside the .devcontainer directory, you will need to rebuild the container for the changes to take effect.

  3. Connect to the Database:

    • Open your preferred PostgreSQL database client.
    • Connect to the database running on localhost:5432.
    • The username, password, and database should all be set to postgres.

      database-config.png

  4. You can also set your own values in the docker-compose.yml file generated by VS Code.

     db:
     environment:
       POSTGRES_PASSWORD: postgres
       POSTGRES_USER: postgres
       POSTGRES_DB: postgres
    

    Congratulations! You have just set up your very first containerized development environment.

Useful Dev Container tips

Here are some helpful tips that can make developing inside dev containers a little easier:

Increase container resource allocation

Since you will be doing all of your development work inside your containers, it might be a good idea to increase the amount of CPU cores, memory, and storage allocated to them. You can do this through your containerization software's GUI or CLI:

Docker desktop (GUI):

preferences > advanced

docker-resources.png

Rancher (GUI):

preferences > virtual machine

rancher-resources.png

Podman (CLI):

podman machine stop
podman machine set -cpus=x --memory=y --disk-size=z
podman machine start

Add a post create command

To automate your dev environment’s build step, you can create a post-create command in your devcontainer.json. This command will run as soon as the dev container is created for the first time. It is a great place to install dependencies through a package manager like pip, npm, or yarn.

Example:

"postCreateCommand": "npm install",

Speed up NPM installations

If you are experiencing slow NPM installations, it is recommended to create a separate targeted named volume to store your node modules. This ensures that the node modules you install do not persist in the same location on your host machine.

Example:

app:
    volumes:
        - app-node-modules:/workspaces/app/client/node_modules

volumes:
    app-node-modules

To delete the contents of the installed node modules, simply delete the volume:

 docker volume rm app-node-modules

For more information, refer to the Visual Studio Code documentation on improving container performance.

Slow jest test cases and unexpected crashes

If your Jest test cases are running slower than usual inside the container or are causing your containers to crash, try the following solutions:

  • Run Tests In-Band:

      jest --runInBand
    
  • Manually Set the Max Number of Workers:

      jest --maxWorkers=4
    

For more details, refer to this Stack Overflow discussion on running Jest tests in Docker.

Webpack file watcher limit exceeded

When developing inside a container using webpack dev server or a similar program that monitors files in your project for changes, you might encounter the following error:

Error: ENOSPC: System limit for number of file watchers reached

To fix this error, you can try the following solutions:

  1. Increase the file watcher Limit:

     echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
    
  2. Ignore the node_modules Folder in your webpack.config.js file:

     watchOptions: {
     ignored: /node_modules/,
     }
    

The node_modules folder could have thousands of files that are not likely to change during the course of development.

For more details, refer to this Stack Overflow discussion on system limits for file watchers.

Container networking

If you want your dev container to communicate with other containers defined in your docker-compose.yml, you may want to set up container networking. When using container networking, your service’s hostnames will be their container names.

Example configuration in docker-compose.yml:

    services:
        app:
            container-name: app
            networks:
                - devnetwork
        db:
            container-name: app-db
            networks:
                - devnetwork

    networks:
      devnetwork:
        name: devnetwork
        driver: bridge

In this setup, the app and db containers can communicate with each other using their respective container names (app and app-db).

Spawn a terminal on your host machine

Sometimes you will want to spawn a terminal on your host machine instead of inside your dev container. You can easily do this in VS Code by opening the command palette (ctrl+shift+p or cmd+shift+p) and searching for Create New Integrated Terminal (local).

Conclusion

After completing this tutorial, you should see the benefits of developing inside containers and consider adopting containerized development for your own projects. This approach provides a portable development environment that you can share with your team via source control, running on most operating systems with minimal setup.