One PostgreSQL container, multiple databases

2025 Nov 23

The need

I have multiple services that I develop in a single monorepo.

Almost all of these services need PostgreSQL to work and for clarity, I usually prefer having one database per service.

This is not a problem if you use your system's PostgreSQL but since I use Docker Compose for local development my PostgreSQL is a container using the official postgres image. That image does not support creating multiple databases by default.

Having one container per service is overkill so I searched for another way to do that.

The "standard" way

The official image supports adding initialization scripts that are run by the entrypoint which is enough for what I want.

The following is the script I'm using:

#!/bin/bash

set -e
set -u

function create_db_and_user() {
    local db_name=$1
    echo "Processing database and user '$db_name'"

    # 1. Create User (Idempotent: ignores "already exists" error)
    # We use a DO block to catch the duplicate_object error
    psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "postgres" <<-EOSQL
        DO \$\$
        BEGIN
            CREATE ROLE $db_name WITH LOGIN PASSWORD '$db_name' NOSUPERUSER NOCREATEDB;
        EXCEPTION WHEN duplicate_object THEN
            RAISE NOTICE 'Role % already exists, skipping create.', '$db_name';
        END
        \$\$;
EOSQL

    # 2. Create Database (Idempotent: checks pg_database first)
    # We check if the DB exists in the system catalog before trying to create it.
    if psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "postgres" -tAc "SELECT 1 FROM pg_database WHERE datname='$db_name'" | grep -q 1; then
        echo "  Database '$db_name' already exists. Skipping."
    else
        echo "  Creating database '$db_name'..."
        psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "postgres" <<-EOSQL
            CREATE DATABASE $db_name OWNER $db_name;
            GRANT ALL PRIVILEGES ON DATABASE $db_name TO $db_name;
EOSQL
    fi
}

if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then
    echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES"

    # Split the comma-separated variable into an array and iterate
    for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do
        create_db_and_user $db
    done

    echo "Multiple databases processed"
fi

Put this in a file named postgres-init-multiple-dbs.sh and make sure it's executable with chmod +x postgres-init-multiple-dbs.sh.

Now you can start the container like this:

docker run -d --name postgres \
  -v ./postgres-init-multiple-dbs.sh:/docker-entrypoint-initdb.d/init-multiple-dbs.sh:ro \
  -e POSTGRES_USER=foo \
  -e POSTGRES_PASSWORD=foo \
  -e POSTGRES_MULTIPLE_DATABASES=foo,bar,baz \
  postgres:17-alpine

This will create the following users and databases: foo, bar and baz.

With Docker Compose

Here's how I use this script in my compose.yaml file:

postgres:
  image: postgres:17-alpine
  environment:
    POSTGRES_USER: foo
    POSTGRES_PASSWORD: foo
    POSTGRES_MULTIPLE_DATABASES: foo,bar,baz
  ports:
    - "5432:5432"
  volumes:
    - postgres_data:/var/lib/postgresql/data
    - ./postgres-init-multiple-dbs.sh:/docker-entrypoint-initdb.d/init-multiple-dbs.sh:ro
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U foo -d postgres"]
    interval: 10s
    timeout: 5s
    retries: 5

This works well and does not require creating a custom PostgreSQL image. Problem solved, right?

With Github Actions

Not quite. In addition to Compose I'm also using a Github Actions workflow for my CI and the PostgreSQL requirements haven't changed.

Initially, I thought about using the jobs.<job_id>.services.<service_id>.volumes feature, just like with my compose.yaml file. However, this didn't work as I expected, I kept running into this error from the postgres container:

/usr/local/bin/docker-entrypoint.sh: line 185: /docker-entrypoint-initdb.d/init-multiple-dbs.sh: Is a directory

Turns out, the service containers are started by the runner before the code is even checked out, so they can never access the script.

The solution is simple though: build a custom PostgreSQL image. The following Dockerfile does the job:

FROM mirror.gcr.io/postgres:17-alpine

COPY postgres-init-multiple-dbs.sh /docker-entrypoint-initdb.d/init-multiple-dbs.sh
RUN chmod +x /docker-entrypoint-initdb.d/init-multiple-dbs.sh

Build and publish this image on a container registry, and you won't have to mess with mounting the script as a volume anywhere, simplifying everything.