If you are developing microservices, docker-compose(compose), can be a very powerful tool. Think of a case with an API service and database. Creating and maintaining a database with the use of the Liquibase.

Initial plan

…would be:

  1. start DB
  2. run the Liquibase to make changes
  3. start an app

Adding docker-compose as a driver to utilize this process:

sequenceDiagram autonumber Docker -->> PostgresDB: start PostgresDB -->> PostgresDB: wait to finish start PostgresDB -->> Docker: done Docker -->> LiquibasePG: start LiquibasePGDB -->> PostgresDB: execute changeLog LiquibasePGDB -->> Docker: done Docker -->> Echo: start

PostgresDB Setup

This is the simplest step. According to the docs, this would be enough:

service:
...
  pg:
    container_name: pg
    image: postgres:13.1
    healthcheck:
      test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER
      timeout: 45s
      interval: 10s
      retries: 10
    restart: always
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: password
      POSTGRES_DB: docker
    volumes:
      - ./db:/docker-entrypoint-initdb.d/
    ports:
      - 5432:5432
...

Without going into Postgres setup specifics above configuration can be interpreted as:

  • service name is pg
  • container name is pg, too
  • use image postgres version 13.1
  • setup healthcheck
  • restart always container is it not running
  • set environment variables
  • attach volume
  • expose container port to the host

healthcheck

This instruction tells Docker how to test container health status. Container starts with starting status. After interval period test is executed. If in timeout period test return 0, then container is healthy. If test returns 1, or there is no response after timeout period test fails. Next test is executed after interval passes. If retries number of time test fails, container is considered unhealthy.

restart policy

The restart policy defines what to do when the container fails or stops.

environment

This instruction defines environment variables set in the container. The application running in the container will be able to read them.

volumes

The volumes mount a local file system within a container file system. In case above directory ./db will be mounted to container as /docker-entrypoint-initdb.d/. If ./db does not exist Docker will make it.

Liquibase Postgres

We could start the PG database and then run the Liquibase manually. But why?:

service:
...
  liquibase_pg:
    container_name: pg_updater
    image: liquibase/liquibase:4.7.0
    volumes:
      - ./liquibase:/liquibase/changelog
    command: liquibase --url="jdbc:postgresql://pg:5432/docker" --changeLogFile=./changelog/changelog.xml --username=root --password=password update
    depends_on:
      - pg
...

So, the new service is named liquibase_pg. As well it mounts volume /liquibase/changelog for the local directory ./liquibase. Then it executes command but only when service pg is healthy as it depends on pg service. The command is the Liquibase specific. It defines connection string, credentials, command and utilizes mounted volume.

Mounted volume should contain Liquibase changelog.

depends

This instruction defines a list of services that need to be running and healthy before Docker can start this one.

Application

services:
...
  app:
    container_name: echo
    build: .
    ports:
      - 9999:9999
    volumes:
      - ./configs:/app/configs
    depends_on:
      - liquibase_pg
    restart: always
...

This is some application developer is working on. It does not pull any image. Rather if the image does not exist it will be built from Dockerfile located in the path defined with build. And it will start only after Liquibase is started.

Nothing here that we did not already use.

Putting all together

The whole configuration can be found here.

In full setup, the application is run on MariaDB as well.

Resources