Develop ASP.NET Core web apps faster with Dev Container
- 7 minutes read - 1453 wordsDeveloping ASP.NET Core web apps with MS SQL Server databases on macOS is not very straightforward, as I’ve blogged about before. There’s lots of custom scripts needed and it’s complicated for a new developer to get started in a project like this. My current project Gig Local had this very problem, so I decided enough was enough, and went all in with Dev Containers to make local development easier.
In this post, I’ll walk through how to configure a Dev Container to develop an ASP.NET Core web app with an MS SQL Server database on macOS, Linux or Windows!
Dev Container
A Dev Container allows a project’s dependencies and dev tooling to be abstracted away via Docker the Remote Development VS Code extension pack. This significantly reduces the time for a new developer to get started, whilst also reducing the chance of incorrect installations or getting stuck during installation.
A Dev Container is defined by a devcontainer.json
file that must be inside a .devcontainer
folder in the root of your VS Code workspace. Within this file, you define the required services, as well as configuration for your specific application. Once the container is built, you can open VS Code inside the container, just as if you were developing locally! This saves a tonne of time as the installation is automated and everything the app needs is in the container!
There’s lots to know about Dev Container, but the easiest way to get up and running is to open an existing folder in a container and choose one of the pre-defined templates. I got going with this project by choosing the C# (.NET) and MS SQL
template:
Selecting this template creates a bunch of files in a .devcontainer
folder which I then edited to suit my project’s needs.
Pre-building the web app dev container
To reduce the need to build images locally, it’s possible to pre-build your dev container images and store them in a docker registry. This can be achieved with a simple CI/CD pipeline to automatically build and deploy the images. I loved the sound of this so I created a new repository called GigLocal/devcontainer where all Dev Containers images related to the project are now stored.
devcontainer.json
Pre-building dev containers is done via the devcontainer CLI, and also requires a devcontainer.json
. For the web app image, this is as simple as:
{
"build": {
"dockerfile": "Dockerfile",
"args": {
"vscode": "true"
}
}
}
This tell the devcontainer CLI
to build an image from the Dockerfile
and that it should only be used in VS Code.
Dockerfile for the web app
The web app is a monolith whose environment is defined by the Dockerfile
from the template:
# [Choice] .NET version: 6.0, 5.0, 3.1
ARG VARIANT="6.0"
FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT}-focal
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# Install SQL Tools: SQLPackage and sqlcmd
COPY installSQLtools.sh installSQLtools.sh
RUN bash ./installSQLtools.sh \
&& apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts
# Install dotnet tools
RUN dotnet tool install -g dotnet-ef
RUN dotnet tool install -g Microsoft.Web.LibraryManager.Cli
ENV PATH $PATH:/root/.dotnet/tools
This uses the Microsoft .NET 6 devcontainer image as a base, and adds in some additional tooling for SQL Server, all from the template. I just added in the last three lines to install dotnet-ef
for managing EF Migrations, and libman
for managing client-side libraries. I have also turned off the Node.js installation as I’m not using Node in this project.
CI/CD
I then added a GitHub Action to build and deploy the web app image to GitHub Container Registry once a month or whenever the main
branch is pushed to:
name: Generate Dev Container Images
on:
schedule:
- cron: '0 0 1 * *'
push:
branches:
- 'main'
paths:
- '*/.devcontainer/**/*'
permissions:
contents: read
packages: write
jobs:
website:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: |
set -e
# Update this based on your image name and the path of the .devcontainer folder in your repository
FOLDER_WITH_DOT_DEVCONTAINER="website"
IMAGE_NAME="website-devcontainer"
IMAGE_REPOSITORY="$(echo "ghcr.io/${{ github.repository_owner }}/${IMAGE_NAME}" | tr '[:upper:]' '[:lower:]')"
# [Optional] Enable buildkit, set output to plain text for logging
export DOCKER_BUILDKIT=1
export BUILDKIT_PROGRESS=plain
# Do the build - update
npm install -g "@vscode/dev-container-cli"
devcontainer build --no-cache --image-name "${IMAGE_REPOSITORY}" "${FOLDER_WITH_DOT_DEVCONTAINER}"
# Push image to GitHub Container Registry
echo "${{ github.token }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
docker push "${IMAGE_REPOSITORY}"
This is based on the template from the VS Code documentation.
You can see the built web app image in Github Container Registry, ready for use in the main Dev Container.
The main Dev Container
Now that the web app image is pre-built, it can now be referenced in the main Dev Container definition for this project.
docker-compose
Since we’re running a web app and a database, the template comes with a docker-compose.yml
file to coordinate both of these services:
version: '3'
services:
app:
image: ghcr.io/giglocal/website-devcontainer
volumes:
- ..:/workspace:cached
- ${HOME}/.aspnet/https:/home/vscode/.aspnet/https:cached
- ${HOME}/.microsoft/usersecrets:/root/.microsoft/usersecrets:ro
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
network_mode: service:db
db:
image: mcr.microsoft.com/mssql/server:2019-latest
restart: unless-stopped
environment:
SA_PASSWORD: P@ssw0rd
ACCEPT_EULA: Y
This is all largely from the template, except I have added the following:
app:image
is targeting our pre-built dev container- The last 2 volumes to mount the ASP.NET Core self-signed certificate and the
dotnet user-secrets
from the local system
postCreateCommand
Dev Container has a facility, postCreateCommand
, to run commands after the container is built. This perfectly suited for restoring client-side libraries and setting up the EF database. For this, I defined a postCreate.sh
scrpt inside .devcontainer
folder:
#!/bin/bash
bash .devcontainer/mssql/postCreateCommand.sh 'P@ssw0rd' './bin/Debug/' './.devcontainer/mssql/'
cd src
libman restore
dotnet ef database update
This runs commands to initialise the SQL Server database, restore libman
libraries, and updates the EF database to the latest migration.
devcontainer.json
Everything is now in place for the main Dev Container. It’s devcontainer.json
is defined as:
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/dotnet-mssql
{
"name": "Gig Local Website",
"service": "app",
"dockerComposeFile": "docker-compose.yml",
"workspaceFolder": "/workspace",
// Set *default* container specific settings.json values on container create.
"settings": {
"mssql.connections": [
{
"server": "localhost,1433",
"database": "",
"authenticationType": "SqlLogin",
"user": "sa",
"password": "P@ssw0rd",
"emptyPasswordInput": false,
"savePassword": false,
"profileName": "mssql-container"
}
]
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-dotnettools.csharp",
"ms-mssql.mssql",
"editorconfig.editorconfig",
"ms-azuretools.vscode-bicep"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
5000,
5001
],
"remoteEnv": {
"ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere",
"ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx",
},
"postCreateCommand": "bash .devcontainer/postCreate.sh"
}
Most of this is from the template, however I added the following:
extensions
: a list of VS Code extensions needed for this project, which will be installed as part of the container setup.forwardedPorts
: this forwards ports5000
and5001
from the container to the local machine so they can be accessed for testing in the browser.remoteEnv
: environment variables needed to configure the ASP.NET Core HTTPS certificate.postCreateCommand
: as mentioned, I have a custompostCreate
script that is linked here.
All together now
The project can be opened in VS Code, inside the Dev Container that was just defined!
Prerequisites
The following are assumed to be on the local machine before openning the Dev Container:
-
Docker Desktop
-
VS Code
-
Remote Development extension pack for VS Code
-
.NET
-
ASP.NET Core HTTPS certification, which can be exported with:
dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere"
-
Any .NET User Secrets are on the local machine in the required secret location
Reopen in Container
Now that the Dev Container is defined in our repository, when this folder is opened in VS Code it will ask to “Reopen in Container”:
Clicking “Reopen in Container” will cause VS Code to reopen the window and start building the container according to the devcontainer.json
configuration:
This may take some time, but eventually the build will finish and you can start developing inside the container. You even have access to the native terminal inside the container!
Summary
In this post, a Dev Container was built for an ASP.NET Core web app with an MS SQL Database, including pre-building the web app image with GitHub Actions (CI/CD) and storing it in GitHub Container Registry. Dev Container’s significantly reduce the effort of a developer in setting up their local environment. One downside to note is that as this approach uses Docker Desktop, you will run out of battery faster on your laptop (if that’s what you use), but apart from that I haven’t had any other issues! Happy dev-ing!