The goal of this series is to introduce some best practices in the local development environment and create a CI/CD pipeline for NodeJS applications.
GitHub repo for this article series: https://github.com/jeanycyang/js-in-pipeline
In this article, we are going to use Docker Compose in our local development environment.
First Glance at Docker Compose
So now we can start our gift code server as a Docker Container.
$ docker run -it -p 40000:3000 --name myapp -v `pwd`:`pwd` -w `pwd` giftcodeserver npm run dev
Maybe we can write it in our README.md
. And copy and paste it in the terminal every time we need it. (Of course, you can use alias
!)
Is there any other way to start a Docker Container — Could it be just like Dockerfile, very useful and declarative? Yes! It is called Docker Compose.
Let’s create a docker-compose.yaml
file in the project directory (.yml
extension also works).
# ${PROJECT_DIR}/docker-compose.yaml
# only for development
version: '3'
services:
server:
build:
context: ./
ports:
- "40000:3000"
volumes:
- ./:/usr/src/app
command: ["npm", "run", "dev"]
It is almost the same as our docker-run command.
This app now is named server
. You can call it whatever you want, e.g. giftcodeserver
, nodeserver
, api_server
.
build context tells Docker Compose to build this app’s Docker image and the Dockerfile locates in ./
. It works the same as context: ./Dockerfile
.
volumes
section tells Docker Compose to mount ./
(host) to /usr/src/app
(container).
And command works the same as our docker-run command. It overrides the CMD
instruction in our Dockerfile. When the container starts, it will run npm run dev
instead of node index.js
.
Run docker-compose up
in the project directory and you shall see Docker Compose take care of everything for you! To stop it, press Ctrl+C
in the terminal.
Obviously, Docker Compose can do much more than just running one Container. Docker Compose’s killer feature is, it can start multiple Containers as described in the docker-compose.yaml
file at once; moreover, you can instruct the relationships between containers.
Run Multiple Containers!
We use MySQL as our database. It’s time to get rid of “google “MySQL”; click MySQL’s website; find the download page; download the installer; install MySQL; set up everything and try to start it” process!
Let’s take a look at MySQL’s official Docker images: https://hub.docker.com/_/mysql.
We will use MySQL 5.7.
# ${PROJECT_DIR}/docker-compose.yaml
# only for development
version: '3'
services:
server:
....
db:
image: mysql:5.7
Name our MySQL Container db
and use the image tagged 5.7
.
The Environment Variables section on the page tells us that we can set up MySQL using environment variables.
db:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: V4TynEq8RsfvyW7JxXGUpjD9MSrbaB9W
MYSQL_USER: giftcodeserver
MYSQL_PASSWORD: m5kCtWQ2G8Zr9VxD79R8LMCfmzX6SAWZ
MYSQL_DATABASE: giftcodeserver
ports:
- "3306:3306"
Now we have set up our MySQL.
Run docker-compose up
in the project directory. You will see Docker Compose start two Containers for us! Later, if necessary, you can add more Containers — be that Redis, RabbitMQ, MongoDB… whatever you need!
So far so goo…What? Wait! A new problem shows up — our server
Container can no longer connect to the db
Container!
// Sequelize is an ORM package, we will use it in this series
const sequelize = new Sequelize('giftcodeserver', 'giftcodeserver', 'm5kCtWQ2G8Zr9VxD79R8LMCfmzX6SAWZ', {
host: 'localhost',
dialect: 'mysql'
});
It is because the host is no longer “localhost”. Our gift code server is started by Docker Compose, runs as a Docker Container in its own network.
As stated in the documentation, Links
allow you to define extra aliases by which a service is reachable from another service. They are not required to enable services to communicate — by default, any service can reach any other service at that service’s name.
Changing host
property to db
makes our server work again.
const sequelize = new Sequelize('giftcodeserver', 'giftcodeserver', 'm5kCtWQ2G8Zr9VxD79R8LMCfmzX6SAWZ', {
host: 'db', // the service name set in docker-compose.yaml
dialect: 'mysql'
});
Nonetheless, explicitly describing it in docker-compose.yaml
is still a best practice.
server:
build:
context: ./
......
links:
- db
Last but not least, we can add one more property — depends_on
.
Generally speaking, starting a database instance and getting it ready takes more time than starting an API server. We can use depends_on
to control the startup order.
server:
build:
context: ./
......
links:
- db
depends_on:
- db
Stop Docker Compose by Crtl+C
then run docker-compose up
again.
Now we see the power of Docker Compose and how great it is for our local development environment!
One More Thing — Persistent Data
You might notice, every time we stop Docker Compose, the data in database will also be removed as the Container dies. If you wish to persist it, you can use Docker Volume again — mount a directory in your local machine (host) to the Container so that your data will be persisted in the host.
The “Where to Store Data” section tells us which directory to mount.
db:
image: mysql:5.7
ports:
- "3306:3306"
volumes:
- ./mysql-data:/var/lib/mysql
You will see a directory named mysql-data
being created and MySQL data will be stored in this directory. Let’s add mysql-data
to .gitignore
.
Next time you restart your container using Docker Compose, the data will still be there unless you delete the mysql-data
folder in your machine.
Done!