Sails development with Docker and Docker Compose
Tags: docker, api, tutorial, containers
Published: 2021-04-14 14:18:32

Docker makes it easy to set up and run a development environment and also simplifies the deployment of your applications. In this article we will see how to setup a Sails development workflow with Docker and Docker Compose.

#Whats and whys: Docker & Docker Compose
The goal of this article is to make setting up a Docker workflow for Sails as simple as possible. Docker makes it easier to set up and run a development environment and also simplifies the deployment. 

First, we’ll get into how Docker and Docker Compose work and its benefits. Then a step by step guide to make our beloved Sails run inside a Docker container. Let's get started!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v4v8sh7kawk5r78yez0u.jpg)

###Containerization
Before talking about Docker and Sails we need to understand what is containerization and why we want it.
Containerization is a form of virtualization, through which applications are run in isolated user spaces called containers. We can think of a container as a lightweight virtual machine (but to be clear, it is not).

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1njhg35emitkono64h6y.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/16o7ijl9n57b2p35f349.png)*Comparing Containers and Virtual Machines* [1]

But, why do we want it?

With containerization, everything an application needs to run, let's say, its binaries, libraries, configuration files and dependencies, are encapsulated and isolated in its container. This makes it easier to set up and run a development environment, and also simplifies the deployment by reducing the chances of missing or different versions of libraries, files or dependencies errors.

The container itself is abstracted away from the host OS, with only limited access to underlying resources. Isolation has its advantages, for example portability, stability and security.

So, standard, lightweight, secure… **yes as developers we want containerization!**


###Docker and Docker Compose
The word "Docker" refers to several things including an open source project, tools and a company (Docker Inc., the company that primarily supports the project and tools).

Talking about the tools, Docker is a tool designed to make it easier to create, deploy, and run applications by using containers.

Container orchestration automates the deployment, management, scaling, and networking of containers. It can help you deploy the same application across different environments without needing to redesign it. As an example, microservices in containers make it easy to orchestrate services, including storage, networking, and security. **Docker Compose is an orchestration tool for Docker containers.** [2]

###Installing Docker
Mac: https://docs.docker.com/docker-for-mac/install/
Windows: https://docs.docker.com/docker-for-windows/install/
Ubuntu: https://docs.docker.com/engine/install/ubuntu/
Others: https://docs.docker.com/engine/install/


#Sails containerization
Now, how do we make Sails, our beloved Node framework, run inside a container? 

###Sails CLI image
Our container needs an image to run. Let's create one.
To do so, create docker-sails-cli directory and a file named Dockerfile inside it with this content:

```Dockerfile
FROM node:lts

RUN npm install -g sails@1.4.2
```

The FROM statement declares we are using node latest lts version as base image for ours. The syntax used is `imageName:tag`, if no tag is specified, the default is used. [3]

At the same time Node lts image is based on Debian Streatch. So this is our project environment, Sails 1.4.2, node 14.15.4 (at the time of writing this) running on a Debian Streatch.

Now we need to build our image.

```sh
docker build . -t sails-cli:1.4.2
```

Simple, right?

Let’s see what we did.

```docker build [context] -t [tag]```

**context**: The context where we are building. As we are in the same directory as the Dockerfile we use `.`.  And also, there’s no need to specify the file name as Dockerfile is the default one.

**tag**: It is optional, but we want to tag the image we just built as we are probably going to have many different images with different versions for different projects.


###Creating a new Sails project
Once we have our Sails CLI image built we are going to use it to create a new Sails project.

```sh
docker run --rm -v `pwd`:/app -w /app sails-cli:1.4.0 sails new sails-docker-example --no-frontend
```

**docker run**: Create and run a new container
**--rm**: Remove container after process finishes as we don’t need the container after that
**-v**: Mount current working directory (in our host computer) in /app path inside container. As a result we are sharing current directory with the container
**-w**: Set container current working directory to /app
**sails-cli:1.4.0**: Our previously built Sails image

**sails new sails-docker-example --no-frontend**: Sails CLI command we want to run in the container.

As a result, after running `sails new` in the container, we are going to find a new directory created with our new Sails project. 

Now, let’s run our project:

```sh
cd sails-docker-example
docker run --rm -it -v `pwd`:/app -w /app -p 1337:1337 sails-cli:1.4.0 sails lift
```
**-i**: Keep STDIN open even if not attached
**-t**: Allocate a pseudo-TTY
**-p**: Maps container 1337 to host computer so we can reach our app through localhost:1337

It's alive!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3chsoinwvj4pmagcqpxp.gif)


###Running and debugging
As you may notice, although our project is running, if you modify a file you will need to stop the container and launch it again to get the changes applied.

