How to set up a dev container for a Next.js/TypeScript app

Read More

It’s the first day of your new software engineering job, and you’re super excited to make your first pull request to the codebase, but before you can do that, you have to get your local environment up and running. This painstaking process can take you an entire day or week. You will need accounts with multiple cloud providers and access to environment variables, and you will spend considerable time editing outdated documentation. And now, you’ve lost the spark to contribute to the team.

OR

You’re looking for an open source project to contribute to. You follow the documentation, but it’s outdated and hard to follow, so you abandon your pursuit of contributing.

OR

You want to try out a new framework and understand how it works behind the scenes, but to do that, you have to npm install 5 million different things, and you’re not that interested, so you decide to binge-watch HBO’s Succession instead.

These are three very common experiences that happen to software engineers of all levels, whether new to a job or to an open source project. So, what’s the solution?

The software engineering industry acknowledges that the time-to-first-pull-request or time-to-first-contribution can be time-consuming and cumbersome. As a result, the industry created many solutions in-browser sandbox solutions like CodeSandbox and Replit. For larger projects, cloud-based development environments like GitPod and GitHub Codespaces streamline onboarding and collaboration. One major hurdle in using tools like GitHub Codespaces or GitPod is the lack of knowledge on how to configure them to automatically run your projects, which is understandable since these concepts are still relatively new in the software engineering industry. Luckily, you have me, and you found this blog post. Disclaimer: I work at GitHub as a Developer Advocate. However, there was a part of me that felt a little intimidated by the process of setting up a development container. Like, what even is a development container? Should I just copy and paste one? After taking some time to truly understand devcontainers, they feel more approachable for me.

In this blog post, I’ll walk you through setting up a development container for a static Next.js/TypeScript web application so that it can automatically run in GitHub Codespaces.

My new open source project

On a related note, I’m excited to share that I’ve started maintaining my first open source project, the Open Contributions Project, alongside Josh Goldberg. Our goal is to advocate for and explain corporate contributions to open source. It’s a static documentation website built with Next.js and TypeScript. We welcome contributions in design, documentation, and code, particularly from beginners. Check out the repository below.

]

I set up a development container and enabled GitHub Codespaces to streamline the development process for the Open Contributions Project. If you launch the Open Contributions Project in GitHub Codespaces, it will install the required dependencies, run the project, and launch a browser preview of the project. You can try it out here or by clicking the button below!

Open in GitHub Codespaces

What is GitHub Codespaces?

GitHub Codespaces is a cloud-based development environment that’s hosted in the cloud. This helps developers onboard faster, code on any device, and code in a consistent environment. The UI for GitHub Codespaces can resemble your favorite IDE like Visual Studio Code, Visual Studio, Jupyter Notebook, or JetBrains, but it opens in your browser. You can customize your project for GitHub Codespaces by creating config files. These config files create a repeatable codespace configuration for all users of your project. If you don’t set up any custom configuration, your project will use the default built-in GitHub Codespaces configuration.

You can learn more about GitHub Codespaces from the following resources:

What is a development container?

GitHub Codespaces are built on top of virtual machines that utilize development containers, or dev containers, which are essentially Docker containers that offer a comprehensive development environment. You can configure a dev container to provide a uniform developer experience. You can configure a dev container with these three files: devcontainer.json, Dockerfile, and docker-compose.yml. For a small, static Next.js/Typescript project like ours, we only need the devcontainer.json file.

You can learn more about development containers from the following resources:

Understanding Codespace lifecycle properties

In this guide, I use a few Codespace lifecycle properties, so I figured I would give a brief overview of what they are and why we use them. Codespaces lifecycle properties are special properties that are run automatically at specific points during the creation and management of a Codespace environment. You can use them to perform customized tasks at specific points during the lifecycle of the environment.

Here are some commonly used Codespace lifecycle properties:

  • preCreateCommand: runs before a Codespace is created and is used to set up the environment by installing any required software or dependencies.
  • onCreateCommand: runs immediately after the Codespace is created and is used to perform any additional setup tasks that are required.
  • postCreateCommand: runs after the environment is created and is used to perform any final configuration tasks.
  • updateCommand: updates the environment and is typically used to install any new software or dependencies that have been added since the last time the environment was created
  • updateContentCommand: updates the content of the environment and is typically used to update files or data within the environment.
  • postAttachCommand: In this context, “attach” means to connect to an existing and already created Codespace. This command runs after a user attaches to the Codespace and is used to perform any tasks that should be executed each time the Codespace is attached, such as starting a development server.

