Containers are everywhere. It's not just to make both the development and production environment consistent but also to build CI/CD pipelines, test automations, and so on. While everyone does their own way of building containers, it has become the maintenance job.
Maintenance will become easier if it uses standardised ways. What if there is an open standard spec for it? Actually, there is. It is called Development Container or DevContainer. You can build the container and store it in your GitHub repository with this spec. Then, GitHub Codespaces makes use of it. At least a project using the same GitHub repository uses the same development environment. Throughout this post, I'm going to introduce the DevContainer spec and discuss how it's used for Azure and .NET app development.
You can download the DevContainer template from this GitHub repository, which is used for this post.
This is the template repository that contains the devcontainer settings for .NET app development
devcontainers for .NET
This is the template repository that contains the devcontainer settings for .NET app development.
Getting Started
If you want to use this devcontainer settings, you can create a new repository with this template repository, by clicking the "Use this template" button.
Options – devcontainer.json
Base Image
By default, this devcontainer settings uses the base image of Ubuntu 22.04 LTS (jammy).
However, there is currently a bug on the C# extension v1.25.0 for Razor support on Ubuntu 22.04 LTS (jammy). Therefore, if you need Razor support, build your devcontainer with Ubuntu 20.04 LTS (focal).
"build": {"dockerfile": "./Dockerfile","context": ".","args": {// Use this only if you need Razor support, until OmniSharp supports .NET 6 properly"VARIANT": "6.0-focal"}
All you need for building a DevContainer are those two files:
devcontainer.json: It defines all the metadata details to build a DevContainer.
Dockerfile: It defines the Docker container image that is invoked within devcontainer.json.
Optionally, you can call a shell script during the container building lifecycle. The postCreateCommand attribute of the devcontainer.json file takes care of it. For example, the attribute executes the post-create.sh file.
Alternatively, you can invoke the RUN command within Dockerfile for the shell script execution. In this case, you're running the script with the root permissions. On the other hand, postCreateCommand runs the script with the user privileges declared in devcontainer.json.
Structure of devcontainer.json
devcontainer.json consists of various sections for metadata declarations. I only deal with a small portion of it in this post.
build: This section is for the Docker container build – location of Dockerfile, parameters passed to it.
forwardPorts: This section defines the list of port numbers to expose. For example, 7071 is for Azure Functions, 5000 and 5001 are for ASP.NET Web/API apps, and 4280 is for Azure Static Web App development.
features: While you can add everything in Dockerfile for the build, there are already pre-configured features you can optionally add. You can find the complete list of the features at here. Some examples of those features are common utilities and tools like Azure CLI, GitHub CLI and Terraform, and languages like node.js, Java, .NET, Python, etc.
customisations: This section is responsible for the tools used in DevContainer. One of the tools is VS Code which the vscode attribute represents. It defines which extensions need to be included (extensions) and how the editor behaves (settings).
remoteUser: This sets the user account within DevContainer. Unless it is set, DevContainer runs as the root account. In this post, we use vscode as the non-root account.
postCreateCommand: Once DevContainer is created, run additional commands with the remote-user account privileges. Through this attribute, you can run an additional shell script.
There are more attributes than the ones mentioned above. If you want to know more, visit this page.
Order of Building DevContainer
How does DevContainer get built? Here is the rough order.
Build the Docker container. If you add the shell script through the RUN command in Dockerfile, the shell script is run this time.
Run features declared in the features section of devcontainer.json while building the Docker container.
Run commands declared in the postCreateCommand attribute of devcontainer.json.
Apply dotfiles after postCreateCommand, if you have it.
Apply both extensions and settings of devcontainer.json at the startup of the DevContainer.
By understanding the order above, let's build the DevContainer for .NET app development on Azure.
Base Container Image
When you visit the repository for DevContainer images, there are pre-configured list of base Docker container images for DevContainer. Let's choose the one for .NET. You can choose many different variants, but either 6.0-jammy (Ubuntu 22.04) or 6.0-focal (Ubuntu 20.04) would be yours if you prefer using Ubuntu-based images with .NET 6. Ubuntu 22.04 is set to the default base image in this post.
Let's configure devcontainer.json containing all the metadata details.
The build Section
It declares the location of Dockerfile and sends parameters to it. In this case, use 6.0-jammy for VARIANT.
"build":{"dockerfile":"./Dockerfile","context":".","args":{"VARIANT":"6.0-jammy"// Use this only if you need Razor support, until OmniSharp supports .NET 6 properly// "VARIANT": "6.0-focal"}}
If you are building a Blazor app, pass 6.0-focal (Ubuntu 20.04) instead of 6.0-jammy (Ubuntu 22.04). It's because the C# extension has a bug on Ubuntu 22.04.
The forwardPorts Section
You might need to expose some specific port numbers – 7071 for Azure Functions, 5000 and 5001 for ASP.NET Web/API apps, and/or 4280 for Azure Static Web Apps.
"forwardPorts":[// Azure Functions7071,// ASP.NET Core Web/API App, Blazor App5000,5001,// Azure Static Web App CLI4280]
The features Section
On top of the base image, if you want to add more tools and/or languages, add them to the features section.
Common Utils: You can add zsh and oh-my-zsh through this feature.
"features":{// Install common utilities"ghcr.io/devcontainers/features/common-utils:1":{"installZsh":true,"installOhMyZsh":true,"upgradePackages":true,"username":"vscode","uid":"1000","gid":"1000"}}
Azure CLI: You can add Azure CLI through this feature.
"features":{// Uncomment the below to install Azure CLI"ghcr.io/devcontainers/features/azure-cli:1":{"version":"latest"}}
GitHub CLI: You can add GitHub CLI through this feature.
"features":{// Uncomment the below to install GitHub CLI"ghcr.io/devcontainers/features/github-cli:1":{"version":"latest"}}
node.js: You can add the latest LTS version of node.js through this feature.
"features":{// Uncomment the below to install node.js"ghcr.io/devcontainers/features/node:1":{"version":"lts","nodeGypDependencies":true,"nvmInstallPath":"/usr/local/share/nvm"}}
Suppose you have another feature, but it doesn't exist yet in this features list. In that case, you can manually add it through the postCreateCommand attribute by invoking post-create.sh.
Do you want to add those features in order? Then use this overrideFeatureInstallOrder attribute. Here in this post, the common-utils feature runs first, and then the rest features are installed in random order.
You might have some extensions automatically installed while creating the DevContainer. The customisations.vscode.extensions section holds all the extensions you want to install. The list of extensions below is for Azure and .NET app development. If you want to add more, search them on Visual Studio Code Marketplace and add their extension ID. For example, the C# extension has its extension ID of ms-dotnettools.csharp.
You might also want to personalise your editor settings while creating the DevContainer. You can set them on the customizations.vscode.settings section. The list of settings below are examples of personalised settings. If you want to get them more personalised, refer to the User and Workspace Settings page.
The bash shell is the default terminal. If you want to change its default behaviour to zsh, use these settings.
"customizations":{"vscode":{"settings":{// Uncomment if you want to use zsh as the default shell"terminal.integrated.defaultProfile.linux":"zsh","terminal.integrated.profiles.linux":{"zsh":{"path":"/usr/bin/zsh"}}}}}
If you want to change your terminal font, use this setting. It's specifically for oh-my-zsh or oh-my-posh.
"customizations":{"vscode":{"settings":{// Uncomment if you want to use CaskaydiaCove Nerd Font as the default terminal font"terminal.integrated.fontFamily":"CaskaydiaCove Nerd Font"}}}
If you want to disable the minimap feature, use this setting.
"customizations":{"vscode":{"settings":{// Uncomment if you want to disable the minimap view"editor.minimap.enabled":false}}}
If you want to change the behaviour of the explorer, use these settings. All files are sorted by extension and nested by relevant files.
"customizations":{"vscode":{"settings":{// Recommended settings for the explorer pane"explorer.sortOrder":"type","explorer.fileNesting.enabled":true,"explorer.fileNesting.patterns":{"*.bicep":"${capture}.json","*.razor":"${capture}.razor.css","*.js":"${capture}.js.map"}}}}
The postCreateCommand Section
Once your DevContainer is created, you might want to do something more. For example, you can't add an extra feature through the features section because it's not ready. However, executing a shell script can add those additional features through this postCreateCommand section.
Here's the one for running post-create.sh through the bash shell.
Let's take a look at what happens within post-create.sh.
post-create.sh Execution
This post-create.sh is run right after the DevContainer is created.
CaskaydiaCove Nerd Font
It's a good idea to install a custom font, if you use either oh-my-zsh or oh-my-posh for your terminal. The following script is to install CaskaydiaCove Nerd Font.
## CaskaydiaCove Nerd Font# Uncomment the below to install the CaskaydiaCove Nerd Fontmkdir$HOME/.localmkdir$HOME/.local/sharemkdir$HOME/.local/share/fontswgethttps://github.com/ryanoasis/nerd-fonts/releases/latest/download/CascadiaCode.zipunzipCascadiaCode.zip-d$HOME/.local/share/fontsrmCascadiaCode.zip
Azure CLI Extensions
You can run this script if you install Azure CLI through the feature section of devcontainer.json. However, because it adds ALL extensions, it takes about 30-60 mins, depending on your network latency. Therefore be careful to use.
## AZURE CLI EXTENSIONS ### Uncomment the below to install Azure CLI extensionsextensions=$(azextensionlist-available--query"[].name"|jq-c-r'.[]')forextensionin$extensions;doazextensionadd--name$extensiondone
You can use extensions=(list of selected extensions you want), instead of using extensions=$(az extension list-available --query "[].name" | jq -c -r '.[]'). For example, I use extensions=(account alias deploy-to-azure functionapp subscription webapp).
Azure Bicep CLI
If you use Azure Bicep, you can run this script to install Bicep CLI.
## AZURE BICEP CLI ### Uncomment the below to install Azure Bicep CLIazbicepinstall
Azure Functions Core Tools
Do you develop Azure Functions apps? Then activate this script. As it installs through npm, you should install the node.js feature through the features section of devcontainer.json.
## AZURE FUNCTIONS CORE TOOLS ### Uncomment the below to install Azure Functions Core Toolsnpmi-gazure-functions-core-tools@4--unsafe-permtrue
Azure Static Web Apps CLI
Unlock this script if you're building a Blazor WASM app and deploying it to Azure Static Web Apps. Like Azure Functions Core Tools, it relies on the node.js feature through the features section of devcontainer.json.
## AZURE STATIC WEB APPS CLI ### Uncomment the below to install Azure Static Web Apps CLInpminstall-g@azure/static-web-apps-cli
Azure Dev CLI
If you want to activate the Azure Dev CLI, run the following script. Azure Dev CLI needs both Azure CLI and GitHub CLI as dependencies. Therefore, make sure you have already installed them through the features section of devcontainer.json.
## AZURE DEV CLI ### Uncomment the below to install Azure Dev CLIcurl-fsSLhttps://aka.ms/install-azd.sh|bash
oh-my-zsh – Plugins and Themes
There are a bunch of plugins and themes for oh-my-zsh. You can follow the pattern below. Here are some recommended plugins and theme – powerlevel10k.
## OH-MY-ZSH PLUGINS & THEMES (POWERLEVEL10K) ### Uncomment the below to install oh-my-zsh plugins and themes (powerlevel10k) without dotfiles integrationgitclonehttps://github.com/zsh-users/zsh-completions.git$HOME/.oh-my-zsh/custom/plugins/zsh-completionsgitclonehttps://github.com/zsh-users/zsh-syntax-highlighting.git$HOME/.oh-my-zsh/custom/plugins/zsh-syntax-highlightinggitclonehttps://github.com/zsh-users/zsh-autosuggestions.git$HOME/.oh-my-zsh/custom/plugins/zsh-autosuggestionsgitclonehttps://github.com/romkatv/powerlevel10k.git$HOME/.oh-my-zsh/custom/themes/powerlevel10k--depth=1ln-s$HOME/.oh-my-zsh/custom/themes/powerlevel10k/powerlevel10k.zsh-theme$HOME/.oh-my-zsh/custom/themes/powerlevel10k.zsh-theme
oh-my-zsh – powerlevel10k Theme Configurations
powerlevel10k has its settings file, called p10k.zsh. I've got my own p10k.zsh settings – with a clock and without the clock. Activate the script below to copy them to DevContainer. You don't need to run the following script if you have your own ones from your dotfiles repository.
## OH-MY-ZSH - POWERLEVEL10K SETTINGS ### Uncomment the below to update the oh-my-zsh settings without dotfiles integrationcurlhttps://raw.githubusercontent.com/justinyoo/devcontainers-dotnet/main/oh-my-zsh/.p10k-with-clock.zsh>$HOME/.p10k-with-clock.zshcurlhttps://raw.githubusercontent.com/justinyoo/devcontainers-dotnet/main/oh-my-zsh/.p10k-without-clock.zsh>$HOME/.p10k-without-clock.zshcurlhttps://raw.githubusercontent.com/justinyoo/devcontainers-dotnet/main/oh-my-zsh/switch-p10k-clock.sh>$HOME/switch-p10k-clock.shchmod+x~/switch-p10k-clock.shcp$HOME/.p10k-with-clock.zsh$HOME/.p10k.zshcp$HOME/.zshrc$HOME/.zshrc.bakecho"$(cat$HOME/.zshrc)"|awk'{gsub(/ZSH_THEME=\"codespaces\"/, "ZSH_THEME=\"powerlevel10k\"")}1'>$HOME/.zshrc.replaced&&mv$HOME/.zshrc.replaced$HOME/.zshrcecho"$(cat$HOME/.zshrc)"|awk'{gsub(/plugins=\(git\)/, "plugins=(git zsh-completions zsh-syntax-highlighting zsh-autosuggestions)")}1'>$HOME/.zshrc.replaced&&mv$HOME/.zshrc.replaced$HOME/.zshrcecho"
# To customize prompt, run 'p10k configure' or edit ~/.p10k.zsh.
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh
">>$HOME/.zshrc
oh-my-posh – Installation
Are you into PowerShell but also want to use something like oh-my-zsh? Then oh-my-posh is your friend. Run the following script to install.
## OH-MY-POSH ### Uncomment the below to install oh-my-poshsudowgethttps://github.com/JanDeDobbeleer/oh-my-posh/releases/latest/download/posh-linux-amd64-O/usr/local/bin/oh-my-poshsudochmod+x/usr/local/bin/oh-my-posh
oh-my-posh – Configurations
oh-my-posh has its own powerlevel10k configurations. Run the following script to copy those settings files to your DevContainer. Again, if you've set them on your dotfiles repository, you don't need to run the script below.
## OH-MY-POSH - POWERLEVEL10K SETTINGS ### Uncomment the below to update the oh-my-posh settings without dotfiles integrationcurlhttps://raw.githubusercontent.com/justinyoo/devcontainers-dotnet/main/oh-my-posh/p10k-with-clock.omp.json>$HOME/p10k-with-clock.omp.jsoncurlhttps://raw.githubusercontent.com/justinyoo/devcontainers-dotnet/main/oh-my-posh/p10k-without-clock.omp.json>$HOME/p10k-without-clock.omp.jsoncurlhttps://raw.githubusercontent.com/justinyoo/devcontainers-dotnet/main/oh-my-posh/switch-p10k-clock.ps1>$HOME/switch-p10k-clock.ps1mkdir$HOME/.config/powershellcurlhttps://raw.githubusercontent.com/justinyoo/devcontainers-dotnet/main/oh-my-posh/Microsoft.PowerShell_profile.ps1>$HOME/.config/powershell/Microsoft.PowerShell_profile.ps1cp$HOME/p10k-with-clock.omp.json$HOME/p10k.omp.json
Once you complete the configuration like above, run your GitHub Codespaces instance. Then, you will see the terminal like below. On the left-hand side, it's the zsh shell applying oh-my-zsh. On the right-hand side, it's PowerShell using oh-my-posh.
So far, we've discussed what the DevContainer is, how it is helpful for .NET app development on Azure, and what needs to be configured for DevContainer for .NET app development on Azure, using GitHub Codespaces. What has been discussed here in this post is the only small part of the DevContainer. Therefore, if you want to apply it to your or your team's development environment, you need to do more research by yourself. However, I hope this post gives at least some sort of insights for building DevContainers.
Want to know more about DevContainers?
There are documents and learning materials for you: