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