Steps for building a development container for a Next.js/TypeScript codebase

Step 1: Create a Codespace for your project

Let’s open our projects in GitHub Codespaces by choosing “Code”> “Codespaces”> “Create Codespace on [default branch].”

Shows code popover and Codespaces tab with green button that says Create Codespace

Step 2: Add a pre-made dev container

First, open the command palette. We can access the command palette using either of these options:

  • Pressing this keyboard shortcut combination Shift+Command+P (Mac) OR Ctrl+Shift+P (Windows/Linux)
  • From the Application Menu, click View > Command Palette
  • Pressing F1

Then, in the command palette, choose: “Add dev container configuration files.”

shows command palette with add a dev container option

Step 3: Create a new configuration

Then, we will see two options:

  • Modify your active configuration
  • Create a new configuration

Since we don’t have a configured dev container yet, we want to choose “Create a new configuration.”

Shows command palette with create a new configuration option

Step 4: Choose the Node.js and TypeScript template

This next step leads us to a list of predefined dev containers, so we don’t have to start from scratch! Currently, the “Node.js & TypeScript” dev container is the most suitable start for a Next.js/TypeScript project.

Shows command palette with node and typescript definition

Step 5: Choose your preferred Node.js version

The next screen will prompt us for our preferred Node.js version. I chose 20, but you should choose whatever version works best for your project.

Shows command palette with Node versions

Step 6: Skip the features! Create the dev container

The next step is to add features. In our case, we don’t need any features, so we can press “OK.” Pressing “OK” adds a .devcontainer directory to your project with a devcontainer.json file that we can further customize.

Shows command palette with 0 selected features

Here’s what our initial devcontainer.json file should look like:


// For format details, see https://aka.ms/devcontainer.json. For config options, see the

// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node



    "name": "Node.js & TypeScript",

    // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile

    "image": "mcr.microsoft.com/devcontainers/typescript-node:0-20"

    // Features to add to the dev container. More info: https://containers.dev/features.

    // "features": ,

    // Use 'forwardPorts' to make a list of ports inside the container available locally.

    // "forwardPorts": [],

    // Use 'postCreateCommand' to run commands after the container is created.

    //"postCreateCommand": "yarn install",

    // Configure tool-specific properties.

    // "customizations": ,

    // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.

    // "remoteUser": "root"


Step 7: Automate installing project dependencies

As part of the local setup, I run pnpm install to install my project’s dependencies. pnpm is a package manager for Node.js packages. It is designed to be fast, disk space efficient, and feature-rich. It creates a single instance of each package on a disk and uses symlinks to link to the installed packages in other projects.

Your project might use a different command and package manager for installing project dependencies. Perhaps, your project is using yarn or npm, so you don’t want to copy me verbatim for this step. Instead, you can use my advice in this step as a guiding example! If you’re using npm install or yarn install to install your project dependencies, replace pnpm install with the required command for your project.

Anyways, I wanted to automate this step so that project contributors wouldn’t have to manually run pnpm install, so I used my dev container to automate this. In my devcontainer.json file, I added a property called:

"updateContentCommand": "pnpm install"

This will enable my codespace to install the latest project dependencies. I considered delegating the pnpm install command to a different Codespace lifecycle property, but I wanted the benefits of the updateCommentCommand. The biggest benefit of using this lifecycle property is that instead of installing all dependencies each time the environment is created, it would only install new dependencies that have been added since the last time the environment was created.

Step 8: Automatically run the local development server

Because my project is static and doesn’t have too many complexities, after installing the required dependencies, I need to start my local environment with the command pnpm dev. Again, I don’t want project contributors to manually type this in. I want the project up and running when they open my project in GitHub Codespaces, so I automated this portion in my dev container by adding the following lines to my devcontainer.json file:

"postAttachCommand": "pnpm dev"

