Did you ever try to run code on your local machine, but it simply didn’t work? And when you ask for help all you hear back is "It works on my machine"? Well, Docker can be a good solution to end not just this, but many other problems like that.
In this article, we are going to create a development environment with Javascript language but these concepts can be used for other languages as well.
Grab your coffee (or beer), and let’s dig into it!
If you don’t have docker and docker-compose installed on your computer, I’m going to make your life easier and leave the links you need to install it bellow:
Docker install
Docker Compose install
Note: If you use Linux with WSL2 check your network, firewall, and DNS settings.
JavaScript/NodeJS Environment
First, create a folder in the project directory of your preference, then create and open a Dockerfile.dev
file with the contents of the example below:
FROM node:17.8
RUN npm config set cache /home/node/app/.npm-cache --global
RUN npm install -g npm@8.6.0
USER node
RUN mkdir /home/node/poc
WORKDIR /home/node/poc
COPY . .
EXPOSE 9091
CMD ["tail", "-f", "/dev/null"]
Let’s break down the block above:
- FROM: refers to the base image to be used in the application. Other possibilities are: alpine (use sh and not bash. This command is apk based), slim, slim-buster, stretch, buster, etc (for more informations access Images Node);
- WORKDIR: the main path or root of the container;
- RUN: executes shell commands, sudo isn’t required
- COPY: specifies the folders/files to be copied into the container (dot
.
equals all files and folders). These files are going to be placed in the path set in WORKDIR. - EXPOSE: ports visible for network use;
- CMD: commands to be executed at the end of the Dockerfile. They’ll keep the container running.
Another thing we can do when creating the Dockerfile.dev is to change the user in the container:
FROM node:17.8
RUN groupmod -g 1001 node && usermod -u 1001 -g 1001 node
RUN adduser --disabled-password -uid 1000 user-poc
RUN mkdir /home/user-poc/poc/
RUN npm install -g npm@8.6.0
USER user-poc
RUN mkdir /home/user-poc/poc
WORKDIR /home/user-poc/poc
COPY . .
EXPOSE 9091
CMD ["tail", "-f", "/dev/null"]
What changes between the two?
- change the user node and group node for another uid
- add a user with another uid (consult your uid in the terminal)
After creating the file, there’s two ways to run it:
docker build -t nametag:tagimage -f Dockerfile.dev .
- Use docker-compose
For this scenario, we’ll be using the second one.
Docker Compose
"Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services."(Docker compose overview)
If you’re working with Node.JS you might ask yourself "Do I need to install Node.js?" And the answer is no, you don’t! With docker the image set in Dockerfile already contains all configurations for your selected language.
How to create this YAML file? First, create a file docker-compose.yaml
or docker-compose.yml
with the contents below:
version: '3.4'
services:
poc:
container_name: poc-js
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "9091:9091"
volumes:
- ".:/home/node/poc"
Let’s take a closer look at the contents of the file:
- version: this is the specification version. Check this link on changing versions and compatibility information. Note that specifications of versions higher than 3.4 require at least docker-compose version 2 to work.
- services: is the relation of containers we’ll create:
- poc: this is your service’s name. You can give it any name you want (like
frontend
,backend
,database
, etc).- container_name: container’s display name, especially practical when used with
docker ps
ordocker-compose ps
- build: configuration for image creation:
- context: path to the Dockerfile folder;
- dockerfile: reference to the Dockerfile;
- ports: Ports to be exposed and bound;
- volumes: paths that must be accessible by containers.
- container_name: container’s display name, especially practical when used with
After finishing the configuration file, you just need to run this on your terminal window:
docker-compose up -d --build
This command will build and create the container. The -d (or –detach) option is set to run the containers in the background so it won’t block your terminal. To check the containers currently running you can do the following:
#docker container
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
eb76817fab00 poc-js_poc "./.docker/entrypoin…" 4 hours ago Up 4 hours 9000/tcp, 0.0.0.0:9091->9091/tcp poc-js
#docker compose version 1
docker-compose ps
#docker compose version 2
docker compose ps
Name Command State Ports
---------------------------------------------------------------------------
poc-js ./.docker/entrypoint.sh Up 9000/tcp, 0.0.0.0:9091->9091/tcp
To access the container you can just run:
docker exec -it poc-js bash
#result
node@eb76817fab00:~/poc$
Now run the command:
npm init -y
For this app, we’ll make a single change in the package.json
file. Simply add the type: module property for ECModules used in the project. It should look something like this:
{
"name": "poc",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module"
}
After that, we need to install some packages for dev env. We’ll begin with tests:
npm install -D jest
Create a jest.config.mjs
file with the contents below:
/*
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
export default {
// Automatically clear mock calls, instances and results before running any test
clearMocks: true,
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// A map from regular expressions to paths to transformers
transform: {}
};
In package.json
again, set these lines:
"scripts": {
"test": "NODE_OPTIONS=--experimental-vm-modules npx jest",
"test:coverage": "NODE_OPTIONS=--experimental-vm-modules npx jest --coverage"
},
A few more thigs we need to do:
- Create the folder
./test/unit
(mkdir test && mkdir test/unit
) - Create the file
Item.test.js
Paste the code below in
Item.test.js
:describe('Item', function () { it('Get value item', function () { const item = new Item({ name: 'Billabong T-SHIRT AI', category: 'T-SHIRT', description: 'T-SHIRT ANDY IRONS', value: 30, quantity: 1 }); expect(item.getTotalValue()).toBe(30) }); it('Should return error: Value is required', function () { expect(function () { new Item({ name: 'Billabong T-SHIRT AI', category: 'T-SHIRT', description: 'T-SHIRT ANDY IRONS', quantity: 1 }).toThrow(new Error("Value is required")); }); }); it('Should return error: Quantity is required', function () { expect(function () { new Item({ name: 'Billabong T-SHIRT AI', category: 'T-SHIRT', description: 'T-SHIRT ANDY IRONS', value: 30 }).toThrow(new Error("Quantity is required")); }); });
- Create the folder
./src
(mkdir src && mkdir src/domain && mkdir src/domain/entity) Create file Item.js with these contents:
export default class Item { #name #category #description #value #quantity constructor({ name, category, description, value = undefined, quantity = undefined }) { this.#name = name this.#category = category this.#description = description this.#value = value this.#quantity = quantity this.validation() } validation() { if (this.#value === undefined) throw new Error("Value is required") if (this.#quantity === undefined) throw new Error("Quantity is required") } getTotalValue() { return this.#quantity * this.#value } }
- Import Item.js in Item.test.js:
import Item from "../../src/domain/entity/Item.js"
Now try running npm t
or npm run test
and to check tests coverage run npm run test:coverage
After finishing the all the configuration needed, we might be left with the following scenario: think about setting up this app and running it for the first time. We’d have to run npm install
every time and that’s not practical.
We need to configure Dockerfile so our initial container executes npm install
automatically. Follow these steps:
- Create folder
.docker
; - Create file
entrypoint.sh
with the contents below:#!/bin/bash
cd /home/user-poc/poc
npm i
tail -f /dev/null
3. Remove the CMD entry in the Dockerfile.dev file;
4. Set folder's permissions: chmod -R 777 .docker/
5. Change the docker-compose.yaml
file by adding an entrypoint line to look like this:
```yaml
version: '3.4'
services:
poc:
container_name: poc-js
entrypoint: ./.docker/entrypoint.sh
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "9091:9091"
volumes:
- ".:/home/node/poc"
After all those steps, you can build the container with the docker-compose command.
With that, the configuration is finished. Keep in mind this is one of many possible configurations for this kind of project and they may all vary according to your specific project needs.
To check out the results of all the steps listed here, access this repository
References
https://docs.docker.com/engine/reference/builder/
https://github.com/alohaguilherme/docker-developer-enviroment
We want to work with you. Check out our "What We Do" section!