Intro

Many people who work with me know that I hate anything related to frontend :).

I only work with backend, but sometimes I still have situations when I need to run some GUI which brings pain: you need to have version x for node, version y for npm for one project, then another project requires different versions and other dependencies… And you need to follow all the different scripts for how to start all that mess in each project. Meeeh…

Automation

As I said - I don’t want to have any business with frontend at all - so to minimize the attention I need to put on frontend I decided to automate the set up of a frontend development environment by using containers. This way I can focus on the fun stuff and getting past the necessary much quicker. Enjoy!

Preparation

In this guide I use:

Setup

  1. In the root directory of your project, you need to create a folder called .devcontainer. Here you will store the settings for your environment.
  2. devcontainer.json file must be created in this folder as Visual Studio Code expects this file structure.
  3. Dockerfile can be either created in this folder or in the root of your project. I usually put it in the root.
  4. .dockerignore file is needed if you would like to ignore some files/folders for docker
  5. run.sh shell script with automation commands

devcontainer.json

Let’s take a look at devcontainer.json and describe what we are doing here:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
    "name": "My super service",
    "dockerFile": "../Dockerfile",
    "extensions": ["dbaeumer.vscode-eslint"],
    "settings": {
        "terminal.integrated.shell.linux": "/bin/bash",
        "terminal.integrated.shellArgs.linux": ["/usr/src/app/run.sh"],
    },
    "mounts": [ "source=C:/code/MyService/wwwroot,target=/usr/src/wwwroot,type=bind" ],

    // more options
    "appPort": 3000,
    "postCreateCommand": "yarn install",
    "runArgs": ["-key", "value"]
}
  • name - is pretty obvious :)
  • dockerFile - relative path to a Dockerfile that you wish to use as your image
  • extensions - an array of extension ids that will be installed inside the container
  • settings - default values inside the container
  • mounts - maps source folder from your file system to targer container file system

These are the options that I used in my projects, but there are more that might be useful:

  • appPort - a port or an array of ports that should be available locally when container is running
  • postCreateCommand - a list of command arguments to run after the container is created
  • runArgs - an array of Docker CLI arguments that should be used when running the container

Dockerfile

1
2
3
4
FROM node:latest
WORKDIR /usr/src/app
COPY ./ ./
VOLUME ["/usr/src/wwwroot"]
  • FROM - you choose which image you work with in your project
  • WORKDIR - working directory within container
  • COPY - for this demo I copy everything but you can create multi-stage dockerfile and build step-by-step
  • VOLUME - creates a mount point and marks it as holding externally mounted volumes from native host or other containers

.dockerignore

1
node_modules\

You can ignore the node_modules folder if you want to skip copying half of the universe when you build your container.

run.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/bin/sh
npm_install()
{
  echo -e "\e[30;102mRunning  'npm install'...\e[0m"
  sleep 1
  npm install
  echo -e "\e[30;102mFinished 'npm install'"
  echo "Starting 'npn run dev' in 5 sec..."
  echo -e ".\e[0m"
  sleep 5
  npm run dev
}
start_in_container() 
{
    echo -e "\e[30;102mChanging path...\e[0m"
    cd /usr/src/app
    echo -e "\e[30;102mI'm at $(pwd)"
    npm_install
    npm run devcontainer
}
start_locally()
{
  npm_install
  npm run dev
}
echo "Do you want to start 'npm run dev' (choose number 2 if you have continuous frontend development)?"
select yn in "Container" "Locally" "Just open bash" ; do
  case $yn in
    Container ) start_in_container; break;;
    Locally ) start_locally; break;;
    "Just open bash" ) bash; break;;
  esac
done

Since I did not want to type all the commands manually I created a script file which will be executed each time when a new terminal window is opened. And for cases when you actually need to work with a terminal the script provides you an option to just start bash.

Verification

So you may already have noticed that there is a new green icon in the left bottom corner of your Visual Studio Code.

Remote container icon

If you click on the button it will provide several options on how to use your remote container.

Remote container options

We need Reopen in Container to build it and start the container. This will reopen the visual studio code and start building the image (if you have already done this before it will also prompt to rebuild the image if you have made some changes in the configuration), after a successful build a new terminal window shall be opened with a nice prompt for you.

Inside container

Now you can use your magic for frontend.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Do you want to start 'npm run dev' (choose number 2 if you have continuous frontend development)?
1) Container
2) Locally
3) Just open bash
#? 3
root@09c8f2ec1018:/workspaces/MyService/Fragments# node -v
v13.13.0
root@09c8f2ec1018:/workspaces/MyService/Fragments# npm -v
6.14.4
root@09c8f2ec1018:/workspaces/MyService/Fragments#

NOTE: In devcontainer.json I mounted wwwroot folder just because in my project it copies all frontend output to this folder within my ASP.Net Core project so when I start the web api I can navigate directly to GUI without starting a node instance on another port. This you can do as well by just specifing the appPort and editing the automation script.

Summary

This is the perfect example of how lazyness can trigger people to find bypass solutions, especially for frontend…