タケユー・ウェブ日報

Ruby on Rails や Flutter といったWeb・モバイルアプリ技術を武器にお客様のビジネス立ち上げを支援する、タケユー・ウェブ株式会社の技術ブログです。

DockerCompose + PostgreSQL Replication

  • Railsの複数データベース機能を開発環境でも有効にしたかった
  • docker-compose up でセットアップから起動まで一発で動くようにしたい

Docker Compose

docker-compose.yml

version: "3"
volumes:
  pg_primary_data:
    driver: local
  pg_readonly_data:
    driver: local
services:
  pg_primary:
    build:
      context: ./docker/pg
      dockerfile: Dockerfile.primary
    command: postgres -c log_destination=stderr -c log_statement=all
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - pg_primary_data:/var/lib/postgresql/data
  pg_readonly:
    build:
      context: ./docker/pg
      dockerfile: Dockerfile.readonly
    command: postgres -c log_destination=stderr -c log_statement=all
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - pg_readonly_data:/var/lib/postgresql/data
    depends_on:
      - pg_primary

docker/pg/Dockerfile.primary

FROM postgres:11-alpine

ENV LANG C.UTF-8

COPY ./setup-primary.sh /docker-entrypoint-initdb.d/setup-primary.sh
RUN chmod 0666 /docker-entrypoint-initdb.d/setup-primary.sh

docker/pg/setup-primary.sh

#!/bin/bash
set -e

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE ROLE replication_user LOGIN REPLICATION PASSWORD 'replicationpassword';
EOSQL

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
SELECT * FROM pg_create_physical_replication_slot('node_a_slot');
EOSQL

mkdir $PGDATA/archive

cat >> "$PGDATA/postgresql.conf" <<EOF
wal_level = hot_standby
max_wal_senders = 10
max_replication_slots = 10
synchronous_commit = off
EOF

echo "host replication replication_user 0.0.0.0/0 md5" >> "$PGDATA/pg_hba.conf"

docker/pg/Dockerfile.readonly

FROM postgres:11-alpine

ENV LANG C.UTF-8
ENV ENTRYKIT_VERSION 0.4.0

RUN wget https://github.com/progrium/entrykit/releases/download/v${ENTRYKIT_VERSION}/entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && tar -xvzf entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && rm entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && mv entrykit /bin/entrykit \
  && chmod +x /bin/entrykit \
  && entrykit --symlink

# ログ転送にscpを使用する
RUN apk --update add openssh-client && rm -rf /var/cache/apk/*

COPY ./setup-readonly.sh /setup-readonly.sh
RUN chmod +x /setup-readonly.sh

# https://github.com/docker-library/postgres/blob/primary/11/Dockerfile
ENTRYPOINT [ \
    "prehook", \
        "/setup-readonly.sh", \
        "--", \
    "docker-entrypoint.sh" \
]
CMD ["postgres"]

docker/pg/setup-primary.sh

#!/bin/bash
set -e

if [ ! -s "$PGDATA/PG_VERSION" ]; then
    echo "*:*:*:replication_user:replicationpassword" > ~/.pgpass
    chmod 0600 ~/.pgpass
    until ping -c 1 -W 1 pg_primary
    do
        echo "Waiting for primary to ping..."
        sleep 1s
    done

    until pg_basebackup -h pg_primary -D ${PGDATA} -U replication_user -vP -W
    do
        echo "Waiting for primary to connect..."
        sleep 1s
    done

    sed -i 's/wal_level = hot_standby/wal_level = replica/g' ${PGDATA}/postgresql.conf

    cat > ${PGDATA}/recovery.conf <<EOF
standby_mode = on
primary_conninfo = 'host=pg_primary port=5432 user=replication_user password=replicationpassword application_name=pg_readonly'
primary_slot_name = 'node_a_slot'
EOF

    chown postgres:postgres ${PGDATA} -R
    chmod 700 ${PGDATA} -R
fi

Rails で使う

https://github.com/takeyuweb/rails6-multidb-sample

docker-compose.yml

  app:
    build:
      context: ./docker/app
      dockerfile: Dockerfile.dev
    environment:
      DATABASE_HOST_PRIMARY: pg_primary
      DATABASE_HOST_READONLY: pg_readonly
      DATABASE_USER: postgres
      DATABASE_PASSWORD: password
    user: ruby
    command: ["bundle", "exec", "rails", "s", "-p", "3000", "-b", "0.0.0.0"]
    volumes:
      - .:/src
    ports:
      - 3000:3000
    tty: true
    stdin_open: true
    depends_on:
      - pg_primary
      - pg_readonly

config/database.yml

default: &default
  adapter: postgresql
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  # 仮にこれを `hoge:` にすると `schema.rb` は `hoge_schema.rb` になる。
  primary:
    <<: *default
    database: MyApp_development
    host: <%= ENV.fetch('DATABASE_HOST_PRIMARY') { 'db' } %>
    username: <%= ENV.fetch('DATABASE_USER') { 'postgres' } %>
    password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %>
  primary_readonly:
    <<: *default
    database: MyApp_development
    host: <%= ENV.fetch('DATABASE_HOST_READONLY') { 'db' } %>
    username: <%= ENV.fetch('DATABASE_USER') { 'postgres' } %>
    password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %>
    replica: true

test:
  primary:
    <<: *default
    database: MyApp_test
    host: <%= ENV.fetch('DATABASE_HOST_PRIMARY') { 'db' } %>
    username: <%= ENV.fetch('DATABASE_USER') { 'postgres' } %>
    password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %>
  primary_readonly:
    <<: *default
    database: MyApp_test
    host: <%= ENV.fetch('DATABASE_HOST_READONLY') { 'db' } %>
    username: <%= ENV.fetch('DATABASE_USER') { 'postgres' } %>
    password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %>
    replica: true

app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  connects_to database: { writing: :primary, reading: :primary_readonly }
end