Full-Stack Magic With Docker. How to Dockerize a full-stack app that…
By: Stuart Haas
Tags: React, Programming, Docker, Express, Mongodb

Coverage

Setting up the application infrastructure and getting it up and running on Docker.

Prerequisites

I’d recommend having some basic understanding of Docker, as I’m not going to go into detail explaining what it is or how it works, but there are plenty of resources to find out for yourself if you don’t already know. Also, you should have some understanding of React, Express, Mongo, although that will be more for the next part of the series.

Setup

Open up your favorite code editor. Mine is Visual Studio Code. Create a new project and give it a name of your choosing, although if you are OCD about naming things like myself you can just call it full-stack-docker . Open your terminal. I either use Hyper or the VS Code integrated one. Initialize a git repo with git init or if you prefer you can do it with a gui client like Source Tree or Kraken. Add a gitignore with the following content. I’ll leave it up to you to add more if you like, but you really shouldn’t need more than this.

node_modules

build

dist

.env

yarn-error.log*

In the project root, create one directory called client and another called server. Those are where the React and Express applications will live respectively. Open the client directory in the terminal and run npx create-react-app and you can wait for it to finish or you can open another terminal in the meantime and go into the server directory and run npm init and follow the prompts. You’ll need to add dependencies for the server once that’s done. Paste the following in your terminal and run it:

yarn add express body-parser cors dotenv helmet mongoose

We’ll need to add nodemon globally as well.

yarn global add nodemon

Let’s set up the npm scripts to spin up the server. Add this command to the scripts in the server package.json.

"dev": "nodemon --watch src src/index.js"

Create a .env file in the server root (not inside src) and include these lines:

DATABASE_URL='mongodb://database:27017/fullstack'

CLIENT_ORIGIN="http://localhost:3000"

Okay now time to actually start coding. Inside server created a new folder called src and add the entrypoint file with the name index.js . and add the content below. We’ll add the code to connect to Mongo once we get everything Dockerized. If you’re wondering what some of the dependencies do a good ol’ fashioned Google search should help you out.

const express = require('express');

const mongoose = require('mongoose');

const cors = require('cors');

const helmet = require('helmet');

const bodyParser = require('body-parser');

const dotenv = require('dotenv');



dotenv.config();



const PORT = process.env.PORT || 5000;



const app = express();



app.use(helmet());

app.use(cors({ origin: process.env.CLIENT_ORIGIN }));

app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: true }));



function start() {

app.listen(PORT, () => {

console.log(

`Application is up and running on port ${PORT}`

);

});

}



start();

Run yarn dev and in the terminal you should see that the application is running.

❯ yarn dev

yarn run v1.22.5

$ nodemon --watch src src/index.js

[nodemon] 2.0.4

[nodemon] to restart at any time, enter `rs`

[nodemon] watching path(s): src/**/*

[nodemon] watching extensions: js,mjs,json

[nodemon] starting `node src/index.js`

Application is up and running on port 5000

Sweet. Now for peace of mind let’s start up the React app as well. From client run yarn start and if it’s all savy than a browser window should pop up with the app running at http://localhost:3000 Try not stare at the spinning atom for too long. We’ve got work to do! Okay next let’s do some Docker magic so we can tell our friends that we’re wizards.

But not JUST Docker. We’re going to use Docker Compose to make our lives a little bit easier and a Makefile to provide some shortcuts for doing Docker things and to make shit-happen .

Go ahead and stop the client and server processes with CTRL + C or just close the terminals running them. Next, let’s add the docker-compose.yml to the project root and add these ingredients. They’re not actually called ingredients but we are making a potion so…

version: '3.4' services: client:

container_name: fullstack_client

stdin_open: true

build:

context: ./client

depends_on:

- server

volumes:

- ./client:/app/client

- /app/client/node_modules

ports:

- 3000:3000

environment:

CHOKIDAR_USEPOLLING: 'true' server:

container_name: fullstack_server

stdin_open: true

build:

context: ./server

depends_on:

- database

volumes:

- ./server:/app/server

- /app/server/node_modules

ports:

- 5000:5000

environment:

CHOKIDAR_USEPOLLING: 'true'



database:

image: mongo:latest

container_name: fullstack_database

volumes:

- mongo-data:/data/db

environment:

- MONGO_INITDB_DATABASE=local volumes:

mongo-data:

Okay now you might notice that the client and server both have a build context. That’s because we are going to set up Dockerfiles for each of them to conjure up their containers.

In client and server add a Dockerfile and .dockerignore (to keep out those pesky trolls so we can pass the context bridge without having to pay a fee).

Here’s the client Dockerfile.

FROM node:12 WORKDIR /app/client COPY package.json .

COPY yarn.lock . RUN yarn install COPY . . EXPOSE 3000 ENV NODE_ENV=development CMD [ "yarn", "start" ]

And the .dockerignore

node_modules

npm-debug

.dockerignore

Dockerfile

And for the server Dockerfile

FROM node:12 WORKDIR /app/server COPY package.json .

COPY yarn.lock . RUN yarn install COPY . . EXPOSE 5000 ENV NODE_ENV=development RUN yarn global add nodemon CMD [ "yarn", "dev" ]

…And the .dockerignore

node_modules

npm-debug

.dockerignore

Dockerfile

And one last thing before we cast our spell er brew our potion. This is either a potions class or a spells class, but I’m having too much fun interjecting wizard humor (is that a thing?).

The ‘make’ file

Do things or uh make things happen just by using make <command> . Neat. And now your training is nearly complete. Oh I’m trying so hard to hold back on the wizard jokes and fantastic dad jokes but I can’t help myself. It’s in my nature. The only thing that could make this better is a meme. Speaking of.

Add the Makefile to your project root and run make dev and wait patiently.

A wizard is never late, nor is he early, he arrives precisely when he means to.

How have I not made a broom or staff reference yet. My mind can’t decide what fantasy universe to reside in — Harry Potter or LOTR.

dev:

docker-compose up start:

docker-compose up -d login-client:

docker exec -it fullstack_client sh login-server:

docker exec -it fullstack_server sh login-database:

docker exec -it fullstack_database mongo

Okay so if everything went according to plan your terminal should be telling you that the server, client, and database are all up and running. HUZZAH!

I know. I made the same face when I found out, too. Well would you just look at that! We’ve managed to do some magic without it blowing up in our faces. It’s quite satisfying indeed. Onward.

Oh man I just referenced a Disney movie rather unintentionally but just so happens to also feature magic!

Building the API

Let’s go back to the server index.js and connect the application to the database. Make this change to the file.

const express = require('express');

const mongoose = require('mongoose');

const cors = require('cors');

const helmet = require('helmet');

const bodyParser = require('body-parser');

const dotenv = require('dotenv'); dotenv.config(); const PORT = process.env.PORT || 5000; const app = express(); app.use(helmet());

app.use(cors({ origin: process.env.CLIENT_ORIGIN }));

app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: true })); function start() {

mongoose

.connect(process.env.DATABASE_URL, {

useNewUrlParser: true,

useUnifiedTopology: true,

})

.then(() => {

console.log(

`Database is connected to ${process.env.DATABASE_URL}`

);

app.listen(PORT, () => {

console.log(

`Application is up and running on port ${PORT}`

);

});

})

.catch((error) => {

console.log(error);

});

} start();

When you save the file the server should reload and in the terminal it should read:

fullstack_server | Database is connected to mongodb://database:27017/fullstack

fullstack_server | Application is up and running on port 5000

Awesome. That’s it for now. In the next part of the series I’m going to show you how I like to build Express and React applications. We’ll write some endpoints in the API and make a simple UI in React Bootstrap and have all sorts of fun with CRUD! Thanks for reading and remember…