Project Structure

Here’s the project structure for my application. Adjust to your needs. I used the Express application generator to scaffold the program.

.
├── docker-compose.yml
├── Dockerfile
├── healthcheck.js
├── LICENSE
├── node_app
│   ├── app.js
│   ├── bin
│   │   └── www
│   ├── db
│   │   ├── Dockerfile
│   │   ├── knex.js
│   │   ├── migrations
│   │   └── seeds
│   ├── knexfile.js
│   ├── node_modules
│   ├── package.json
│   ├── pnpm-lock.yaml
│   ├── public
│   ├── routes
│   └── views
└── README.md

Dockerfile & docker-compose

The Dockerfile must have the Postgres connection string as a build argument.

See Docker ARG, ENV and .env - a Complete Guide for more information on ARG, ENV and Docker.

Example excerpt from Dockerfile:

# set database connection string
ARG DATABASE_URL
ARG DATABASE_URL_TEST
ENV DATABASE_URL $DATABASE_URL
ENV DATABASE_URL_TEST $DATABASE_URL_TEST

Example docker-compose.yml:

version: '2.4'

services:
  node_app:
    build:
      context: .
      args:
        - NODE_ENV=development
        - DATABASE_URL=postgresql://postgres:postgres@db:5432/mocha_chai_tv_shows
        - DATABASE_URL_TEST=postgresql://postgres:postgres@db:5432/mocha_chai_tv_shows_test
    command: ../node_modules/.bin/nodemon --inspect=0.0.0.0:9229 ./bin/www
    ports:
      - '80:3000'
      - '9229:9229'
      - '9230:9230'
    volumes:
      - ./node_app:/opt/node_app/app:delegated
      - ./node_app/package.json:/opt/node_app/package.json
      - ./node_app/pnpm-lock.yaml:/opt/node_app/pnpm-lock.yaml
      - notused:/opt/node_app/app/node_modules
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      disable: true

volumes:
  notused:

Install Knex.js & Postgres

We also use docker-compose.yaml to create a container for the Postgres database. Let’s call the service db:

db:
    build:
      context: ./node_app/db
      dockerfile: Dockerfile
    expose:
      - 5432
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      start_period: 10s
      timeout: 4s
      retries: 3

The database folder is located inside the node application: node_app/db.
Here’s the Dockerfile for the Postgres database:

# node_app/db/Dockerfile

# pull official base image
FROM postgres:12.2-alpine

# run create.sql on init
ADD create.sql /docker-entrypoint-initdb.d

create.sql spins up the databases for development, testing, production:

# node_app/db/create.sql

CREATE DATABASE mocha_chai_tv_shows;
CREATE DATABASE mocha_chai_tv_shows_test;
CREATE DATABASE mocha_chai_tv_shows_prod;

The Postgres connection string inside the docker-compose.yaml reflects this:

# build args for the node app
- DATABASE_URL=postgresql://postgres:postgres@db:5432/mocha_chai_tv_shows
- DATABASE_URL_TEST=postgresql://postgres:postgres@db:5432/mocha_chai_tv_shows_test

Your Node application must have Knex.js and the postgres driver.

For example:

docker-compose exec node_app npm install -g knex
docker-compose exec node_app npm install --save knex
docker-compose exec node_app npm install --save pg

Initialization

Initialize knex:

docker-compose exec node_app knex init
> Created ./knexfile.js

Now we can adjust the configuration to our needs:

module.exports = {
  test: {
    client: 'pg',
    connection: process.env.DATABASE_URL,
    migrations: {
      directory: __dirname + '/db/migrations',
    },
    seeds: {
      directory: __dirname + '/db/seeds/test',
    },
  },
  development: {
    client: 'pg',
    connection: process.env.DATABASE_URL_TEST,
    migrations: {
      directory: __dirname + '/db/migrations',
    },
    seeds: {
      directory: __dirname + '/db/seeds/development',
    },
  },
  production: {
    client: 'pg',
    connection: process.env.DATABASE_URL,
    migrations: {
      directory: __dirname + '/db/migrations',
    },
    seeds: {
      directory: __dirname + '/db/seeds/production',
    },
  },
}

Migrations

docker-compose exec knex migrate:make <example>

up and down functions: You’ll find a new file inside the migrations folder.

exports.up = function(knex, Promise) {
  let createQuery = `CREATE TABLE <examples>(
    id SERIAL PRIMARY KEY NOT NULL,
    message TEXT,
    created_at TIMESTAMP
  )`
  return knex.raw(createQuery)
}

exports.down = function(knex, Promise) {
  let dropQuery = `DROP TABLE <examples>`
  return knex.raw(dropQuery)
}

Run migrations:

docker-compose exec node_app knex:migrate --env development
docker-compose exec node_app knex:migrate --env test

Seeds

docker-compose exec node_app knex seed:make <example> --env development
docker-compose exec node_app knex seed:make <example> --env test

For example:

docker-compose exec node_app knex seed:make shows_seed --env development
docker-compose exec node_app knex seed:make shows_seed --env test

This will create new folders called seeds/development and seeds/test which you can modify.

In my repository, the files are node_app/seeds/development/show-seed.js and node_app/seeds/tet/show-seed.js

Then run docker-compose exec node_app seed:run --env development (and --env test).

Glue Everything Together

Create knex.js Inside the database folder (node_app/db/knex.js):

const knex = require('knex')
const knexfile = require('../knexfile.js')

const environment = process.env.NODE_ENV || 'development'

const config = knexfile[environment]

module.exports = knex(config)

Example GitHub Repository

See GitHub.

Further Reading