DEV Community

Cover image for Dockerizing a Node.js / Express app from the very first [Part 2]
Rumon
Rumon

Posted on • Updated on

Dockerizing a Node.js / Express app from the very first [Part 2]

Previously, we created a Node-Express application completely off of Docker. We didn't need to have Node.js installed on our machine at all. We used Docker for basically scaffolding a new Node.js project and setting up the development workflow. In this article, we'll see how we can add a database solution to our current app without having any database server installed on our machine.

Let's start by editing our docker-compose file:

services:
  app:
    depends_on:
      - database
    image: node:lts-alpine
    working_dir: /usr/src/app
    volumes:
      - ./code:/usr/src/app:rw
    ports:
      - 8080:12345
    command:
      - npx
      - nodemon
      - -L
      - app.js
  database:
    image: postgres:alpine
    volumes:
      - postgres:/var/lib/postgresql/data:delegated
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: dockerized_node_express_app
    restart: always
  npm:
    image: node:lts-alpine
    working_dir: /tmp/app
    volumes:
      - ./code:/tmp/app:rw
    entrypoint:
      - npm
volumes:
  postgres: ~
Enter fullscreen mode Exit fullscreen mode

We've just followed the official documentation to add a PostgreSQL database server. This will act as an application container, as our app will soon be depended on it. And in order to make sure that the database container is always started whenever we start our application container, we've added a new depends_on key to the app service and let it know which other service(s) it, well, depends on. This will make sure that the database server is up and running before the app. You may also have noticed that we've mounted volume to our database server's datapath a bit differently. This is known as "named volume". We'll discuss it a bit more in another part, it's not relevant to what we're learning here.

Now, if we try to start the application using docker compose up -d app command, we'll see that the application will start up along with the database itself. Let's change our application code so that we can connect to the database from the application.

const express = require('express');
const { Sequelize } = require('sequelize');

const {
  DB_HOST,
  DB_PORT,
  DB_USER,
  DB_PASS,
  DB_NAME
} = process.env;

const app = express();
const sequelize = new Sequelize({
  dialect: 'postgres',
  host: DB_HOST,
  port: DB_PORT,
  username: DB_USER,
  password: DB_PASS,
  database: DB_NAME,
  logging: false,
});

app.get('/', (req, res) => {
  res.json({ status: 200, message: 'Hello, world!' });
});

sequelize
  .authenticate()
  .then(() => console.log('Established connection with the database...'))
  .then(() => app.listen(12345))
  .then(() => console.log('Started the application server...'))
  .catch((error) => console.error(error.message));
Enter fullscreen mode Exit fullscreen mode

NEVER USE DATABASE CREDENTIALS DIRECTLY IN THE CODE.

Notice that we've got a few environment variables. We're running the application using Docker Compose, how are we supposed to tell Docker to set and define those? We use environment key.

services:
  app:
    depends_on:
      - database
    image: node:lts-alpine
    working_dir: /usr/src/app
    volumes:
      - ./code:/usr/src/app:rw
    environment:
      DB_HOST: database
      DB_PORT: 5432
      DB_USER: postgres
      DB_PASS: postgres
      DB_NAME: dockerized_node_express_app
    ports:
      - 8080:12345
    command:
      - npx
      - nodemon
      - -L
      - app.js
  database:
    image: postgres:alpine
    volumes:
      - postgres:/var/lib/postgresql/data:delegated
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: dockerized_node_express_app
    restart: always
  npm:
    image: node:lts-alpine
    working_dir: /tmp/app
    volumes:
      - ./code:/tmp/app:rw
    entrypoint:
      - npm
volumes:
  postgres: ~
Enter fullscreen mode Exit fullscreen mode

Look at the DB_HOST variable. Usually we'd use localhost, but why are we using database? Well, we'd use localhost if we had both the application and the database server running on a single machine. Remember, we're using Docker, and all the containers here are completely isolated from each other, although each one of them is running on the same machine. It's like everything is being operated within its own "machine". And because of that, the application container doesn't know how to talk to the database server, it needs to know the IP address of that container. Luckily, if we use the name of the service of the container, Docker will internally resolve that to its IP address. That's how we establish communication among multiple containers.

We're almost done. Our app now needs few more dependencies, let's install them and finally bring up the app. We're gonna use our good-old npm utility container once more.

docker compose run --rm npm i sequelize pg
Enter fullscreen mode Exit fullscreen mode
docker compose up app
Enter fullscreen mode Exit fullscreen mode

In case of you just started from this part and missed the previous one, run the following commands one after another: docker compose run --rm npm init -y, docker compose run --rm npm i express, docker compose run --rm npm i -D nodemon.

After a while (since the connection process is asynchronous and takes some time to finish), you'll see the success message appear in the terminal console. Yay! Such an awesome tool Docker is! LONG LIVE THE DOCKER!

Now that we've added database to our app and used a popular ORM "Sequelize", you should feel like home. Feel free to leverage that ORM (models, migrations, seeders etc.) and make this app more useful to the world.

In the next part, I'll discuss about file uploading. Because it appears that user-generated files are handled and managed a bit differently when you use Docker as a part of your development workflow. See ya there!

Top comments (0)