Nodemon to the rescue!


```sh
docker run --rm -v `pwd`:/app -w /app sails-cli:1.4.0 npm install nodemon --save-dev
```

Now, open **package.json** and in the **scripts** section add a new one, **debug**:

```json
"scripts": {
	"debug": "nodemon --inspect=0.0.0.0 app.js",
	"start": "NODE_ENV=production node app.js",
...
```

And run again, but this time instead of sails lift we are going to run with npm:

```sh
docker run --rm -v `pwd`:/app -w /app -p 1337:1337 -p 9229:9229 sails-cli:1.4.0 npm run debug
```

Now, modify a file, save it and verify that the project is restarted automatically.

Finally, as we are running inside a container we need a way to get the debugger connected to the process, so we are mapping the 9559 port.

If you use VS Code you can use the following configuration in the 
launch.json file:

```json
{
    "version": "0.2.0",
    "configurations": [
        {
            "address": "localhost",
            "localRoot": "${workspaceFolder}",
            "name": "Attach to Remote",
            "port": 9229,
            "remoteRoot": "/app",
            "request": "attach",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "type": "node",
            "restart": true
        }
    ]
}
```


#Docker compose
As we mentioned before Docker Compose is an orchestration tool. Compose allows us to define our infrastructure, let’s say, services, networks, volumes in an YAML file.

As a first step we will create a very simple file defining our Sails service on it. For this service we will set image, command, ports mapping, volume mounts, working directory, etc, as we did with Docker command line tool.

Our docker-compose.yml:

```yml
version: '3'

services:
    api:
        image: sails-cli:1.4.0 # same as with docker cli
        # Run npm install before starting sails to keep our dependencies installed and updated
        command: sh -c "npm install && npm run debug"
        working_dir: /app # same as -w with docker cli
        ports: # same as -p with docker cli
            - '1337:1337'
            - '9229:9229'
        volumes: # same as -v with docker cli
            - .:/app
```

Then, we will use docker-compose command to lift our service.

```sh
docker-compose up
```

The result is the same as running with Docker CLI.

In case we want to remove the created infrastructure, once stopped, run:

```sh
docker-compose down
```
This will remove all the containers, networks, volumes created with `docker-compose up`.


#Database containerization
In most cases we will require a database for our project. In this example we will be using MongoDB but it’s possible to do the same with other db engine supported by Sails. [4]

For those who don’t know, MongoDB is a free and open-source cross-platform document-oriented database program. [5]

Fortunately MongoDB has an official image that we will be using to add a database service to our project.

First, we need to add Sails MongoDB adapter to our project. As we did before, we can add and install a dependency to our project with Docker like this: 

```sh
docker run --rm -v `pwd`:/app -w /app sails-cli:1.4.0 npm install sails-mongo
```

Then, modify our project config and set mongo as our default adapter.

In the config/datastore.js file:

```javascript
.
.
.

default: {
    adapter: require('sails-mongo'),
    url: process.env.DB_URL
},

```

Next we need to add MongoDB service to our docker-compose.yml and provision Sails service with needed config.

docker-compose.yml

```yml
version: '3'

services:
    api:
        image: sails-cli:1.4.0
        command: sh -c "npm install && npm run debug"
        working_dir: /app
        ports:
            - '1337:1337'
            - '9229:9229'
        volumes:
            - .:/app
        env_file:
	      - ./sails.env # set env vars using a file

    db:
        container_name: mongodb # set name to the container
        image: mongo:4.4 # as of this writing, the latest version is 4.4.3
        volumes:
            - ./mongo:/data/db
```

We added a db service to our compose file, and also set an env file to Sails service. We will use that file to config our database connection.

sails.env
```
DB_URL=mongodb://root:@mongodb:27017/sails
```

Compose allows us to connect to db using the container name as network address, that is why we can use **mongodb** as the address in our connection url.

**Important note**: we are not enabling MongoDB authentication for this example but it is a good idea to do so for production environments. [6]

Finally, use `docker-compose up` to run, you will see MongoDB output along with Sails.

And we are done, a fully Sails development environment running on Docker!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hlh5ipk0erii4jlnanyc.gif)



Thank you so much for reading my post, comments are welcome! 

Here is the link to the writter's twitter @vidueirof for any questions you may have. 


[1] https://www.docker.com/resources/what-container
[2] https://www.docker.com/why-docker
[3] https://hub.docker.com/_/node
[4] https://sailsjs.com/documentation/concepts/extending-sails/adapters/available-adapters
[5] https://www.mongodb.com
[6] https://hub.docker.com/_/mongo