Zac Fukuda
055

Docker Compose Node.js+MySQL in 2024

docker-compose.yml
version: '3.8'
services:
  mysql:
    image: mysql:8.3.0
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_ROOT_HOST: "%"
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
    volumes:
      - mysql:/var/lib/mysql
  node:
    image: node:20.11.0-alpine
    user: ${NODE_USER}
    working_dir: /home/node/app
    environment:
      - NODE_ENV=${NODE_ENV}
    command: "yarn start"
    ports:
      - ${NODE_PORT}:${NODE_PORT}
    volumes:
      - ./:/home/node/app
    depends_on:
      - mysql
volumes:
  mysql:

This docker compose is designed of local development only.

Networks

You may want to specify networks.

version: '3.8'
services:
  mysql:
    …
    networks:
      - nodejs-mysql
  node:
    …
    networks:
      - nodejs-mysql
networks:
  nodejs-mysql:
    driver: bridge

Node Command

I prefer yarn start.

I don’t know why but yarn start command stops container much faster than npm run start.

With npm run start it takes roughly 10s to stop the container. With yarn start it takes less than 1s.

Two commands do not make a difference when create or start a container.

Environment

Replace ${…} with strings if you don’t want to rely on .env.

Create Test App

The rest of article is about creating an example application to see if this docker compose actually works in more realistic development environment.

You need three additional files:

.
├── .env
├── package.json
└── server.js

Run the next command to create them:

touch .env package.json server.js

Add the following codes to each file:

.env
# Docker Node.js
NODE_USER=node
NODE_ENV=production
NODE_PORT=3000

# Docker MySQL
MYSQL_HOST=mysql
MYSQL_PORT=3306
MYSQL_ROOT_PASSWORD=example
MYSQL_DATABASE=example
MYSQL_USER=user
MYSQL_PASSWORD=example
package.json
{
  "scripts": {
    "start": "nodemon -L server.js"
  },
  "dependencies": {
    "body-parser": "^1.20.2",
    "dotenv": "^16.4.1",
    "express": "^4.18.2",
    "mysql2": "^3.9.1"
  },
  "devDependencies": {
    "nodemon": "^3.0.3"
  }
}

You need to pass -L flag when you run Nodemon via container. See Application isn't restarting.

server.js
require('dotenv').config()

const express = require('express')
const bodyParser = require("body-parser")
const mysql = require('mysql2')
const { MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, NODE_PORT } = process.env

const uri = `mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}`
const app = express()
const connection = mysql.createConnection(uri)

app.use(bodyParser.json())
app.get('/', function (req, res) {
  res.send('Hello, world')
})
app.get('/books', async function(req, res) {
  const sql = 'SELECT * FROM books'
  const [books] = await connection.promise().query(sql)

  res.json({ data: { books } })
})
app.post('/books', async function(req, res) {
  const { title } = req.body
  const sql = `INSERT INTO books (title) VALUES ("${title}")`
  const [result] = await connection.promise().query(sql)

  res.json({ data: { result } })
})
app.listen(NODE_PORT)

Install Node Modules

npm install
# or
yarn

I am not sure if pnpm would work with Docker’s volume mounting.

Compose

# Up
docker compose up --build -d

# Start/stop containers
docker compose start
docker compose stop

# Stop and remove containers, networks
docker compose down

Create MySQL Table

Login in to the MySQL container with cli with the password you define in .env:

docker exec -it your-container-name mysql -u user -p

Run the following SQLs:

use example;
CREATE TABLE books (id int NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, PRIMARY KEY (id));

-- Check if the table is created
SHOW TABLES;

Testing

Open http://localhost:3000. You see “Hello, world” message.

Next, open http://localhost:3000/books. This time you see JSON string like this:

{"data":{"books":[]}}

Now open your terminal and make a couple of curl requests:

curl -X POST -H "Content-Type:application/json" -d '{"title": "Atomic Habits"}' http://localhost:3000/books
curl -X POST -H "Content-Type:application/json" -d '{"title": "The Psychology of Money"}' http://localhost:3000/books

You will get a response(output) like below for each request:

{"data":{"result":{"fieldCount":0,"affectedRows":1,"insertId":1,"info":"","serverStatus":2,"warningStatus":0,"changedRows":0}}}

Back to http://localhost:3000/books, you get an array of two books:

{"data":{"books":[{"id":1,"title":"Atomic Habits"},{"id":2,"title":"The Psychology of Money"}]}}

Logs

When you run a container in daemon, you cannot see the log of Node.js process when any error or bug occurs. You have two options to check the log.

1. Docker Dashboard

  1. Open Docker Dashboard
  2. Click “Containers”
  3. Click your node container
  4. You can see the logs

2. VS Code Docker extension

  1. Install “Docker” extension if not installed, then restart VS Code
  2. Open “Command Palette” (Shift + Cmd + p)
  3. Select “Docker Containers: View Logs”
  4. Choose your stack
  5. Choose your node container
  6. VS Code shows the container logs in Terminal

Resources