How to set up a dev container for a Next.js/TypeScript app
Open your Next.js app in Codespaces
Table of contents
- My new open source project
- What is GitHub Codespaces?
- What is a development container?
- Understanding Codespace lifecycle properties
- Steps for building a development container for a Next.js/TypeScript codebase
- Step 1: Create a Codespace for your project
- Step 2: Add a pre-made dev container
- Step 3: Create a new configuration
- Step 4: Choose the Node.js and TypeScript template
- Step 5: Choose your preferred Node.js version
- Step 6: Skip the features! Create the dev container
- Step 7: Automate installing project dependencies
- Step 8: Automatically run the local development server
- Step 9: Choose your forwarded port
- Step 10: Get a browser preview
- Step 11: Automatically add extensions
- Step 12: Make it synchronous
- Step 13: The final devcontainer.json file
- Step 14: Try it out!
- Video walkthrough
- Share your learnings
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!
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:
Overview and Getting Started with GitHub Codespaces - GitHub documentation
A beginner's guide to learning to code with GitHub Codepsaces
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:
How to automate your dev environment with dev containers and GitHub Codespaces
How I used dev containers to enable GitHub Codespaces for ChatGPT
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]."
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."
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."
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.
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.
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.
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"
}
}
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."
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."
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!