This line is saying after EVERYTHING in the codespace is set up, and the user connects to the codespace, start the development server. Please remember that the commands to run your local development server may differ. Maybe you need to use npm run dev or yarn start. Double-check before copying and pasting!

Step 9: Choose your forwarded port

Now that we automated running our local development server in the step above, we can now choose a specific port for Codespaces to forward traffic in the container to a port on your local machine. In the example below, I chose port 3000.

    "forwardPorts": [3000]

As a result, GitHub Codespaces will generate a URL that follows this naming convention:

`https://<your-github-handle>-<a-codespaces-id-which-combines-a-words-and-random-characters>-<your-port-number>.preview.app.github.dev/`

For me, it generated this URL:

https://blackgirlbytes-ubiquitous-couscous-777759vgvpjcrv4q-3000.preview.app.github.dev/

You have the ability to change your port’s visibility! To learn more about forwarded ports, check out the following resources:

Step 10: Get a browser preview

My favorite part of GitHub Codespaces is that you can open a browser preview directly within your codespace by adding the following lines to your devcontainer.json file:


    "portsAttributes": 

        "3000": 

            "label": "Application",

            "onAutoForward": "openPreview"

        

    

Shows Codespace with browser preview

Step 11: Automatically add extensions

We can configure a project to automatically install extensions in GitHub Codespaces. We can do this by right-clicking on any extension and selecting “Add to devcontainer.json.”

Shows option to add extension to devcontainer

I added the following extensions:

  • Code spell checker
  • ES Lint
  • Markdown-lint
  • Remote-containers

After this, it should add the following object to our devcontainer.json file:


"customizations": 

        "vscode": 

            "extensions": [

                "streetsidesoftware.code-spell-checker",

                "dbaeumer.vscode-eslint",

                "esbenp.prettier-vscode",

                "DavidAnson.vscode-markdownlint",

                "ms-vscode-remote.remote-containers"

            ]

        

    

Step 12: Make it synchronous

Using the waitFor property empowers us to have more control over the Codespace lifecycle and the timing of specific commands. For instance, I used the following configuration in my devcontainer.json file:


"waitFor": "onCreateCommand"

This tells the Codespace creation process to wait for the onCreateCommand to complete before moving on to the next step in the Codespaces lifecycle. This ensures the environment is set up before running other commands.

Caveat

I actually don’t have an onCreateCommand in my devcontainer.json file at all, so it doesn’t make sense that I added this line. Perhaps, it might make more sense if I add the line below:


"waitFor": "updateConentCommand"

I’m still learning about GitHub Codespaces extensively, so that was a mistake I made that I’d like to rectify.

Step 13: The final devcontainer.json file

Let’s confirm the final result! In my .devcontainer directory, I have a devcontainer.json with the contents below:

// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node

    "name": "Node.js & TypeScript",
    "image": "mcr.microsoft.com/devcontainers/typescript-node:0-18",
    "waitFor": "onCreateCommand",
    "updateContentCommand": "pnpm install",
    "postAttachCommand": "pnpm dev",
    "customizations": 
        "vscode": 
            "extensions": [
                "streetsidesoftware.code-spell-checker",
                "dbaeumer.vscode-eslint",
                "esbenp.prettier-vscode",
                "DavidAnson.vscode-markdownlint",
                "ms-vscode-remote.remote-containers"
            ]
        
    ,
    "portsAttributes": 
        "3000": 
            "label": "Application",
            "onAutoForward": "openPreview"
        
    ,
    "forwardPorts": [3000]

Step 14: Try it out!

We can test out our changes to ensure that GitHub Codespaces will install the required dependencies, install required extensions, run our local development server, and open a browser preview by fully rebuilding the container.

To rebuild the container, we’ll need to access the command palette. We can access the command palette using either of these options:

  • Pressing this keyboard shortcut combination Shift+Command+P (Mac) OR Ctrl+Shift+P (Windows/Linux)
  • From the Application Menu, click View > Command Palette
  • Pressing F1

Then, in the command palette, choose: “Codespaces: Full Rebuild Container.”

Shows command palette with option to rebuild container

Video walkthrough

I live streamed myself walking through the steps to build a development container and planning this blog post. Check it out!

Share your learnings

If you’ve tried out development containers or picked up any cool tips and tricks for using GitHub Codespaces, comment below!