Docker と MinIO を使った ActiveStorage 開発環境の構築
Ruby on Rails の ActiveStorage はデフォルトだとローカルではディスクに保存するようになっていますが、S3互換オブジェクトストレージである MinIO を使うことによって S3 を用いた場合と近い環境で開発することができます。このエントリーでは、Docker Compose と MinIO を使って、ActiveStorage の開発環境を構築する方法を紹介します。
MinIOを導入する
まず、MinIO を導入します。Dockerイメージ が用意されているので、これを使います。その場合、以下のようにします。
---
services:
rails:
...
ports:
- target: 3000
published: 3000
environment:
FILE_AWS_S3_BUCKET: file
FILE_AWS_S3_ENDPOINT: http://minio:9000
FILE_AWS_S3_FORCE_PATH_STYLE: true
FILE_AWS_ACCESS_KEY_ID: user
FILE_AWS_SECRET_ACCESS_KEY: password
minio:
image: minio/minio:RELEASE.2023-07-11T21-29-34Z
command: minio server /data --console-address ':9001'
ports:
- target: 9000
published: 9000
- target: 9001
published: 9001
volumes:
- type: volume
source: minio
target: /data
environment:
MINIO_ROOT_USER: user
MINIO_ROOT_PASSWORD: password
volumes:
minio:
local:
service: S3
access_key_id: <%= ENV.fetch('FILE_AWS_ACCESS_KEY_ID', '') %>
secret_access_key: <%= ENV.fetch('FILE_AWS_SECRET_ACCESS_KEY', '') %>
region: ap-northeast-1
#<% if ENV['FILE_AWS_S3_ENDPOINT'].present? %>
endpoint: <%= ENV.fetch('FILE_AWS_S3_ENDPOINT', '') %>
#<% end %>
bucket: <%= ENV.fetch('FILE_AWS_S3_BUCKET') %>
force_path_style: <%= ENV.fetch('FILE_AWS_S3_FORCE_PATH_STYLE', 'false') %>
public: true
ただし、この場合以下の問題があります。
-
http://file.minio:9000/xxxx
のように virtual-hosted style でアクセスできない- MinIO と S3 で場合分けする必要がある
- ブラウザからアクセスするときに
http://minio:9000/xxxx
でアクセスしようとするが、minio
の名前解決ができない-
/etc/hosts
で対処する必要がある
-
nip.io と network-scoped alias を使ってブラウザからも Rails コンテナからも MinIO コンテナにアクセスできるようにする
上記の問題は、nip.io と Docker の network-scoped alias を使うと解決できます。
nip.io とは
nip.io は、 <ip-address>.nip.io
もしくは <sub-domain>.<ip-address>.nip.io
とすることで、指定したIPアドレスに名前解決できるワイルドカードDNSサービスです。
$ dig 127.0.0.1.nip.io +short
127.0.0.1
$ dig 192.168.1.10.nip.io +short
192.168.1.10
$ dig www.127.0.0.1.nip.io +short
127.0.0.1
$ dig www.192.168.1.10.nip.io +short
192.168.1.10
network-scoped alias とは
通常、Docker Compose で起動したコンテナは、Docker内部の内蔵DNSとサービス名を使うことでほかのコンテナにアクセスすることができます。
$ docker compose run --rm rails dig minio +short
172.25.0.3
ここで、compose.yaml
でサービスを定義するときに network-scoped alias を指定すると、Dockerの内蔵DNSに登録され、その名前でもコンテナにアクセスすることができます。
---
services:
rails:
...
minio:
...
networks:
default:
aliases:
- my-minio
$ docker compose run --rm rails dig minio +short
172.25.0.3
$ docker compose run --rm rails dig my-minio +short
172.25.0.3
ブラウザからも Rails コンテナからも MinIO コンテナにアクセスできるようにする
上記を組み合わせて MinIO のドメインを minio.127.0.0.1.nip.io
とし、network-scoped alias で minio.127.0.0.1.nip.io
を登録すると、ブラウザからは nip.io で名前解決されたループバックアドレスと Docker の Port forwarding によって MinIO のコンテナにアクセスでき、コンテナ内部では Docker内蔵DNSによってコンテナのIPに名前解決されるため、こちらでも MinIO のコンテナにアクセスできます。また、MinIO を virtual-hosted style でアクセスできるようになります。
---
services:
rails:
...
ports:
- target: 3000
published: 3000
environment:
FILE_AWS_S3_BUCKET: file
FILE_AWS_S3_ENDPOINT: http://minio.127.0.0.1.nip.io:9000
FILE_AWS_ACCESS_KEY_ID: user
FILE_AWS_SECRET_ACCESS_KEY: password
minio:
...
ports:
- target: 9000
published: 9000
- target: 9001
published: 9001
environment:
MINIO_DOMAIN: minio.127.0.0.1.nip.io
MINIO_ROOT_USER: user
MINIO_ROOT_PASSWORD: password
networks:
default:
aliases:
- minio.127.0.0.1.nip.io
- file.minio.127.0.0.1.nip.io
local:
service: S3
access_key_id: <%= ENV.fetch('FILE_AWS_ACCESS_KEY_ID', '') %>
secret_access_key: <%= ENV.fetch('FILE_AWS_SECRET_ACCESS_KEY', '') %>
region: ap-northeast-1
#<% if ENV['FILE_AWS_S3_ENDPOINT'].present? %>
endpoint: <%= ENV.fetch('FILE_AWS_S3_ENDPOINT', '') %>
#<% end %>
bucket: <%= ENV.fetch('FILE_AWS_S3_BUCKET') %>
public: true
他の端末からアクセスしたときも MinIO コンテナにアクセスできるようにする
今のままだと、他の端末のブラウザからアクセスしたときに、MinIO のアドレスがループバッグアドレスとなってしまうため、うまくいきません。この場合は、nip.io のドメインのIPアドレス部分を docker compose up
しているPCのプライベートIPアドレスを指定することで解決できます。
---
services:
rails:
...
ports:
- target: 3000
published: 3000
environment:
FILE_AWS_S3_BUCKET: file
FILE_AWS_S3_ENDPOINT: http://minio.${HOST_IP:-127.0.0.1}.nip.io:9000
FILE_AWS_ACCESS_KEY_ID: user
FILE_AWS_SECRET_ACCESS_KEY: password
minio:
...
ports:
- target: 9000
published: 9000
- target: 9001
published: 9001
environment:
MINIO_DOMAIN: minio.${HOST_IP:-127.0.0.1}.nip.io
MINIO_ROOT_USER: user
MINIO_ROOT_PASSWORD: password
networks:
default:
aliases:
- minio.${HOST_IP:-127.0.0.1}.nip.io
- file.minio.${HOST_IP:-127.0.0.1}.nip.io
HOST_IP=192.168.1.10 docker compose up
MinIO を管理できる mc を使ってバケットの管理を楽にする
mc を導入する
MinIO にはWebコンソールが用意されているので、そこにアクセスすることでバケットを作成したりファイルをアップロードしたりできます。しかし、バケットを作り直してまっさらにした場合はコンソール画面からだとすべてのファイルを手動で削除しないといけないので面倒です。そのため、これを簡単にするために MinIO を管理できるCLIツールである mc を導入して簡単にできるようにします。
mc は Dockerイメージが用意されているのでこれを使います。ここでは、compose.yaml
で mc コンテナを起動して、MinIOコンテナに簡単にアクセスできるようにします。
まず、mc で minio
という名前で対象の MinIO コンテナにアクセスできるようにするために ~/.mc/config.json
に設定を記述します。
{
"version": "10",
"aliases": {
"minio": {
"url": "http://minio:9000",
"accessKey": "user",
"secretKey": "password",
"api": "s3v4",
"path": "auto"
}
}
}
---
services:
...
mc:
image: minio/mc:RELEASE.2023-07-11T23-30-44Z
volumes:
- type: volume
source: mc
target: /root/.mc
- type: bind
source: ./docker/mc/config.json
target: /root/.mc/config.json
volumes:
mc:
つぎに、mc は docker compose run mc
で実行できるようにしておきたいですが、mc
コンテナ起動時に minio
コンテナの起動およびデーモンの起動が完了している必要があるので、Docker のヘルスチェック機能と depends_on
を使います。ここで、MinIO には GET /minio/health/live
というヘルスチェックのためのエンドポイントが用意されているので、これを使います。mc
コンテナは depends_on
で minio
コンテナに対して service_healthy
を指定すれば minio
コンテナのヘルスチェックが通った後に起動します。
---
services:
...
minio:
...
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 1s
timeout: 20s
retries: 20
mc:
...
depends_on:
minio:
condition: service_healthy
このように指定することで、以下のように mc コマンドで MinIO コンテナのバケットを作ったり、削除したりできるようになります。
# file バケットを作成する
docker compose run --rm mc mb minio/file
# file バケットを削除する
docker compose run --rm mc rb --force minio/file
# file バケットを public アクセスできるようにする
docker compose run --rm mc anonymous set public minio/file
profiles を指定して docker compose up の時は mc コンテナを起動しないようにする
現在だと docker compose up
をすると mc
コンテナも同時に起動してしまいますが、profiles
を指定することで、通常は起動しないようにできます。 profiles
は本来の使い方だとオプショナルなサービスを起動するために指定するものですが、今回は docker compose up
で起動しないようにするために使います。
---
services:
...
mc:
...
profiles:
- tools
まとめ
以上で、Docker Compose と MinIO を使って、ActiveStorage の開発環境を構築する方法を紹介しました。compose.yaml
等はまとめると以下のようになります。
# direnv
export HOST_IP=127.0.0.1
---
services:
rails:
build:
context: .
dockerfile: docker/app/Dockerfile
init: true
depends_on:
db:
condition: service_healthy
minio:
condition: service_healthy
command: >
bash -c "
rm -rf tmp/pids/server.pid &&
rails s -b 0.0.0.0 -p 3000
"
ports:
- target: 3000
published: 3000
volumes:
- type: bind
source: .
target: /app
- type: volume
source: rails_bundle
target: /usr/local/bundle
- type: volume
source: rails_log
target: /app/log
- type: volume
source: rails_cache
target: /app/tmp/cache
environment:
DB_HOST: db
FILE_AWS_S3_BUCKET: file
FILE_AWS_S3_ENDPOINT: http://minio.${HOST_IP}.nip.io:9000
FILE_AWS_ACCESS_KEY_ID: user
FILE_AWS_SECRET_ACCESS_KEY: password
HOST_IP: ${HOST_IP}
db:
image: mysql:8
volumes:
- type: volume
source: mysql_data
target: /var/lib/mysql
ports:
- target: 3306
published: 3306
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 1
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
interval: 1s
timeout: 20s
retries: 20
minio:
image: minio/minio:RELEASE.2023-07-11T21-29-34Z
ports:
- target: 9000
published: 9000
- target: 9001
published: 9001
command: minio server /data --console-address ':9001'
environment:
MINIO_DOMAIN: minio.${HOST_IP}.nip.io
MINIO_ROOT_USER: user
MINIO_ROOT_PASSWORD: password
volumes:
- type: volume
source: minio
target: /data
networks:
default:
aliases:
- minio.${HOST_IP}.nip.io
- file.minio.${HOST_IP}.nip.io
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 1s
timeout: 20s
retries: 20
mc:
image: minio/mc:RELEASE.2023-07-11T23-30-44Z
profiles:
- tools
depends_on:
minio:
condition: service_healthy
volumes:
- type: volume
source: mc
target: /root/.mc
- type: bind
source: ./docker/mc/config.json
target: /root/.mc/config.json
volumes:
mysql_data:
rails_bundle:
rails_cache:
rails_log:
minio:
mc:
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
local:
service: S3
access_key_id: <%= ENV.fetch('FILE_AWS_ACCESS_KEY_ID', '') %>
secret_access_key: <%= ENV.fetch('FILE_AWS_SECRET_ACCESS_KEY', '') %>
region: ap-northeast-1
#<% if ENV['FILE_AWS_S3_ENDPOINT'].present? %>
endpoint: <%= ENV.fetch('FILE_AWS_S3_ENDPOINT', '') %>
#<% end %>
bucket: <%= ENV.fetch('FILE_AWS_S3_BUCKET') %>
public: true
{
"version": "10",
"aliases": {
"minio": {
"url": "http://minio:9000",
"accessKey": "user",
"secretKey": "password",
"api": "s3v4",
"path": "auto"
}
}
}
また、上記の方針で実装したサンプルが mrk21/rails-activestorage-minio-dev-sample にあります。
Discussion