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: ~
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));
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: ~
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
docker compose up app
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)