- 投稿日:2020-05-23T21:05:38+09:00
Docker版Oracle11gのデータ永続化
Docker 版 Oracle11g のデータ永続化のやり方をメモる。
使用するイメージはこれ:https://registry.hub.docker.com/r/wnameless/oracle-xe-11g-r2やり方
- 空のコンテナを作る。
- コンテナからOracleのインストールフォルダをホストにまるごとコピーする。
- コンテナを一旦削除。
- mount をつけて再度ンテナを作る。
1、ます、コンテナをそのまま作る。
# mkdir oracle11g # cd oracle11g# docker run -d -p 49161:1521 \ -e ORACLE_ALLOW_REMOTE=true \ -e ORACLE_DISABLE_ASYNCH_IO=true \ -e TZ=Asia/Tokyo \ -v /etc/localtime:/etc/localtime:ro \ --name oracle11g \ wnameless/oracle-xe-11g-r2 \ && docker logs -f oracle11g以下のログが出力される。30秒~1分ぐらいかかるので、気長に待ちましょう。
f6c46d814f7db9cde443c78f122d91524e334e30cf44e5979836a906beae0649 Starting Oracle Net Listener. Starting Oracle Database 11g Express Edition instance. System altered. System altered. Shutting down Oracle Database 11g Express Edition instance. Stopping Oracle Net Listener. Starting Oracle Net Listener. Starting Oracle Database 11g Express Edition instance.ログが出たら、接続可能になります。確認して見てください。
Ctrl+c でログ監視を終了させ、docker ps -a で状況確認。# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES f6c46d814f7d wnameless/oracle-xe-11g-r2 "/bin/sh -c '/usr/sb乧" 4 minutes ago Up 4 minutes 22/tcp, 8080/tcp, 0.0.0.0:49161->1521/tcp oracle11g問題なさそうですね。あとは接続して試しにテーブルを作ったりして確認。(確認手順は省く)
2、コンテナ内のファイルをホスト側にコピーする。
# docker cp oracle11g:/u01/app/oracle .
フォルダの大きさを確認
# du -sh oracle 1.7G oracle
だったの1.7GB 、素晴らしい!
Oracle 12c は 10GB 以上必要あるから全然こっちの方が良い!!※ オプション
1つのフォルダぐらいを mount したいから、tmp フォルダを作っておく。特に必要ないなら、やらなくて良い。3、コンテナを一旦削除、docker ps -a で削除されたことを確認
# docker stop oracle11g && docker rm oracle11g oracle11g oracle11g # docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES4、もう一回コンテナを作るが、今度はmount付き
docker run -d -p 49161:1521 \ -e ORACLE_ALLOW_REMOTE=true \ -e ORACLE_DISABLE_ASYNCH_IO=true \ -e TZ=Asia/Tokyo \ -v $(pwd)/tmp/:/tmp/ \ -v $(pwd)/oracle/:/u01/app/oracle \ -v /etc/localtime:/etc/localtime:ro \ --name oracle11g \ wnameless/oracle-xe-11g-r2 \ && docker logs -f oracle11g説明すると
-v $(pwd)/tmp/:/tmp//tmp フォルダをマウント。ファイルをコンテナに転送する時は便利。
-v $(pwd)/oracle/:/u01/app/oracle1回目で作ったコンテナのOracleフォルダをマウント。これでデータ永続化可能なる。
-v /etc/localtime:/etc/localtime:ro
timezone をホスト機と同じにする。
再度接続したら、先程確認で作ったテーブルが存在するはず。
以上。
- 投稿日:2020-05-23T20:46:36+09:00
コンテナとハイパーバイザーの違いとは?
はじめに
今回は、コンテナとハイパーバイザーの違いについてアウトプットを込めて解説していきたいと思います。
この違いは、LPIC304にも試験範囲として出題されていました。
そのため、これから受験しようと思う人には参考になる記事だと思います。コンテナ(container)とは何か?
1つのOSの中にアプリケーションを格納する箱を複数用意するという考え方の仮想化技術になります。
図にするとこのような形になります。
特徴
- とにかく処理が軽量
- 一つのOS内でアプリケーションを動作させることができる
- コンテナ設定の再利用が可能
代表的なコンテナ技術
- docker
- Linuxコンテナ
ハイパーバイザー(Hypervisor)とは何か?
ハイパーバイザーとは、コンピューターを仮想化するためのソフトウェアです。
図にするとこのような形になります。
ハイパーバイザーという仮想化ソフトウェアの上に複数のOS(仮想マシン)が乗っているという構成になります。
特徴
- ハイパーバイザーの上に複数の仮想マシンを立てることができる
- それぞれ別のOSでサーバーを立てることができる
代表的なハイパーバイザー技術
- KVM
- OpenStack
- vSphere
- Hyper-V
- Xen
コンテナとハイパーバイザーの違いは?
アプリケーション起動速度
コンテナの方が高速です。
なぜなら、OSを一回一回立ち上げる必要がないからです。
試験的にdockerを構築後に「nginx」を導入したことがあります。
実際に「nginx」を起動させてみました。
結果、vSphere上でOSを起動させるよりも圧倒的に早かったです。(30秒程)このように、アプリケーション起動速度はコンテナの方が早いです。
OSのカスタマイズ性
こちらは、ハイパーバイザーの方が優れています。
なぜなら、コンテナ型は使用するOSが限られているからです。
一方、ハイパーバイザーはOS毎に箱が作成されます。「一方のサーバーのOSはLinuxにして、他はWindowsにする」
このような要件であれば、ハイパーバイザーの方が優れています。このように、OSカスタマイズ性を考えるとハイパーバイザーの方が使いやすいです。
参考記事
- 投稿日:2020-05-23T19:21:20+09:00
dockerfileでRUN sudo〜をつかうとbuildに失敗するときの対処
Dockerfileをbuild中・・
以下のエラーが発生しました。
翻訳すると「sudo: tty が存在せず、askpass プログラムが指定されていない」とのこと。sudo: no tty present and no askpass program specifiedエラーの箇所は・・?
rootユーザからhostと同名のユーザに切り替えて、
sudo
を使ってコマンドを実施している部分です。〜〜〜〜上記略 USER $HOME_USER RUN sudo apt-get install -y \ 〜〜〜〜以下略原因・対応
原因を調べたところ、
sudo
するときにpassword要求されていることにあるらしいです。
なので、sudoersにpasswordなしの設定を加えることにしました。コンテナの中の
/etc/sudoers
をDockerfileと同じ階層にコピーして、最終行に以下を追記します。
(myusernameは$HOME_USERの値)# /etc/sudoerの最終行に以下を追記 myusername ALL=(ALL) NOPASSWD: ALLDokerfileに
ADD sudoers /etc
を追記〜〜〜〜上記略 ADD sudoers /etc USER $HOME_USER RUN sudo apt-get install -y \ 〜〜〜〜以下略これで解決・・?
これで
sudo
するときにパスワードが不要になり、buildし直すとエラー自体は発生しなくなりました。注意
ただし、個別ユーザにsudoするときに全権を与えるのでセキュリティ的には危険です。
さらに気になって調べた所、Dockerfile のベストプラクティスのUSERの項目に以下の記載がありました。TTY やシグナル転送を使わないつもりであれば、 sudo のインストールや使用を避けたほうが良いでしょう。使うことで引き起こされる問題の解決は大変だからです。もし、どうしても sudo のような機能が必要であれば(例:root としてデーモンを初期化するが、実行は root 以外で行いたい時)、 「 gosu 」を使うことができます。
結論
というわけで、Dockerfile内ではsudoは使わずに、厳密にUSERコマンドで適宜切り換えていくやり方がいいかもしれません。
参考
- 投稿日:2020-05-23T19:09:37+09:00
Docker / ECR / ECS コンテナ入門まとめ③
前編
参考文献
- AWS CLI で Amazon ECR に docker イメージを push する
- Amazon EC2 Container Service (ECS)を試してみた
- CircleCI+ECS+ECR環境でDockerコンテナのCD(継続的デプロイ)環境を構築する -前編-
- CircleCI+ECS+ECR環境でDockerコンテナのCD(継続的デプロイ)環境を構築する -後編-
前準備
◆ Dockerインストール
$ sudo yum update -y $ sudo amazon-linux-extras install docker◆ Docker起動
$ sudo systemctl start docker◆ dockerグループにユーザ追加
$ sudo usermod -a -G docker ec2-user◆ dockerアクセス確認
$ docker infoECR編
◆ ECRログイン
$ aws ecr get-login --no-include-email ※出力される長いコマンドでログイン◆ ECRリポジトリ作成
$ aws ecr create-repository --repository-name sample ※リポジトリ名◆ 環境変数セット
$ export registryId=[出力されたレジストリId] $ export region=ap-northeast-1 ※リージョン名 $ export imagename=sample $ export tag=ver1◆ Dockerイメージ作成
$ docker build -t ${imagename}:${tag} .◆ Dockerイメージtag付け
$ docker tag ${imagename}:${tag} ${registryId}.dkr.ecr.${region}.amazonaws.com/${imagename}:${tag}◆ Dockerイメージプッシュ
$ docker push ${registryId}.dkr.ecr.${region}.amazonaws.com/${imagename}:${tag}ECS編
◆ IAMロールの作成
- 「AmazonEC2ContainerServiceforEC2Role」ポリシーの付与
ecs-role{ "Version": "2020-5-23", "Statement": [ { "Action": "ecs:*", "Effect": "Allow", "Resource": "*" } ] }◆ クラスター作成コマンド
$ export region=us-east-1 $ export cluster=sample-cluster $ aws ecs create-cluster --region ${region} --cluster-name ${cluster} $ aws ecs list-clusters --region ${region} --output json◆ ec2作成
- AMI選択:ami-34ddbe5c
- IAMロール:上記のecs-roleを使用
- ユーザデータ:下記入力
userdata#!/bin/bash echo ECS_CLUSTER=[クラスター名] >> /etc/ecs/ecs.config◆ ECS/EC2紐付け確認
$ aws ecs list-container-instances --cluster ${cluster} --output json --region ${region} $ ssh ec2-user@XX.XX.XX.XX ※ec2ログイン $ sudo docker psECSタスク登録・実行
$ vim sample.jsonsample.json[ { "environment": [], "name": "sample", "image": "sample-image", "cpu": 10, "portMappings": [], "entryPoint": [ "/bin/sh" ], "memory": 10, "command": [ "sample", "360" ], "essential": true } ]■ タスク登録コマンド
$ aws ecs register-task-definition --family sample --container-definitions file://sample.json --region ${region} --output json■ タスク確認
$ aws ecs list-task-definitions --region ${region}■ タスク実行コマンド
$ aws ecs run-task --cluster ${cluster} --task-definition sample:1 --count 1 --region ${region} --output json■ タスク実行確認
$ sudo docker ps
- 投稿日:2020-05-23T18:19:14+09:00
素人基盤エンジニアがDockerでDjangoを触るシリーズ⑤:View, Template, CSS
TL;DR
素人基盤エンジニアがDockerでDjangoを触るシリーズ④:urls.pyの続き。
1から読みたい場合は↓こちら。
素人基盤エンジニアがDockerでDjangoを触るシリーズ①:DockerでDjangoを作る
今回もDjango Girlsのサンプルを作りながらdjangoと戯れる。
viewでは、modelを介して受け取ったデータをTemplateに渡したり、その他処理を行うことができる。
Templateは見た目を整える部分。htmlやCSSなどはこのTemplateにあたる。
まずは、シンプルなViewを作ることでその構造を見ていく。view
①views.pyを編集する
views.py
views.pyfrom django.shortcuts import render from django.utils import timezone from .models import Post def post_list(request): posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date') return render(request, 'blog/post_list.html', {'posts': posts})よくわかる解説
from django.shortcuts import rendershortcuts関数は、DjangoにおけるMTVの各レベルを橋渡しするのに必要な関数やクラスを定義している。今回は、引数として指定したtemplateをレンダしてHttpResponseを返すrenderという関数をインポートしている。
from django.utils import timezone from .models import Postユーティリティ関数に含まれている時間に関する機能を提供するtimezoneと、モデルの作成の時に作成したPostのモデルをインポートしている。Viewの立ち位置については第二回の時にも触れたが、モデル関数を利用して実際にデータを取得し、Templateに渡す役割もあるので、利用したいモデルはViewの中でインポートし、Templateのオプションとして引き渡す必要がある。
def post_list(request): posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date') return render(request, 'blog/post_list.html', {'posts': posts})post_list関数の定義部分。前回のurls.pyを記述した際に、
views.post_list
を指定したのは覚えているだろうか。urls.pyへリクエストが来た際に、パスに応じて指定したviews.pyの中の関数が呼び出されリクエストが転送される。そして、必要な処理を行った後にTemplateをレンダリングしてレスポンスを返すということである。二行目では、Postモデルを使ってpublished_dateが現在の時間よりも前の投稿を取得し、新しい順に並び変えた上でblog/post_list.html
に渡している。Template
①html用のディレクトリを作成する
cd ~/blog/template mkdir blogアプリケーションのディレクトリであるblogディレクトリの下に、templateというディレクトリがあるので、その下にさらにblogというディレクトリを作成する。
なぜこんなややこしい構造にするのかと思うかもしれないが、大規模なアプリケーションを管理しようとするとhtmlに関しても機能ごとにディレクトリを分けておいたほうがよいらしい。②post_list.htmlを編集する
post_list.html
blog/template/blog/post_list.html{% load static %} <html> <head> <title>Django Girls blog</title> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css"> <link rel="stylesheet" href="{% static 'css/blog.css' %}"> </head> <body> <div class="page-header"> <h1><a href="/">Django Girls Blog</a></h1> </div> <div class="content container"> <div class="row"> <div class="col-md-8"> {% for post in posts %} <div class="post"> <div class="date"> <p>published: {{ post.published_date }}</p> </div> <h2><a href="">{{ post.title }}</a></h2> <p>{{ post.text|linebreaksbr }}</p> </div> {% endfor %} </div> </div> </div> </body> </html>HTMLの基本的な部分を解説するのは主旨とずれるので、ここでは重要な部分のみを解説する。
よくわかる解説
{% load static %}{% load static %}は後述するCSSファイルのような、静的コンテンツを読み込むための記述。
<link rel="stylesheet" href="{% static 'css/blog.css' %}">ここで読み込むCSSを指定している。
{% for post in posts %} <div class="post"> <div class="date"> <p>published: {{ post.published_date }}</p> </div> <h2><a href="">{{ post.title }}</a></h2> <p>{{ post.text|linebreaksbr }}</p> </div> {% endfor %}この部分で注目したいのは、{{ post.OBJECT }}でPostモデルの項目を指定している部分。Postモデルを利用して取得した投稿に関しては、前述したviews.pyの中でこのTemplateに渡されているため、このように簡単に呼び出せるということだ。Postモデルで取得した投稿はリストの形になっているため、for文を利用して複数の表示を実現することもできる。
③CSS用のディレクトリを作成する
cd ~/blog/static mkdir css④blog.cssを編集する
blog/static/css/blog.css.page-header { background-color: #C25100; margin-top: 0; padding: 20px 20px 20px 40px; } .page-header h1, .page-header h1 a, .page-header h1 a:visited, .page-header h1 a:active { color: #ffffff; font-size: 36pt; text-decoration: none; } .content { margin-left: 40px; } h1, h2, h3, h4 { font-family: 'Lobster', cursive; } .date { color: #828282; } .save { float: right; } .post-form textarea, .post-form input { width: 100%; } .top-menu, .top-menu:hover, .top-menu:visited { color: #ffffff; float: right; font-size: 26pt; margin-right: 20px;![006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369619/5f4c27cb-8697-0236-f655-2ec2beaf3805.png) } .post { margin-bottom: 70px; } .post h2 a, .post h2 a:visited { color: #000000; }CSSの解説についてはさらに本筋ではないので、形を整えるためにはこんなものを書くんだというくらいの認識でコピペいただければと思う。HTML, CSSに関してはそれ専用の本などで勉強されることをお勧めする。
ブログ表示用サイトの起動
docker-compose up
ここまでで、(表示をするのみだが)webサイトが完成しているはずである。
dockercompose.yamlが配置されているディレクトリまで移動して、docker-compose up
を使って実際に起動してみよう。docker-compose uphttp://[IPADDRESS]:8000/にアクセスしてみる。
表示された!
ちなみにThis is test postという投稿は、第三回で説明したadminページを利用して投稿したもの。ユーザがポストを行うには、投稿フォームを備えたページを作成する必要がある。フォームを作成するにあたって、Djangoはformsという便利な機能を提供している。その話はまた次回。まずは一区切りといった感じだろうか。。
つづく
- 投稿日:2020-05-23T18:00:40+09:00
VSCodeでリモートホスト上のDocker上で開発する
経緯
開発環境がローカルマシンの環境に依存するのって嫌ですよね。
というわけで最近はもっぱらDocker上に開発環境を構築して、VSCodeのRemote-Containersで開発しています。ただ、使ってるメインマシンがWindowsなのでDockerの使い勝手がいまいち良くない。。。
そこで、押入れサーバの仮想環境(ESXi)上にDocker専用のホストを立てて、そこにWinマシンのVSCodeから接続して開発できるようにしたかったわけです。
ESXi上にDocker用のホストOSをインストールする
DockerのホストOSにはPhotonOSを使います。
VMWareが提供している、VMware vSphereに最適化された最小限のLinuxコンテナホストです。こんなに、自分の要望をどストライクで満たすOSがあるとはびっくりです。
PhotonOSのダウンロード
ここからOVAファイルをダウンロードしてきます。
今回はOVA with virtual hardware v13 (UEFI Secure Boot)
を選択しました。ESXiにPhotonOSの仮想マシンを作成
PhotonOSの設定
root / changeme
でログイン。パスワードの変更を求められるので対応する。# アップデート tdnf update -y # tarのインストール tdnf install -y tar # SSHポートフォワードの許可 vi /etc/ssh/sshd_config > AllowTcpForwarding yes systemctl restart sshd # 一般ユーザの作成 useradd -m -g docker -G wheel user passwd user # dockerの自動起動設定 systemctl enable docker systemctl start docker # docker動作確認 docker run --rm hello-worldVSCodeの設定
前提条件
- Windows環境
- Dockerがインストールされている(クライアントだけ使うのでDockerホストは起動していなくてもOK)
- VSCodeに以下の拡張が入っている
- Docker(ms-azuretools.vscode-docker)
- Remote Development(ms-vscode-remote.vscode-remote-extensionpack)
SSH接続の設定 1
https://code.visualstudio.com/docs/containers/ssh
公開鍵を登録してパスワードなしで接続できるようにする
接続元(windows)# 鍵ペアの生成 [^1] ssh-keygen -t ecdsa -b 521 # 公開鍵をリモート側に転送 scp .ssh/id_ecdsa.pub user@remote-host:接続先(photon)# ローカル側の公開鍵を登録 cat id_ecdsa.pub >> .ssh/authorized_keys rm id_ecdsa.pub管理者権限で以下実行して
ssh-agent
サービスが自動起動するようにするsc config ssh-agent start=auto net start ssh-agent ssh-add .ssh\id_ecdsaDockerのリモートホストの設定
VSCodeの設定から
docker.host
にDockerホストサーバへの接続情報を設定ssh://user@remote-hostうまく行けば、リモートホスト側のDockerの情報が表示されます。
Remote-Containerでのファイル共有設定
残念ですが、Dockerはリモートサーバ上にいるので、ローカルのファイルをDocker側に共有することはできません。
ローカルからファイルをコピーしたい場合はSCPなどで転送するのがよいでしょう。NAS上にプロジェクトを作ってしてしまうのもありかと思います。
リモートホスト側のフォルダにDocker内のファイルを共有する場合
ローカルマシン側の
devcontainer.json
の共有設定を以下のようにする。"workspaceMount": "source=/absolute/path/on/remote/machine,target=/workspace,type=bind,consistency=cached"所感
やりたいことがうまく実現できて満足。
開発に使うリソースはすべてサーバ上で完結し、ローカルの環境にはVSCodeとDockerのクライアントのみでよいのでスペックの低いマシンでも存分に開発ができます。
例えば、ノートとデスクトップの2台を持っている場合、どちらからでも同じ開発環境が使えるわけです。
これって、かなり素晴らしいと思うのでみなさんも
Windows 10 build 1909 以前の場合、RSAではなくECDSAをつかう必要がある
https://code.visualstudio.com/docs/containers/ssh#_tips ↩
- 投稿日:2020-05-23T16:19:50+09:00
windows10 HomeでDockerを使う
プロローグ
dockerを使いたいのですが、自宅環境がWindows10 homeです。
通常のdocker for windowsはHyper-vが必要ですが、windows10 homeにはhyper-vが導入されていません。
調べるとdocker toolboxを利用するとwindows10 homeでもdockerが使えるようです。docker toolboxの導入
docker toolboxの導入方法はこちらの公式に記載がありますので、参考にします。
exeファイルをGithubからダウンロードするようです。
インストール対象を選択します。(ついでにGitを入れました)
内容に問題がなければそのまま「Install」を押下します。
確認メッセージがでましたが、そのまま「OK」を押下しました。
インストールすると「Docker Quickstart Terminal」というショートカットが作成されました。
実行します。
動作確認
公式が用意している動作確認用のHello-worldコンテナを呼び出してみます。
Hello from Docker!と表示されたら完了です。
docker run hello-world
- 投稿日:2020-05-23T16:08:15+09:00
Rails+Webスクレイピング+DockerのアプリをEC2で立てようとしたら、デフォルト設定だとt2.small(2GB)とかが必要になりそう
※情弱なのでデフォルトのメモリ設定をしてしまってますが、もしかしたらDockerのメモリ割り当てをうまくやればもっと小さいサイズでいけるかもしれません。
概要
dockerでWebサイト(Rails + Webスクレイピング)をAWSのEC2で立てたいと思っていたんですが、自分しか使わないような遊びサイトだったので、できるだけ安い金額で立てられたらと思っていたんですが、見事に撃沈した話です。
結論
デフォルト設定(Docker、Rails)だとt2.smallを使わないと、
bundle install
のnokogiri
を入れるときにメモリエラーになります。※この記事のようにDockerを動かすだけならt2.nanoでも動きます。
※t2.smallならスクレイピング(docker-selenium)も動きました詳細
どんな環境?
Docker: 19.03.6-ce
docker-compose: 1.25.5
ruby:2.5.3 (Dockerhubのruby:2.5.3
のイメージを使用)
Rails: 5.2.3AWSにDockerを入れるのにはこちらを参考にさせてもらいました。
どんなことが起きたの?
t2.nanoとt2.microのdocker環境内で
bundle install
をしたら以下エラーが起きました。
(どうでもいいですが、ローカル環境で動いていたDockerfileでbundle install
したときに以下エラーが起き、まだイメージが出来上がる前だったので、わざわざDockerfileのbundle install
やdocker-compose.ymlのコマンドからbundle install
やそれに依存したrails s
をコメントアウトしてdocker-compose.ymlでtail -f Gemfile
をしてコンテナ延命した状態で以下のように独立してbundle install
を実行しました。docker-composeに慣れてdocker run
が打てない人間の悲しい性です)# bundle install The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`. Fetching gem metadata from http://rubygems.org/............ Fetching rake 12.3.3 Installing rake 12.3.3 Fetching concurrent-ruby 1.1.5 Installing concurrent-ruby 1.1.5 Fetching i18n 1.6.0 Installing i18n 1.6.0 Fetching minitest 5.11.3 Installing minitest 5.11.3 Fetching thread_safe 0.3.6 Installing thread_safe 0.3.6 Fetching tzinfo 1.2.5 Installing tzinfo 1.2.5 Fetching activesupport 5.2.3 Installing activesupport 5.2.3 Fetching builder 3.2.3 Installing builder 3.2.3 Fetching erubi 1.8.0 Installing erubi 1.8.0 Using mini_portile2 2.4.0 Fetching nokogiri 1.10.3 Installing nokogiri 1.10.3 with native extensions Gem::Ext::BuildError: ERROR: Failed to build gem native extension. current directory: /usr/local/bundle/gems/nokogiri-1.10.3/ext/nokogiri /usr/local/bin/ruby -I /usr/local/lib/ruby/site_ruby/2.5.0 -r ./siteconf20200523-12-t3a56p.rb extconf.rb checking if the C compiler accepts ... yes Building nokogiri using packaged libraries. Using mini_portile version 2.4.0 checking for gzdopen() in -lz... yes checking for iconv... yes ************************************************************************ IMPORTANT NOTICE: Building Nokogiri with a packaged version of libxml2-2.9.9 with the following patches applied: - 0001-Revert-Do-not-URI-escape-in-server-side-includes.patch - 0002-Remove-script-macro-support.patch - 0003-Update-entities-to-remove-handling-of-ssi.patch Team Nokogiri will keep on doing their best to provide security updates in a timely manner, but if this is a concern for you and want to use the system library instead; abort this installation process and reinstall nokogiri as follows: gem install nokogiri -- --use-system-libraries [--with-xml2-config=/path/to/xml2-config] [--with-xslt-config=/path/to/xslt-config] If you are using Bundler, tell it to use the option: bundle config build.nokogiri --use-system-libraries bundle install Note, however, that nokogiri is not fully compatible with arbitrary versions of libxml2 provided by OS/package vendors. ************************************************************************ Extracting libxml2-2.9.9.tar.gz into tmp/x86_64-pc-linux-gnu/ports/libxml2/2.9.9... OK Running git apply with /usr/local/bundle/gems/nokogiri-1.10.3/patches/libxml2/0001-Revert-Do-not-URI-escape-in-server-side-includes.patch... OK Running git apply with /usr/local/bundle/gems/nokogiri-1.10.3/patches/libxml2/0002-Remove-script-macro-support.patch... OK Running git apply with /usr/local/bundle/gems/nokogiri-1.10.3/patches/libxml2/0003-Update-entities-to-remove-handling-of-ssi.patch... OK Running 'configure' for libxml2 2.9.9... OK Running 'compile' for libxml2 2.9.9... OK Running 'install' for libxml2 2.9.9... OK Activating libxml2 2.9.9 (from /usr/local/bundle/gems/nokogiri-1.10.3/ports/x86_64-pc-linux-gnu/libxml2/2.9.9)... ************************************************************************ IMPORTANT NOTICE: Building Nokogiri with a packaged version of libxslt-1.1.33 with the following patches applied: - 0001-Fix-security-framework-bypass.patch Team Nokogiri will keep on doing their best to provide security updates in a timely manner, but if this is a concern for you and want to use the system library instead; abort this installation process and reinstall nokogiri as follows: gem install nokogiri -- --use-system-libraries [--with-xml2-config=/path/to/xml2-config] [--with-xslt-config=/path/to/xslt-config] If you are using Bundler, tell it to use the option: bundle config build.nokogiri --use-system-libraries bundle install ************************************************************************ Extracting libxslt-1.1.33.tar.gz into tmp/x86_64-pc-linux-gnu/ports/libxslt/1.1.33... OK Running git apply with /usr/local/bundle/gems/nokogiri-1.10.3/patches/libxslt/0001-Fix-security-framework-bypass.patch... OK Running 'configure' for libxslt 1.1.33... OK Running 'compile' for libxslt 1.1.33... OK Running 'install' for libxslt 1.1.33... OK Activating libxslt 1.1.33 (from /usr/local/bundle/gems/nokogiri-1.10.3/ports/x86_64-pc-linux-gnu/libxslt/1.1.33)... checking for -llzma... yes checking for xmlParseDoc() in libxml/parser.h... yes checking for xsltParseStylesheetDoc() in libxslt/xslt.h... yes checking for exsltFuncRegister() in libexslt/exslt.h... yes checking for xmlHasFeature()... yes checking for xmlFirstElementChild()... yes checking for xmlRelaxNGSetParserStructuredErrors()... yes checking for xmlRelaxNGSetParserStructuredErrors()... yes checking for xmlRelaxNGSetValidStructuredErrors()... yes checking for xmlSchemaSetValidStructuredErrors()... yes checking for xmlSchemaSetParserStructuredErrors()... yes creating Makefile current directory: /usr/local/bundle/gems/nokogiri-1.10.3/ext/nokogiri make "DESTDIR=" clean current directory: /usr/local/bundle/gems/nokogiri-1.10.3/ext/nokogiri make "DESTDIR=" make failedCannot allocate memory - make "DESTDIR=" Gem files will remain installed in /usr/local/bundle/gems/nokogiri-1.10.3 for inspection. Results logged to /usr/local/bundle/extensions/x86_64-linux/2.5.0/nokogiri-1.10.3/gem_make.out An error occurred while installing nokogiri (1.10.3), and Bundler cannot continue. Make sure that `gem install nokogiri -v '1.10.3' --source 'http://rubygems.org/'` succeeds before bundling. In Gemfile: rails was resolved to 5.2.3, which depends on actioncable was resolved to 5.2.3, which depends on actionpack was resolved to 5.2.3, which depends on actionview was resolved to 5.2.3, which depends on rails-dom-testing was resolved to 2.0.3, which depends on nokogiri長すぎて何がエラーかわからないので、見ろと言われたログを見てみます
# cat /usr/local/bundle/extensions/x86_64-linux/2.5.0/nokogiri-1.10.3/gem_make.out current directory: /usr/local/bundle/gems/nokogiri-1.10.3/ext/nokogiri /usr/local/bin/ruby -I /usr/local/lib/ruby/site_ruby/2.5.0 -r ./siteconf20200523-53-7krva6.rb extconf.rb extconf failedCannot allocate memory - /usr/local/bin/rubyここでメモリ不足だとわかりましたorz
そりゃ500MBでdockerだもんなまとめ
月2000円は痛いんですが、仕方ないかぁといった感じです。
誰か同じ状況の人やAWSに関しての意思決定で同じ問題意識を持った人の一助になれば嬉しいです。
- 投稿日:2020-05-23T13:38:59+09:00
WSL 2 対応 Docker Desktop for Windowsを使うための手順
概要
Windows 10 で WSL 2 (Windows Subsystem for Linux)の動作要件を満たしている状態で、Docker Desktop を使えるようにするまでの流れを整理しました。ざっくり書きますと「Your system supports WSL 2!」が表示された時、WSL 2 に移行するための手順です。
動作環境を満たすと、Docker Desktop 起動時、この表示が出ます。
※ 動作環境は、Winddows 10 Inside Program 等で動作要件を満たすか、あるいは GA 版の提供を待つ必要があります。
※ WSL1 や WSL を使わないバージョンからのデータは自動的に移行されません。この投稿は、公式の WSL 2 ドキュメント を読んでも、このままでは Docker for Desktop を動かすに至らなかったので、状況整理のために作成しました。
動作要件
WSL2 が動作する Windows 10 ( ※詳細 )
バージョン 2004、ビルド 19041 以上に更新された Windows 10 を実行している。
本原稿執筆時点では、 Inside Program に加入する必要があります。
手順
全く WSL に対応していないシステムで、WSL 2 対応の Docker Desktop for Windows を使うための手順です。
動作要件を満たすかどうかの確認
Docker Desktop for Windows を起動し、次のウインドウが自動的に出ることを確認します。おそらく、システムが対応すると、意図せず次の画面が出ます。
もし、データ移行の準備が出来ていなかったり、WSL 2 に対する理解が十分でなければ「Now now」をクリックし、現時点では WSL 2 に移行すべきではありません。
ここで「Enable WSL 2」(WSL 2)を有効化するをクリックすると、システム状態のチェックに入ります。デフォルトの Windows 10 では WSL は入っていないため、「WSL 2 is not installed」(WSL 2 はインストールされていません)と表示が出ています。
ここでセットアップ作業を進行するには「 Stop Docker 」をクリックします。
WSL 2 のセットアップ
ここは基本的に 公式ドキュメント の通り作業をします。
「PowerShell」を管理者として実行します。Windows ボタンを押し、「PowerShell」と入力すると実行するプログラム候補が自動的に出ますので、「管理者として実行」をクリックします。
次に PowerShell が起動します。
WSL をセットアップするコマンド
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
を実行します。
公式ドキュメントでは「wsl --set-default-version 2」の実行とありますが、
PS C:\WINDOWS\system32> wsl --set-default-version 2 wsl : 用語 'wsl' は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されません。 名前が正しく記述されていることを確認し、パスが含まれている場合はそのパスが正しいことを確認してから、再試行してください 。 発生場所 行:1 文字:1 + wsl --set-default-version 2 + ~~~ + CategoryInfo : ObjectNotFound: (wsl:String) [], CommandNotFoundException + FullyQualifiedErrorId : CommandNotFoundExceptionこのように
wsl
のパスが通っていない旨の表示が出ます。PC を再起動します。
再起動後 WSL 2 Linux カーネルの更新が必要です( 詳細 )。
マイクロソフトのドキュメントを確認します。ウェブブラウザで、次の URL を開きます。
- WSL 2 Linux カーネルの更新 | Microsoft Docs
この中の「x64 マシン用の最新の WSL2 Linux カーネル更新プログラム パッケージをダウンロードしてください。」のリンク先をクリックします。
「wsl_update_x64.msi」のダウンロードが開始しますので、ファイルを保存して、ダウンロードが終わったら実行します。
以上が WSL 2 のシステムまわりの設定です。
Linux ディストリビューションのセットアップ
次は linux ディストリビューションを Windows 10 にセットアップします。Windows 10 では「アプリ」として Linux ディストリビューションが配布されています。WSL 2 で Docker を使うには、Ubuntu など Linux ディストリビューションをセットアップする必要があります( 詳細 )。
まずは、 Microsoft Store を開きます。
それから「Ubuntu 」を選択し、「入手」をクリックします。
ダウンロードが完了すると、 Windows のスタートメニューをクリックし、「Ubuntu」と入力します。
暫く、次の表示が出るまで待ちます。
Installing, this may take a few minutes... Please create a default UNIX user account. The username does not need to match your Windows username. For more information visit: https://aka.ms/wslusers Enter new UNIX username:ここで、セットアップする Ubuntu のユーザ名とパスワードを指定します。
Enter new UNIX username: docker New password: Retype new password:ここでは
docker
と入力し(何でも構いません)、次に、パスワードを設定しています。そうすると、Ubuntu にログインできます。
Installation successful! To run a command as administrator (user "root"), use "sudo <command>". See "man sudo_root" for details. Welcome to Ubuntu 20.04 LTS (GNU/Linux 4.19.84-microsoft-standard x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage System information as of Sat May 23 12:25:38 JST 2020 System load: 0.13 Processes: 8 Usage of /: 0.4% of 250.98GB Users logged in: 0 Memory usage: 0% IPv4 address for eth0: 172.29.185.157 Swap usage: 0% 0 updates can be installed immediately. 0 of these updates are security updates. The list of available updates is more than a week old. To check for new updates run: sudo apt update This message is shown once once a day. To disable it please create the /home/docker/.hushlogin file.この状態では特にすることもないので、
exit
を入力し、終了します。PowerShell で WSL2 の設定
それから、再び PowerShell で管理者モードとして、実行するべき WSL のバージョンを
2
と明示します。wsl --set-default-version 2Docker Desktop の確認
あとは Docker Desktop を起動します。
画面上、「Skip tuotrial」を選び、起動した画面の右から3つめに「歯車」のマークがあり、そこをクリックすると
User the WSL 2 based engine
にチェックが入っていれば、対応完了です。もし入っていなければ、チェックをいれます。この状態で、Docker は PowerShell 上で使えますので、「dokcer pull hello-world」や「docker run hello-world」などが使えます。
参考文章
- Windows Subsystem for Linux (WSL) を Windows 10 にインストールする | Microsoft Docs
- 投稿日:2020-05-23T12:43:27+09:00
DockerでApache + Laravel の環境を構築する時にNot Foundで泣いた話
この記事の対象者
DockerでApache + Laravelの環境を作りたい初心者(←重要)
Laravelで開発してるけど青売りケーションの仕組みを実はよくわかっていない初心者
DockerもLaravelも長年使っていて未熟者のワシにアドバイスくれる先輩
ソースコードはここに載せてます
https://github.com/maip0902/DockerApachePHP環境
MacOS
Docker 19.03.2なんで泣いたのか
Laravel + nginx の構成でしか開発を行ったことがなく、初めてApacheで構築しようとしたら
ドキュメントルート違ってるやーん ><
ということでこれを直します
せっかくなのでどういう仕組みでアプリケーションの画面が表示されるかにも少し触れながら環境構築を行います〜ディレクトリ構成
apache-laravel-app という名前のプロジェクトを作成するものとして、最終的に以下のようなディレクトリ構造になります。
├ apache-laravel-app // Laravelで作られるファイル | ├ ── php | └ php.ini // なくてもいい | ├ Dockerfile | ├ docker-compose.ymlDockerfile
今回使うイメージは
php:7.2-apache
です。Dockerfile# ① FROM php:7.2-apache # ②ドキュメントルートをデフォルトから修正 ENV APACHE_DOCUMENT_ROOT /var/www/html/apache-laravel-app/public RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf # php.iniを独自で設定するときだけ COPY ./phhp/php.ini /etc # ③モジュールのinstall RUN apt-get update \ && apt-get install -y zlib1g-dev libpq-dev unzip git vim curl\ && docker-php-ext-install pdo_mysql zip # ④Composer入れる # マルチステージビルドを採用cpm COPY --from=composer /usr/bin/composer /usr/bin/composer① 公式からイメージを持ってくる
FROM php:7.2-apachephp:(version)-apacheを使うとapacheにphpのモジュールが入っている状態を持ってこれます。
② ドキュメントルートの修正
ENV APACHE_DOCUMENT_ROOT /var/www/html/apache-laravel-app/public RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf今回のポイント。
apacheの設定ファイルは/etc/apache2
配下にあるのですが、その中でもドキュメントルートを指定しているファイルがあって、その部分のみ環境変数で置き換えるということをしました。Apche起動後にドキュメントルート書き換えたり、独自の.confを使ってRUN rm /etc/apache2/sites-available/000-default.conf COPY ./000-default.conf etc/apache2/sites-availableともできるんですけど(最初自分もこれ考えましたが)環境変数にぶっこんだ方がスマートですね。
まあ公式に書いてあったんですけどねw ⬇️
https://hub.docker.com/_/phpsedコマンドに関しては以下参照。
sedでこういう時はどう書く?また、なぜドキュメントルートが
/var/www/html/apache-laravel-app/public
なのかという話ですが、これはLaravelのプロジェクトを作るとプロジェクト内のpublic配下にindex.phpが作られるためです。また、Apacheの設定で一番最初に見に行くファイルがindex.phpになっています。
実際にindex.phpと書かれているファイルを検索して見てみます。root@be20b535a937:/var/www/html# grep index.php -rl /etc/apache2 /etc/apache2/mods-available/dir.conf # 利用可能なモジュールのディレクトリに関するファイル /etc/apache2/conf-available/docker-php.conf # phpが読み込まれた時の処理に関するファイルということでphpの処理がApacheでどう行われているかを以下のコマンドで見ます。
root@be20b535a937:/var/www/html# less /etc/apache2/conf-available/docker-php.conf <FilesMatch \.php$> SetHandler application/x-httpd-php </FilesMatch> DirectoryIndex disabled DirectoryIndex index.php index.html <Directory ${APACHE_DOCUMENT_ROOT}> Options -Indexes AllowOverride All </Directory>DirectryIndexは最初に見に行くファイルで、それにindex.phpが指定されていることがわかりました。Laravelはindex.phpから始まるので、このファイルが置いてある
/var/www/html/プロジェクト名/public
をドキュメントルートに指定する必要があります。③ モジュールのインストール
# ③モジュールのinstall RUN apt-get update \ && apt-get install -y zlib1g-dev libpq-dev unzip git vim curl\ && docker-php-ext-install pdo_mysql zip
RUN apt-get update
はインストール可能なパッケージの一覧の更新だけが行われます。upgradeにするとパッケージ自体の更新がされます。
apt-get install -y
も必要なもののインストールです。-y は何か請求があったとき全部YESと答えるという意味です。
docker-php-ext-install
の方はphp の拡張機能をインストールしているのですが、このイメージだとmysqli
とか入れようとするとエラーになってしまったんですよね、、ここの原因はまだ不明です、、、、④ Composer入れる
Composer入れます。
こちら参考。
商用環境でも使っている Laravel 用 php-fpm イメージの Dockerfile レシピ# ④Composer入れる # マルチステージビルドを採用 COPY --from=composer /usr/bin/composer /usr/bin/composerdocker-compose.yml
docker-compose.ymlversion: "3.7" services: app: build: . container_name: php-apache-app ports: - 80:80 volumes: - ./apache-laravel-app:/var/www/html db: image: mysql:5.7 container_name: php-apache-database ports: - 3306:3306 environment: - MYSQL_DATABASE=apache_laravel_db - MYSQL_ROOT_PASSWORD=rootコンテナ立ち上げる
$ docker-compose up -dDockerfileで指定すると拡張モジュールがうまく入らないものがあるので手動で入れる。
$ docker exec -it php-apache-app bash root@be20b535a937:/var/www/html# docker-php-ext-install mysqli ~ 処理が走る root@be20b535a937:/var/www/html# php -m // モジュール一覧表示 mysqliがあることを確認なんで手動だとできるのかもまだ謎、、
プロジェクトを作成します。
root@be20b535a937:/var/www/html# composer create-project laravel/laravel apache-laravel-app --prefer-dist "6.*Laravelの画面を開く
http://localhost
にアクセスしてlaravelのページが開ければオッケーです!
でも!以前nginx + Laravel でホーム以外のページの404エラー解決に投稿したみたいにApacheでも同じこと起こるんじゃないかと思って、Auth入れた後に /login にアクセスしてみた結果、、、ビンゴ!(泣)
ドキュメントルートの問題じゃないのになぜなの。。。ホーム以外が見れない原因
ApacheでLaravelを動かす時には
.htaccess
を読ませないといけないのですが、.htaccess<IfModule mod_rewrite.c> <IfModule mod_negotiation.c> Options -MultiViews -Indexes </IfModule> RewriteEngine On # Handle Authorization Header RewriteCond %{HTTP:Authorization} . RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] # Redirect Trailing Slashes If Not A Folder... RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_URI} (.+)/$ RewriteRule ^ %1 [L,R=301] # Handle Front Controller... RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [L] </IfModule>のようになっていて、
mod_rewrite
というモジュールが有効でないと動かないんです。そこで、コンテナ内でroot@be20b535a937:/etc# a2enmod rewriteを実行します。
もしこれでも動かない場合は.htaccess
自体が読み込まれない設定になっている可能性があるのでドキュメントルートでAllowOverride
がAll
になっているか確かめてください!最後に
本来であればDocker関連のファイルはプロジェクトと同じ階層にあると思うのでドキュメントルートはよしなに変えてください。
参考にさせていただいた記事など
ubuntuでapache2のDocumentRootを変更するまで
【find・grep】特定の文字列を含むファイルのリストを取得する方法。
https://gist.github.com/chronon/95911d21928cff786e306c23e7d1d3f3
- 投稿日:2020-05-23T12:04:26+09:00
Docker × Mysqlでユーザー作成出来ない
概要
Docker環境でMysqlを使用する際、
docker-compose.yml
に記載した環境変数が反映されず、作業ユーザーを作成出来ないエラーに遭遇しました。docker-compose.yml
version: "3.7" services: ~~~省略~~~ db: container_name: db build: context: ./db dockerfile: Dockerfile ports: - ${DB_PORT}:3306 environment: MYSQL_DATABASE: ${DB_NAME} # 作成するデータベース名 MYSQL_USER: ${DB_USER} # 作業ユーザー名→上記データベースの捜査権限をもつ MYSQL_PASSWORD: ${DB_PASS} # 作業ユーザーのパスワード MYSQL_ROOT_PASSWORD: ${ROOT_PASS} # rootユーザーのパスワード TZ: ${TZ} volumes: - db-data:/var/lib/mysql - ./db/mysql/my.cnf:/etc/mysql/conf.d/my.cnf - ./db/mysql/init:/docker-entrypoint-initdb.d - ./log:/var/log/mysql volumes: db-data: driver: localDockerfile
FROM mysql:8.0 EXPOSE 3306そもそもなぜ作業ユーザーを作成するか?
下記記事参照。
・【MySQL】ユーザーの作成方法について解説!原因
前作業でrootユーザーのみでdocker構築をしており、volume機能でデータが永続化していたため、後作業で記述した環境変数が反映されていないというものでした。
ビルドし直しても反映されない!そもそも消えることがないようにvolume機能を使用していたのに・・・
解決
MySQLのデータを初期化
$ docker-compose down --volumes $ docker-compose up -d
down
コマンド時に--volumes
オプションを追記すれば、volumeも一緒に削除されます。
次回up
コマンドで起動すれば、新しいvolumeが再構築されます。
- 投稿日:2020-05-23T08:32:18+09:00
docker imageの削除方法
はじめに
不要なimageはリソースのムダ使いに繋がる為、削除方法を忘備録として記載。
前提条件
削除対象のDocker image:
centos
Docker image削除手順
1.Docker image IDを確認する
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE centos latest 470671670cac 4 months ago 237MBIMAGE IDに表示されている
470671670cac
が今回の削除対象のDocker imageIMAGE ID 470671670cac //←このIMAGE IDを指定する2.Docker imageの削除
Docker imageの削除は下記コマンドで実行可能
//$ docker rmi {削除対象のIMAGE ID} $ docker rmi 470671670cac Untagged: centos:latest Untagged: centos@sha256:fe8d824220415eed5477b63addf40fb06c3b049404242b31982106ac204f6700 Deleted: sha256:470671670cac686c7cf0081e0b37da2e9f4f768ddc5f6a26102ccd1c6954c1ee Deleted: sha256:0683de2821778aa9546bf3d3e6944df779daba1582631b7ea3517bb36f9e4007Deletedと表示されていれば完了
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE※以下のようにエラーが表示される場合
停止中のコンテナが利用しているimageのためエラーとなる為、先に停止中のコンテナを削除する必要がある
$ docker rmi 470671670cac Error response from daemon: conflict: unable to delete 470671670cac (must be forced) - image is being used by stopped container 1ba05d50c062停止中のコンテナIDを確認する
$ docker ps
だと停止中のコンテナは表示されない$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
$ docker ps
に-a
をオプションをつけることで停止中のコンテナも表示することが可能$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 124b0dcc686a centos "/bin/bash" 32 minutes ago Exited (0) 32 minutes ago practical_ptolemy下記コンテナIDが削除対象のコンテナID
CONTAINER ID 124b0dcc686a //←このCONTANER IDを利用するコンテナの削除
以下の方法で停止中のコンテナを削除することが可能
//$ docker rm {{CONTAINER ID}} $ docker rm 124b0dcc686a 124b0dcc686a //←上手くいくとCONTANER IDが表示される改めてDocker imageの削除再実行
$ docker rmi 470671670cac Untagged: centos:latest Untagged: centos@sha256:fe8d824220415eed5477b63addf40fb06c3b049404242b31982106ac204f6700 Deleted: sha256:470671670cac686c7cf0081e0b37da2e9f4f768ddc5f6a26102ccd1c6954c1ee Deleted: sha256:0683de2821778aa9546bf3d3e6944df779daba1582631b7ea3517bb36f9e4007Deletedと表示されていれば完了
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE
- 投稿日:2020-05-23T01:53:32+09:00
Docker Compose で作った Redmine を Ubuntu 上でいい感じに動かす手抜きレシピ
はじめに
Docker Compose を使った自分好みの Redmine 実行環境 というものを作っています。構築やバージョンアップが何かと大変な Redmine を、プラグインやテーマがてんこもりの欲張りセットにしつつも、なるべく手をかけずにメンテナスできるようにするにはどうすればいいのかを悩みながら、ようやくそれなりの形になってきたかなといったところです。
以下は、試行錯誤の結果をまとめた記事リンクです。
- Docker Compose を使って自分好みの Redmine 実行環境を作ってみた
- Redmine draw.io plugin がすごい便利そうなので紹介したい
- Docker Compose で作った Redmine だって Full Text Search plugin と ChupaText サーバーを連携させたい
- (番外編) 今度は Redmine を Docker Compose で Raspberry に入れてみた
この状態でも動かせるのですが、せっかくですのでもう少しサーバーっぽく仕立てていきたいと思います。相変わらずあまり手をかけたくないので、ちょっと手抜きなレシピですがお付き合いください。(お気に召さないところがあればアレンジいただいても問題ありません)
おことわり
本記事の内容で実際に運用した際、何らかの問題やトラブルが起きても自己責任でお願いします。安全に運用できるようにするために可能な限り注意を払って執筆しておりますが、至らぬところもあるかと思います。
内容の不備や、よりよい実現方法があればコメントいただけると幸いです。
今回のゴール
Ubuntu 18.04 LTS を使って、複数の Redmine を同時に稼働させる環境を作ります。(Ubuntu 20.04 LTS の方が良いかもしれませんが、リリースされてからまだ日が浅いので今回は見送ります)
詳細は以下のとおりです。
- それぞれの Redmine には HTTPS でアクセスできるようにします。HTTPS 化には Let's Encrypt を使います。
- どの Redmine にアクセスするかはサブドメインで振り分けるようにします。ついでに、ドメイン名ではなく IP アドレスでアクセスしてきた場合は弾くようにします。
- 僕は ConoHa の VPS と ムームードメイン を組み合わせて使っています。自分でサブドメインを自由に割り当てられる環境があれば何でもよいです。
- リバースプロキシサーバーに Nginx を使います。Docker Compose で作った Redmine はバックエンドで動作させます。
- 手抜きして、Nginx はホスト側にインストールします。
- Redmine Security Scanner の検査結果が「A+」になるようにします。
- ログローテーション、Redmine のリマインダー通知、データベースや添付ファイルのバックアップを定期的に行うようにします。
Nginx をホスト側にインストールする理由
ここまで Docker Compose を使ってきたんだから、Nginx も Docker Compose を使えばいいのにと思われる方もいらっしゃると思いますが、これには理由があります。
それは、今回の構成だと、Nginx に限っては Docker を使うよりもホストに直接インストールする方が手っ取り早いと感じたからです。
Certbot で証明書を発行したり定期更新のタスクを仕掛けるのはホスト側のコマンドラインで操作した方が楽だと思ってますし、サブドメイン 1 つに対して 1 回設定するだけでよいので、そこまで手間ではないと思っています。あとは Nginx の conf ファイルをちょこっと触るくらしかないので、Docker を使おうが使わまいがさほど変わらない気がしています。
また、Nginx と Redmine を同じ Docker 内部ネットワークで接続した方が構成としては綺麗なのかもしれませんが、この程度の規模なら Redmine のポートをホスト側にバインドしてもさほど影響ないのではと思っています。
もちろん、Docker 化した方が望ましいと思われる要件があれば検討したいとは思いますが、今回はどうにもそうは思えなかったです。たとえるなら「100 点満点のテストで 100 点を取ろうとして時間切れで結局 20~30 点しか取れないくらいなら、最初から 60~70 点くらいを狙おう」という考えだということです。
と、長々と言い訳がましく書きましたが、結局のところ以下のような構成となります。
ホスト側の Nginx と、Docker Compose の Redmine がちょうどいい感じにソーシャルディスタンスを保っているんじゃないかなと思います。このくらいの方が、後になって方向転換したくなったときに融通が利きやすいだろうという見立てです。
レシピ
前置きが長くなりましたが、ここからが詳細なレシピとなります。
まず、Redmine を 1 つ稼働させるところまでを説明します。その後、2 つ目以降の Redmine を追加していきます。
下準備
ホストマシンの準備
Ubuntu 18.04 LTS のマシンを用意します。VPS でも何でもよいです。設定手順は割愛しますが、以下のリンク先などが参考になります。
以降、下記の条件が満たされている前提で進めます。
- sudo 権限をもつユーザーが作られていること
- 以降、ユーザー名は
[user]
で説明しますので、適宜読み替えてください- インターネットから IP アドレス指定でアクセスできること
あとは、SSH 公開鍵認証で接続できるようにしておいた方が便利です。SSH は
root
で接続できなくしておいたり、パスワードでの認証を無効にしたりするなど、セキュリティリスクを抑えるように設定した方が望ましいです。ドメインの準備
自分でサブドメインを自由に割り当てられる DNS サービスを用意します。以降、ドメイン名は
[your-domain]
で説明しますので、適宜読み替えてください。サブドメイン名は何でもよいです。今回は説明の便宜上、名前を決め打ちします。以降の説明では、その名前にあわせてファイル名やディレクトリ名などを決めていきます。みなさんがお手元で試される際は、ご自身の気に入った名前をつけるようにしてください。
今回、1 つ目の Redmine には
yuni
と名づけます。yuni.[your-domain]
で前述の Ubuntu マシンの IP アドレスが引けるように DNS のレコードを追加してください。ホストマシンの設定
ファイアウォールの設定
ファイアウォールは
ufw
を使います。今回は以下のポートを開けて、他は塞ぎます。
- HTTP(80 番ポート)
- HTTPS(443 番ポート)
- SSH(22 番ポート) ※ リッスンするポート番号を変更している場合は読み替えてください。
コマンドは以下のとおりです。
$ sudo ufw enable $ sudo ufw allow 22 $ sudo ufw allow 80 $ sudo ufw allow 443SSH 接続でファイアウォールの設定をする場合、誤って SSH 接続用ポートを塞いだまま切断してしまうと再接続できなくなってしまうため注意してください。別のセッションで接続確認しながら設定するのがよいです。
設定した内容は
ufw status
で確認できます。$ sudo ufw status 状態: アクティブ To Action From -- ------ ---- 22 ALLOW Anywhere 80 ALLOW Anywhere 443 ALLOW Anywhere 22 (v6) ALLOW Anywhere (v6) 80 (v6) ALLOW Anywhere (v6) 443 (v6) ALLOW Anywhere (v6)Docker と Docker Compose のインストール
それぞれ、以下のリンク先を参照してください。
- Install Docker Engine on Ubuntu | Docker Documentation
- docker グループに
[user]
を追加するところまで進めてください。
- Install Docker Compose | Docker Documentation
※ Ubuntu 20.04 LTS だと
apt install
だけでインストールできるらしいので簡単そうです。なお、動作確認時のバージョンは以下のとおりです。
$ docker -v Docker version 19.03.8, build afacb8b7f0 $ docker-compose -v docker-compose version 1.25.5, build 8a1c60f6インストール先のパスは以下のとおりです。
$ which docker /usr/bin/docker $ which docker-compose /usr/local/bin/docker-composeWeb サービスの設定
Nginx のインストール
以下のリンク先を参照してください。
インストールが完了したら Nginx を起動します。
$ sudo systemctl start nginx.service起動が完了したら、ブラウザから
http://[ホストの IP アドレス]
とhttp://yuni.[your-domain]
のそれぞれにアクセスして、どちらも Nginx のトップページが表示されることを確認してください。Redmine の構築
Redmine を構築するため、 Docker Compose を使った自分好みの Redmine 実行環境 からファイル一式を取得します。
ディレクトリ構成
今回は以下のディレクトリ構成とします。(細部は割愛)
あくまで一例ですので、好みに合わせて変更いただいても問題ありません。/ +-- opt/ +-- redmine/ +-- yuni/ +-- docker-compose.yml +-- db/ +-- redmine/ +-- srv/ +-- redmine/ +-- yuni/ +-- files/ +-- var/ +-- log/ +-- redmine/ +-- yuni/ +-- redmine-chupa-text/ +-- yuni/ファイル一式の配置
/opt
配下にディレクトリを作って、取得したファイル一式をコピーします。ディレクトリ名をyuni
にリネームしておきます。$ cd /opt $ sudo mkdir redmine $ sudo chown [user]:[user] redmine $ cd /opt/redmine $ cp /path/to/myfav-redmine . $ mv myfav-redmine yuni※
/path/to
はファイル一式を格納した任意のディレクトリを表しますので、適宜読み替えてください。docker-compose.yml の編集
ディレクトリ構成にあわせてパスを編集します。コンテナ名もあわせて変更しておきます。
以下、元のファイルからの変更箇所がわかるように差分形式で表しています。
/opt/redmine/yuni/docker-compose.ymlversion: "3.7" services: redmine: build: ./redmine - container_name: myfav-redmine + container_name: yuni-redmine restart: always depends_on: - db - chupa-text ports: - "3000:3000" environment: TZ: Asia/Tokyo REDMINE_DB_POSTGRES: db REDMINE_DB_DATABASE: redminedb REDMINE_DB_USERNAME: redmineuser REDMINE_DB_PASSWORD: redminepassword REDMINE_PLUGINS_MIGRATE: "true" volumes: - - "/srv/redmine/files:/usr/src/redmine/files:z" + - "/srv/redmine/yuni/files:/usr/src/redmine/files:z" - - "/var/log/redmine:/usr/src/redmine/log:z" + - "/var/log/redmine/yuni:/usr/src/redmine/log:z" db: build: ./db - container_name: myfav-redmine-db + container_name: yuni-redmine-db restart: always environment: TZ: Asia/Tokyo POSTGRES_DB: redminedb POSTGRES_USER: redmineuser POSTGRES_PASSWORD: redminepassword volumes: - "dbdata:/var/lib/postgresql/data" chupa-text-proxy: image: groonga/chupa-text:proxy - container_name: myfav-redmine-chupa-text-proxy + container_name: yuni-redmine-chupa-text-proxy restart: always environment: TZ: Asia/Tokyo volumes: - - "/var/log/redmine-chupa-text/proxy:/var/log/squid:z" + - "/var/log/redmine-chupa-text/yuni/proxy:/var/log/squid:z" chupa-text: image: groonga/chupa-text:ubuntu-latest - container_name: myfav-redmine-chupa-text + container_name: yuni-redmine-chupa-text restart: always depends_on: - chupa-text-proxy environment: TZ: Asia/Tokyo http_proxy: http://chupa-text-proxy:3128/ https_proxy: http://chupa-text-proxy:3128/ RAILS_SERVE_STATIC_FILES: "true" volumes: - - "/var/log/redmine-chupa-text/rails:/home/chupa-text/chupa-text-http-server/log:z" + - "/var/log/redmine-chupa-text/yuni/rails:/home/chupa-text/chupa-text-http-server/log:z" volumes: dbdata:必要に応じて、データベース名/ユーザー名/パスワードも変更してください。
Redmine の起動
必要に応じて、プラグインやテーマの追加、メールの設定を行います。それぞれ下記ファイルを編集します。
- プラグインやテーマの追加:
/opt/redmine/yuni/redmine/Docerfile
- メールの設定:
/opt/redmine/yuni/redmine/config/configuration.yml
準備ができたら Redmine を起動します。
$ cd /opt/redmine/yuni $ docker-compose up -d --build
docker-compose ps
を実行すると、起動できているか確認できます。
以下は、正常に起動できているときの結果です。$ docker-compose ps Name Command State Ports ----------------------------------------------------------------------------------------------- yuni-redmine /docker-entrypoint.sh pass ... Up 0.0.0.0:3000->3000/tcp yuni-redmine-chupa-text /bin/sh -c ./start.sh Up yuni-redmine-chupa-text-proxy /bin/sh -c ./start.sh Up yuni-redmine-db docker-entrypoint.sh postgres Up 5432/tcpこの時点では、ブラウザから Redmine の表示は確認できません。
curl -v http://localhost:3000
を実行して Redmine のトップページの HTML ソースが取得できれば OK です。Nginx の設定
Nginx を通じてバックエンドの Redmine にアクセスするための設定を追加します。
/etc/nginx/conf.d
配下にyuni-redmine.conf
というファイルを作って、以下のように設定します。/etc/nginx/conf.d/yuni-redmine.confserver { listen 80; server_name yuni.[your-domain]; client_max_body_size 32m; proxy_buffer_size 32k; proxy_buffers 100 32k; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host $host:$server_port; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; location / { proxy_pass http://localhost:3000; } }Nginx はデフォルトで 1MB までしかリクエストを受け付けません。それ以上のサイズのファイルをアップロードできるようにするため
client_max_body_size
の設定が必要です。
proxy_buffer_size
とproxy_buffers
はバックエンドからのレスポンスデータをバッファリングするための設定です。パフォーマンスに問題があれば調整してください。設定が完了すれば、 Nginx をリロードしてください。設定が反映されます。
$ sudo systemctl reload nginx.service設定が反映されないときは、Nginx を再起動してください。
$ sudo systemctl restart nginx.serviceブラウザからの動作確認
ここまでの設定が正しくできていれば、ブラウザから
http://yuni.[your-domain]
にアクセスすると Redmine のトップページが表示されます。なお、
http://[ホストの IP アドレス]
にアクセスすると、Nginx のトップページが表示されます。Let's Encrypt で HTTPS 化
まずは現時点のセキュリティレベルを検査
HTTPS 化する前後でセキュリティレベルの違いを確認できるように、今の時点で先ほどアクセスできるようになった Redmine を Redmine Security Scanner で検査してみます。
結果は「C」です。「暗号化されていない通信で Redmine にアクセスできてしまうよ」と教えてくれています。これを改善します。
Certbot のインストールと設定
Let's Encrypt の SSL 証明書を取得するために Certbot を使います。
Certbot のインストールと設定は、以下のリンク先を参照してください。コマンドラインで対話形式にいくつかの質問に答えるだけで簡単に設定できます。途中、「HTTP からのアクセスを HTTPS にリダイレクトするか」と問われたら「リダイレクトする」を選んでください。
設定の完了後に Nginx の設定ファイルを開くと、以下のように変更されていることがわかります。
/etc/nginx/conf.d/yuni-redmine.confserver { - listen 80; server_name yuni.[your-domain]; client_max_body_size 32m; proxy_buffer_size 32k; proxy_buffers 100 32k; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host $host:$server_port; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; location / { proxy_pass http://localhost:3000; } + + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/yuni.[your-domain]/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/yuni.[your-domain]/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +} + + +server { + if ($host = yuni.[your-domain]) { + return 301 https://$host$request_uri; + } # managed by Certbot + + + listen 80; + server_name yuni.[your-domain]; + return 404; # managed by Certbot + }これで
https://yuni.[your-domain]
にアクセスできるようになりました。
http://yuni.[your-domain]
にアクセスしても HTTPS にリダイレクトされるようになります。もう一度セキュリティレベルを検査(と微調整)
HTTPS 化ができたら、もう一度 Redmine Security Scanner で検査します。
結果は「A」です。うーん惜しい、あと一歩。どうやらセキュリティ関連のヘッダが足りていないようです。
検査結果を追っていくと「HTTP Strict Transport Security ヘッダがセットされていないよ」と教えてくれています。
どうやら Certbot は、設定時に Strict Transport Security ヘッダをセットしてくれないようですので、手動で Nginx の設定に追記します。
せっかくなので、ついでに HTTP/2 の設定も追記しておきます。以下は変更箇所を抜粋したものです。
/etc/nginx/conf.d/yuni-redmine.conf- listen 443 ssl; # managed by Certbot + listen 443 ssl http2; # managed by Certbot ssl_certificate /etc/letsencrypt/live/yuni.[your-domain]/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/yuni.[your-domain]/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + + add_header Strict-Transport-Security "max-age=31536000;"; }Nginx をリロードまたは再起動させて設定を反映した後に、もう一度検査してみると、結果が「A+」になります。やったぜ。
IP アドレスでのアクセスを弾く
Let's Encrypt が発行する SSL 証明書は、Certbot で設定した時に選択したドメインに対してのみ有効です。IP アドレスでアクセスすると不正な SSL 証明書とみなされてしまいます。そもそも IP アドレスだと Redmine にアクセスできないので弾いちゃいましょう。
Nginx のデフォルト設定(
/etc/nginx/conf.d/default.conf
)を以下の内容に差し替えます。/etc/nginx/conf.d/default.confserver { listen 80 default_server; listen 443 ssl default_server; server_name _; ssl_certificate /etc/letsencrypt/live/yuni.[your-domain]/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yuni.[your-domain]/privkey.pem; return 444; }Nginx は
return 444;
とすると HTTP レスポンスを終了します。これを、他のどの設定のserver_name
にも一致しなかったときにデフォルトで適用されるようにすることで、IP アドレスでのアクセスを弾けるようになります。ただし、HTTPS でのアクセスも弾こうとして
listen 443 ssl
を追加すると、ssl_certificate
とssl_certificate_key
も追加しないと怒られてしまいます。ここでは苦し紛れで、先ほど作った Let's Encrypt の設定を流用しています。厳密に言えば正しくないのですが、アクセスできないページに労力をかけるのもどうかと思うので、今回はこれでヨシ!とします。Redmine の設定
ようやく Redmine の設定に触れていきます。といいつつ、基本的には Redmineガイド を参照してください。
ここでは、本環境で固有の設定だけピックアップして説明します。
Full text search plugin「ChupaTextサーバーのURL」
管理 > プラグイン から Full text search plugin の設定画面を開き、「ChupaTextサーバーのURL」に
http://chupa-text:3000/extraction.json
と設定します。ChupaText の README に書いている説明と、ホスト名:ポート番号が違っているので注意してください。データベースのダンプ
管理画面から一通り設定が完了したら、データベースのダンプを取っておきましょう。何か問題が起きたとき、データのリストアに使えます。
今回はデータベースに PostgreSQL を使っているので
pg_dump
を使います。$ cd /opt/redmine/yuni/ $ docker-compose exec -T db pg_dump -U redmineuser redminedb > yuni-redmine.dump今回はデフォルトのフォーマットでダンプしているので平文で出力されます。他のフォーマットも選択可能です。詳しくは PostgreSQL 12.0 文書 PostgreSQLクライアントアプリケーション - pg_dump を参照してください。
本記事では、この後 2 つ目の Redmine を作るときにリストアの手順を説明します。
ログローテーションの設定
ログローテーションはホスト側の
logrotate.d
で行います。
/etc/logrotate.d
に格納されている他のログローテーション設定に倣って、以下のように設定します。Redmine のログローテーション
/etc/logrotate.d/yuni-redmine/var/log/redmine/yuni/*.log { rotate 4 weekly missingok notifempty compress delaycompress sharedscripts postrotate /usr/local/bin/docker-compose -f /opt/redmine/yuni/docker-compose.yml restart redmine endscript }ChupaText のログローテーション
/etc/logrotate.d/yuni-redmine-chupa-text/var/log/redmine-chupa-text/yuni/*/*.log { rotate 4 weekly missingok notifempty compress delaycompress sharedscripts postrotate /usr/local/bin/docker-compose -f /opt/redmine/yuni/docker-compose.yml restart chupa-text chupa-text-proxy endscript }動作確認
動作確認は
logrotate
コマンドで行います。
-dv
オプションで dry-run 、-f
オプションで強制手動実行できます。以下は dry-run のコマンド例です。
$ sudo logrotate -dv /etc/logrotate.d/yuni-redmine $ sudo logrotate -dv /etc/logrotate.d/yuni-redmine-chupa-textリマインダー通知の設定
rake タスクの実行方法
リマインダー通知を含む
rake
タスクの実行方法は以下のとおりです。$ cd /opt/redmine/yuni/ $ docker-compose exec -T redmine rake redmine:send_reminders任意のディレクトリから
docker-compose
コマンドを実行するときは、-f
オプションでdocker-compose.yml
のパスを指定します。$ docker-compose -f /opt/redmine/yuni/docker-compose.yml exec -T redmine rake redmine:send_remindersリマインダー通知のシェルスクリプト
リマインダー通知の設定をメンテナンスしやすくするために
/opt/redmine/scripts/yuni
配下にシェルスクリプトを作っておきます。以下は、期日が 3 日前までに迫ったチケットのリマインダーを通知する例です。
/opt/redmine/scripts/yuni/send_reminders.sh#!/bin/bash /usr/local/bin/docker-compose -f /opt/redmine/yuni/docker-compose.yml \ exec -T redmine rake redmine:send_reminders days=3シェルスクリプトのファイルには実行権限を付与してください。(
chmod +x
)リマインダーの定期実行
リマインダーの定期実行は、ホスト側の
cron
を使います。以下は、毎平日の朝 6 時にリマインダー通知を実行する例です。ついでにリマインダーの実行結果をログに残すようにしています。
0 6 * * 1-5 /opt/redmine/scripts/yuni/send_reminders.sh >> /var/log/redmine/yuni/send_reminders.log 2>&1バックアップ
バックアップの取得先
バックアップの取得先は
/var/opt/redmine/yuni/backup
にします。念のためroot
ユーザーしかアクセスできないようにしておきます。$ sudo mkdir -p /var/opt/redmine/yuni/backup/{db,files} $ sudo chmod 700 /var/opt/redmine/yuni/backupバックアップスクリプト
添付ファイルのバックアップには
rsync
、データベースのバックアップには前述のpg_dump
を使います。こちらもシェルスクリプトにしておきます。
/opt/redmine/scripts/yuni/backup.sh#!/bin/bash # 添付ファイルのバックアップ rsync -avh /srv/redmine/yuni/files/ /var/opt/redmine/yuni/backup/files # データベースのバックアップ /usr/local/bin/docker-compose -f /opt/redmine/yuni/docker-compose.yml \ exec -T db pg_dump -U redmineuser redminedb \ > /var/opt/redmine/yuni/backup/db/yuni-redmine.dump今回は自サーバー内でバックアップを取得しているため、誤操作などで誤ってデータを紛失したときにリストアが可能ですが、サーバー自身がダメになったときはリストアできません。
取得したバックアップを別のサーバーなどに転送しておけば、サーバー自身がダメになったときもリストアできます。(転送処理は割愛します)
バックアップの定期実行
バックアップの定期実行も、ホスト側の
cron
を使います。以下は、毎日朝 4 時にバックアップを実行する例です。ついでにログも残すようにしています。
0 4 * * * /opt/redmine/scripts/yuni/backup.sh >> /var/log/redmine/yuni/backup.log 2>&1ここまでのまとめ
これで無事に 1 つ目の Redmine の設定が終わりました。Redmine 本体の設定にはあまり触れず、足回りのことばかりダラダラと書いてきましたが…
さて、どうだろうか。
端的に換言すれば、いい感じ。
ディレクトリ構成
追加したディレクトリ、ファイルを含めてディレクトリ構成を再度まとめておきます。
/ +-- etc/ +-- logrotate.d/ +-- yuni-redmine +-- yuni-redmine-chupa-text +-- nginx/ +-- conf.d/ +-- default.conf +-- yuni-redmine.conf +-- opt/ +-- redmine/ +-- scripts/ +-- yuni/ +-- backup.sh +-- send_reminders.sh +-- yuni/ +-- docker-compose.yml +-- db/ +-- redmine/ +-- srv/ +-- redmine/ +-- yuni/ +-- files/ +-- var/ +-- log/ +-- redmine/ +-- yuni/ +-- redmine-chupa-text/ +-- yuni/ +-- opt/ +-- redmine/ +-- yuni/ +-- backup/ +-- db/ +-- yuni-redmine.dump +-- files/2 つ目の Redmine を作ってみる
さて、次は 2 つ目の Redmine を作ってみましょう。やはり名前は何でもいいです。気に入った名前をつけましょう。
本記事では
chloe
と名づけます。chloe.[your-domain]
で DNS のレコードを追加してください。ディレクトリ構成
2 つ目の Redmine ができあがると、以下のようなディレクトリ構成になります。
追加するファイルおよびディレクトリを差分で示します。/ +-- etc/ +-- logrotate.d/ + +-- chloe-redmine + +-- chloe-redmine-chupa-text +-- yuni-redmine +-- yuni-redmine-chupa-text +-- nginx/ +-- conf.d/ +-- default.conf + +-- chloe-redmine.conf +-- yuni-redmine.conf +-- opt/ +-- redmine/ +-- scripts/ + +-- chloe/ + +-- backup.sh + +-- send_reminders.sh +-- yuni/ +-- backup.sh +-- send_reminders.sh + +-- chloe/ + +-- docker-compose.yml + +-- db/ + +-- redmine/ +-- yuni/ +-- docker-compose.yml +-- db/ +-- redmine/ +-- srv/ +-- redmine/ + +-- chloe/ + +-- files/ +-- yuni/ +-- files/ +-- var/ +-- log/ +-- redmine/ + +-- chloe/ +-- yuni/ +-- redmine-chupa-text/ + +-- chloe/ +-- yuni/ +-- opt/ +-- redmine/ + +-- chloe/ + +-- backup/ + +-- db/ + +-- chloe-redmine.dump + +-- files/ +-- yuni/ +-- backup/ +-- db/ +-- yuni-redmine.dump +-- files/Redmine の構築
ファイル一式のコピーと docker-compose.yml の編集
今回は
/opt/redmine/yuni
をまるっとコピーして/opt/redmine/chloe
を作ります。$ cd /opt/redmine $ cp -a yuni chloeコピーができたら次は
docker-compose.yml
を編集します。
今回は Redmine をホスト側の 3001 番ポートにバインドします。/opt/redmine/chloe/docker-compose.ymlversion: "3.7" services: redmine: build: ./redmine - container_name: yuni-redmine + container_name: chloe-redmine restart: always depends_on: - db - chupa-text ports: - - "3000:3000" + - "3001:3000" environment: TZ: Asia/Tokyo REDMINE_DB_POSTGRES: db REDMINE_DB_DATABASE: redminedb REDMINE_DB_USERNAME: redmineuser REDMINE_DB_PASSWORD: redminepassword REDMINE_PLUGINS_MIGRATE: "true" volumes: - - "/srv/redmine/yuni/files:/usr/src/redmine/files:z" + - "/srv/redmine/chloe/files:/usr/src/redmine/files:z" - - "/var/log/redmine/yuni:/usr/src/redmine/log:z" + - "/var/log/redmine/chloe:/usr/src/redmine/log:z" db: build: ./db - container_name: yuni-redmine-db + container_name: chloe-redmine-db restart: always environment: TZ: Asia/Tokyo POSTGRES_DB: redminedb POSTGRES_USER: redmineuser POSTGRES_PASSWORD: redminepassword volumes: - "dbdata:/var/lib/postgresql/data" chupa-text-proxy: image: groonga/chupa-text:proxy - container_name: yuni-redmine-chupa-text-proxy + container_name: chloe-redmine-chupa-text-proxy restart: always environment: TZ: Asia/Tokyo volumes: - - "/var/log/redmine-chupa-text/yuni/proxy:/var/log/squid:z" + - "/var/log/redmine-chupa-text/chloe/proxy:/var/log/squid:z" chupa-text: image: groonga/chupa-text:ubuntu-latest - container_name: yuni-redmine-chupa-text + container_name: chloe-redmine-chupa-text restart: always depends_on: - chupa-text-proxy environment: TZ: Asia/Tokyo http_proxy: http://chupa-text-proxy:3128/ https_proxy: http://chupa-text-proxy:3128/ RAILS_SERVE_STATIC_FILES: "true" volumes: - - "/var/log/redmine-chupa-text/yuni/rails:/home/chupa-text/chupa-text-http-server/log:z" + - "/var/log/redmine-chupa-text/chloe/rails:/home/chupa-text/chupa-text-http-server/log:z" volumes: dbdata:この時、変更箇所に漏れがないように気を付けてください。もし変更漏れがあると 1 つ目に作った Redmine と競合してエラいことになります。
競合を避けるために最初から container_name をつけないという流儀もあるようです。
Redmine の起動
準備ができたら 1 つ目と同じように Redmine を起動します。
$ cd /opt/redmine/chloe $ docker-compose up -d --build
docker-compose ps
で起動確認しましょう。$ docker-compose ps Name Command State Ports ------------------------------------------------------------------------------------------------ chloe-redmine /docker-entrypoint.sh pass ... Up 0.0.0.0:3001->3000/tcp chloe-redmine-chupa-text /bin/sh -c ./start.sh Up chloe-redmine-chupa-text-proxy /bin/sh -c ./start.sh Up chloe-redmine-db docker-entrypoint.sh postgres Up 5432/tcp
curl -v http://localhost:3001
を実行して Redmine のトップページの HTML ソースが取得できれば OK です。Nginx の設定
/etc/nginx/conf.d
配下にchloe-redmine.conf
というファイルを作って、以下のように設定します。/etc/nginx/conf.d/chloe-redmine.confserver { listen 80; server_name chloe.[your-domain]; client_max_body_size 32m; proxy_buffer_size 32k; proxy_buffers 100 32k; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host $host:$server_port; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; location / { proxy_pass http://localhost:3001; } }
server_name
とproxy_pass
のポート番号が違うだけで、残りの箇所は 1 つ目と同じです。設定が完了すれば、Nginx をリロードまたは再起動して反映してください。
ここまでの設定が正しくできていれば、ブラウザからhttp://chloe.[your-domain]
にアクセスすると Redmine のトップページが表示されます。Let's Encrypt で HTTPS 化
certbot instructions - Nginx on Ubuntu 18.04 LTS (bionic) の Certbot インストール後の手順から進めます。
対話形式の質問には「
chloe.[your-domain]
を HTTPS 化する 」⇒「HTTP からのアクセスを HTTPS にリダイレクトする」を選んでください。HTTPS 化が終われば Strict Transport Security ヘッダと HTTP/2 の設定を追加します。
/etc/nginx/conf.d/chloe-redmine.conf- listen 443 ssl; # managed by Certbot + listen 443 ssl http2; # managed by Certbot ssl_certificate /etc/letsencrypt/live/chloe.[your-domain]/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/chloe.[your-domain]/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + + add_header Strict-Transport-Security "max-age=31536000;"; }Nginx をリロードまたは再起動させて設定を反映した後に、 Redmine Security Scanner で
https://chloe.[your-domain]
を検査して、結果が「A+」になることを確認しておきましょう。Redmine の設定
Redmine の設定ですが、今回は 1 つ目の Redmine から取得したデータベースのダンプをリストアしてみましょう。
Redmine を停止してデータベースをいったん空にして、ダンプをリストアして Redmine を起動するという流れになります。
Redmine の停止は以下のコマンドです。
$ cd /opt/redmine/chloe $ docker-compose stop redmine次にデータベースを空にする手順ですが、PostgreSQL の場合は「public スキーマを削除してから再作成する」のが手っ取り早いようです。
$ cd /opt/redmine/chloe $ docker-compose exec -T db psql -U redmineuser redminedb << EOF DROP SCHEMA public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO redmineuser; GRANT ALL ON SCHEMA public TO public; COMMENT ON SCHEMA public IS 'standard public schema'; CREATE EXTENSION IF NOT EXISTS pgroonga; EOF後はリストアしたい時点のダンプファイルを
psql
で流し込んで、 Redmine を再起動するだけです。$ cd /opt/redmine/chloe $ docker-compose exec -T db psql -U redmineuser redminedb < /path/to/yuni-redmine.dump $ docker-compose start redmine※
/path/to
はダンプを格納した任意のディレクトリを表しますので、適宜読み替えてください。後は、ログインして設定を編集しましょう。
少なくとも 設定 > 全般 の「ホスト名とパス」は変更が必要です。他の箇所は必要に応じて自由に変えましょう。ぷー…まあ、いいけど…てゆか、いいじゃん。
ログローテーションの設定、リマインダー通知の設定、バックアップ
ログローテーションの設定、リマインダー通知の設定、バックアップも同じようにします。
スクリプトやcron
ジョブは 1 つ目の Redmine との違いを差分で示します。Redmine のログローテーション
/etc/logrotate.d/chloe-redmine-/var/log/redmine/yuni/*.log { +/var/log/redmine/chloe/*.log { rotate 4 weekly missingok notifempty compress delaycompress sharedscripts postrotate - /usr/local/bin/docker-compose -f /opt/redmine/yuni/docker-compose.yml restart redmine + /usr/local/bin/docker-compose -f /opt/redmine/chloe/docker-compose.yml restart redmine endscript }ChupaText のログローテーション
/etc/logrotate.d/chloe-redmine-chupa-text-/var/log/redmine-chupa-text/yuni/*/*.log { +/var/log/redmine-chupa-text/chloe/*/*.log { rotate 4 weekly missingok notifempty compress delaycompress sharedscripts postrotate - /usr/local/bin/docker-compose -f /opt/redmine/yuni/docker-compose.yml restart chupa-text chupa-text-proxy + /usr/local/bin/docker-compose -f /opt/redmine/chloe/docker-compose.yml restart chupa-text chupa-text-proxy endscript }リマインダー通知のシェルスクリプト
/opt/redmine/scripts/chloe/send_reminders.sh#!/bin/bash -/usr/local/bin/docker-compose -f /opt/redmine/yuni/docker-compose.yml \ +/usr/local/bin/docker-compose -f /opt/redmine/chloe/docker-compose.yml \ exec -T redmine rake redmine:send_reminders days=3リマインダーの定期実行
-0 6 * * 1-5 /opt/redmine/scripts/yuni/send_reminders.sh >> /var/log/redmine/yuni/send_reminders.log 2>&1 +0 6 * * 1-5 /opt/redmine/scripts/chloe/send_reminders.sh >> /var/log/redmine/chloe/send_reminders.log 2>&1バックアップの取得先
$ sudo mkdir -p /var/opt/redmine/chloe/backup/{db,files} $ sudo chmod 700 /var/opt/redmine/chloe/backupバックアップスクリプト
/opt/redmine/scripts/chloe/backup.sh#!/bin/bash # 添付ファイルのバックアップ -rsync -avh /srv/redmine/yuni/files/ /var/opt/redmine/yuni/backup/files +rsync -avh /srv/redmine/chloe/files/ /var/opt/redmine/chloe/backup/files # データベースのバックアップ -/usr/local/bin/docker-compose -f /opt/redmine/yuni/docker-compose.yml \ +/usr/local/bin/docker-compose -f /opt/redmine/chloe/docker-compose.yml \ exec -T db pg_dump -U redmineuser redminedb \ - > /var/opt/redmine/yuni/backup/db/yuni-redmine.dump + > /var/opt/redmine/chroe/backup/db/chroe-redmine.dump3 つ目以降の Redmine も同様に
3 つ目以降の Redmine を作りたいときも、名前とバインドするポートを変えて、あとは 2 つ目の Redmine と同じく以下の手順に沿って進めれば簡単に作れます。
- Redmine の構築
- Nginx の設定
- Let's Encrypt で HTTPS 化
- Redmine の設定
- ログローテーションの設定、リマインダー通知、バックアップ
たとえば、3 つ目の Redmine の名前を
chieru
と名づけて、バインドするポートを 3002 番にして作ることができます。ここまでお読みいただいた皆さんでしたら、詳しく説明しなくてもちぇるっと作れますよね。
以下、
docker-compose.yml
と HTTPS 化する前の Nginx 設定ファイルだけ示しておきます。/opt/redmine/chieru/docker-compose.ymlversion: "3.7" services: redmine: build: ./redmine - container_name: chloe-redmine + container_name: chieru-redmine restart: always depends_on: - db - chupa-text ports: - - "3001:3000" + - "3002:3000" environment: TZ: Asia/Tokyo REDMINE_DB_POSTGRES: db REDMINE_DB_DATABASE: redminedb REDMINE_DB_USERNAME: redmineuser REDMINE_DB_PASSWORD: redminepassword REDMINE_PLUGINS_MIGRATE: "true" volumes: - - "/srv/redmine/chloe/files:/usr/src/redmine/files:z" + - "/srv/redmine/chieru/files:/usr/src/redmine/files:z" - - "/var/log/redmine/chloe:/usr/src/redmine/log:z" + - "/var/log/redmine/chieru:/usr/src/redmine/log:z" db: build: ./db - container_name: chloe-redmine-db + container_name: chieru-redmine-db restart: always environment: TZ: Asia/Tokyo POSTGRES_DB: redminedb POSTGRES_USER: redmineuser POSTGRES_PASSWORD: redminepassword volumes: - "dbdata:/var/lib/postgresql/data" chupa-text-proxy: image: groonga/chupa-text:proxy - container_name: chloe-redmine-chupa-text-proxy + container_name: chieru-redmine-chupa-text-proxy restart: always environment: TZ: Asia/Tokyo volumes: - - "/var/log/redmine-chupa-text/chloe/proxy:/var/log/squid:z" + - "/var/log/redmine-chupa-text/chieru/proxy:/var/log/squid:z" chupa-text: image: groonga/chupa-text:ubuntu-latest - container_name: chloe-redmine-chupa-text + container_name: chieru-redmine-chupa-text restart: always depends_on: - chupa-text-proxy environment: TZ: Asia/Tokyo http_proxy: http://chupa-text-proxy:3128/ https_proxy: http://chupa-text-proxy:3128/ RAILS_SERVE_STATIC_FILES: "true" volumes: - - "/var/log/redmine-chupa-text/chloe/rails:/home/chupa-text/chupa-text-http-server/log:z" + - "/var/log/redmine-chupa-text/chieru/rails:/home/chupa-text/chupa-text-http-server/log:z" volumes: dbdata:/etc/nginx/conf.d/chieru-redmine.confserver { listen 80; server_name chieru.[your-domain]; client_max_body_size 32m; proxy_buffer_size 32k; proxy_buffers 100 32k; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host $host:$server_port; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; location / { proxy_pass http://localhost:3002; } }おわりに
僕が初めて Redmine に触れてからおそらくは 8 年くらい、自分で構築するようになってから 5 年くらい経ちます。これまでに Bitnami Redmine や ALMinium を使って構築したり、 ソフトウェアエンジニアリング ― RedmineをCentOS 7上で動かすーUnicornとNginx編 を参考にスクラッチで構築したりしてきました。
しかし、どの方法で構築しても、インストールはともかく頻繁なバージョンアップにつらみを感じていました。スクラッチで構築したときには Ruby を
rbenv
でインストールしてみたり、Redmine プラグインやテーマをgit submodule
で Redmine 本体とリンクさせてみたり色々やってみたのですが、2, 3 くらいのインスタンスならまだしも、数十くらい…下手すりゃ百を超えてるかもしれないオーダーで存在するインスタンスのバージョンアップを面倒見るには限界がある、と。そこで Docker に着目して、どうやったら最小の手数でインストールからバージョンアップまでできるかを考えてみました。それも素の Redmine ではなく、なるべく便利なプラグインや素敵なテーマを詰め込んだ状態で。この Docker Compose で作った Redmine は
docker-compose build --no-cache
を実行するだけで簡単に最新バージョンに更新することができます。そして、どうやったら誰でもこの Redmine を使って簡単にサーバー構築ができることが伝えられるか、ということで本記事を執筆しました。このレシピはあくまで一例ですが、これでもっともっと多くの方が最新でプラグインやテーマが盛りだくさんな Redmine にすぐ触れられるようになって、Redmine 界隈がもっともっと盛り上がっていけばいいなと願っています。
- 投稿日:2020-05-23T01:31:39+09:00
まだdocker-composeのホスト側portを考えるのに疲弊しているの? 〜IP指定してwell-known ports使い放題、同時に1677万案件回す〜
タイトルは釣りです
1677万案件は試してません
対象読者
- 開発環境を docker-compose で構築しているWebプログラマの人々
- 複数案件を抱え、ホスト側ポート番号を考えるのに疲弊している人々
- HTTP
80
->80
はとっておいて8080
にしたろ!- あの案件で
8080
を使ったからこっちは10080
にしたろ!10080
の次だからとりあえず20080
にしたろ!- うーん、
8000
!w- HMRで
8080
使いたいけれど使われてるから8081
にしたろ!問題提起
docker-compose.ymlの例
anken-1/docker-compose.ymlversion: "3" services: web: image: "nginx" ports: - "80:80" # ...... 1 ... app: image: "php-fpm" ... db: image: "mysql:5.7" ports: - "3306:3306" # ...... 2 environment: - "MYSQL_ROOT_PASSWORD=root" secure-db: image: "mysql:5.7" ports: - "3307:3306" # ...... 3 environment: - "MYSQL_ROOT_PASSWORD=root"
- ホストOSのブラウザで動作確認するためにHTTPサーバーのポートを公開する(
80
)- 同じくホストから繋ぐためにDBのポートを公開する(
3306
)- DBは複数立っていたりする(
3307
)さて別の案件が降ってきました。
anken-2/docker-compose.ymlversion: "3" services: app: image: "httpd:2.4" ports: - "8080:80" # ...... 1 volumes: - "./public/:/usr/local/apache2/htdocs/" db: image: "mysql:5.7" ports: - "13306:3306" # ...... 2 environment: - "MYSQL_ROOT_PASSWORD=root" secure-db: image: "mysql:5.7" ports: - "13307:3306" # ...... 3 environment: - "MYSQL_ROOT_PASSWORD=root"先の案件で振ったポート番号とぶつからないように、well-knownポートの亜種をいい感じに考えて割り当てます
80
は別の案件で使ったので8080
3306
は別の案件で使ったので13306
3307
は別の案件で使ったので13307
さて最初の案件でHMRを導入したくなりました。
anken-1/docker-compose.ymlversion: "3" services: web: image: "nginx" ports: - "80:80" ... app: image: "php-fpm" ... db: image: "mysql:5.7" ports: - "3306:3306" environment: - "MYSQL_ROOT_PASSWORD=root" secure-db: image: "mysql:5.7" ports: - "3307:3306" environment: - "MYSQL_ROOT_PASSWORD=root" + node: + build: + ... + ports: + - "8080:8080" # ...... 4
HMR用のNode.jsサーバーを立てたくなりました。
んじゃ8080で…anken-2/docker-compose.yml... app: image: "httpd:2.4" ports: - "8080:80" ...あ!8080を既に使っていました…
anken-1/docker-compose.ymlversion: "3" services: web: image: "nginx" ports: - "80:80" ... app: image: "php-fpm" ... db: image: "mysql:5.7" ports: - "3306:3306" environment: - "MYSQL_ROOT_PASSWORD=root" secure-db: image: "mysql:5.7" ports: - "3307:3306" environment: - "MYSQL_ROOT_PASSWORD=root" + node: + build: + ... + ports: + - "8081:8080" # ...... 4
8081にしておこう…と、こうなります。
別々の案件を抱える複数の人間が絡むと混乱は加速します。
そのうちポート番号込みでgit管理するのが嫌になってきて、
「ホスト側portはもう各自の.envで管理してくれ」
となります。
anken-1/docker-compose.ymlversion: "3" services: web: image: "nginx" ports: - "${WEB_HOST_PORT}:80" ... app: image: "php-fpm" ... db: image: "mysql:5.7" ports: - "${DB_HOST_PORT}:3306" environment: - "MYSQL_ROOT_PASSWORD=root" secure-db: image: "mysql:5.7" ports: - "${SECURE_DB_HOST_PORT}:3306" environment: - "MYSQL_ROOT_PASSWORD=root" node: build: ... ports: - "${NODE_HMR_HOST_PORT}:8080"anken-1/env_exampleWEB_HOST_PORT=80 DB_HOST_PORT=3306 SECURE_DB_HOST_PORT=3307 NODE_HMR_HOST_PORT=8080anken-2/docker-compose.ymlversion: "3" services: app: image: "httpd:2.4" ports: - "${APP_HOST_PORT}:80" volumes: - "./public/:/usr/local/apache2/htdocs/" db: image: "mysql:5.7" ports: - "${DB_HOST_PORT}:3306" environment: - "MYSQL_ROOT_PASSWORD=root" secure-db: image: "mysql:5.7" ports: - "${SECURE_DB_HOST_PORT}:3306" environment: - "MYSQL_ROOT_PASSWORD=root"anken-2/env_exampleAPP_HOST_PORT=80 DB_HOST_PORT=3306 SECURE_DB_HOST_PORT=3307幾分マシになった感じがしますが、しばらくすると
- 「何箇所設定するね〜んww」
- 「何もしてないのに
docker-compose up -d
が立ち上がらなくなった」
- env_exampleの更新を.envへ反映し忘れている
等々の問題が生じてきます。
解決案 -- ポートをバインドするIPを指定する
- サンプルrepository
- 名前をdocker-compose r にしてしまう痛恨のミス
公式docに書いてありますが、
docker run -p
ではホスト側ポートをバインドするIPアドレスを指定できます。1つは docker run の実行時、 -p IP:ホスト側ポート:コンテナ側ポート か
-p IP::ポート を指定し、特定の外部インターフェースをバインドする指定ができます。「IPアドレス」というのは、は論理的なアドレスです。
物理的なホストに対して複数の論理的なアドレスが割り当て可能です。往来で
「D.Horiyamaさんの80番ポートさん、HTMLファイルをください」
と言われたらHTMLファイルを返しますが、
「‡漆黒の堕天使‡さんの80番ポートさん、HTMLファイルをください」
と言われたら、こっ恥ずかしくて聞いてないふりしますよね。そういうことです。
「この名前で呼ばれた時だけ応答する」ということができるわけです。
さて、このホスト側IP指定、docker-compose.yml でも設定可能です:
docker-compose.ymlversion: "3" services: web: image: "httpd:2.4" ports: - "${IP}:80:80" volumes: - "./public/:/usr/local/apache2/htdocs/" db: image: "mysql:5.7" ports: - "${IP}:3306:3306" environment: - "MYSQL_ROOT_PASSWORD=root" secure-db: image: "mysql:5.7" ports: - "${IP}:3307:3306" environment: - "MYSQL_ROOT_PASSWORD=root".envIP=127.0.0.1こんな風にします。
127.*.*.*
は ローカルループバックアドレス と呼ばれる特別なアドレスで、ホスト自身を指します。
127.0.0.1
〜127.255.255.254
は全て別のIPアドレスであり、個々にポートをバインドできます。
これを使って、「IP=127.0.0.1は案件1用」
というようにして、
80
や3306
といったwell-known portを惜しげもなくじゃぶじゃぶ使うことができます。$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 36d5b663edf6 mysql:5.7 "docker-entrypoint.s…" 3 seconds ago Up 2 seconds 127.0.0.2:3306->3306/tcp, 33060/tcp anken-2_db_1 c347fe183ad6 httpd:2.4 "httpd-foreground" 3 seconds ago Up 2 seconds 127.0.0.2:80->80/tcp anken-2_web_1 8ad0a5ae018d mysql:5.7 "docker-entrypoint.s…" 3 seconds ago Up 2 seconds 33060/tcp, 127.0.0.2:3307->3306/tcp anken-2_secure-db_1 6955a89c43d9 httpd:2.4 "httpd-foreground" 29 seconds ago Up 28 seconds 127.0.0.1:80->80/tcp anken-1_web_1 f13161e0dfef mysql:5.7 "docker-entrypoint.s…" 29 seconds ago Up 28 seconds 33060/tcp, 127.0.0.1:3307->3306/tcp anken-1_secure-db_1 c1905356e589 mysql:5.7 "docker-entrypoint.s…" 29 seconds ago Up 28 seconds 127.0.0.1:3306->3306/tcp, 33060/tcp anken-1_db_1↑
80
,3306
,3307
をanken-1, anken-2それぞれで利用できています。
127.0.0.1:80
,127.0.0.2:80
にリクエストしてみると:$ curl 127.0.0.1:80 <html>案件1</html> $ curl 127.0.0.2:80 <html>案件2</html>ちゃんと、それぞれ別々のサーバーに処理されていることがわかります。
さて、IPv4発明当初の仕様策定陣は、これほどまでにインターネットが隆盛を極めるとは考えていなかったのでしょう。
全アドレス空間のうち1/256 -- 約1677万個 -- もの膨大なアドレス空間をローカルループバックアドレスに宛ててしまいました。
(グローバルIPアドレスとして使えなくなってしまいました)おかげさまで、この方法で、理論的には約1677万案件ぶんの docker-compose 環境を同時に抱えられます!(タイトル回収)
生のIPアドレスはつらいので、
/etc/hosts
を書いたりDNSを立てたりするとより便利になるでしょう:/etc/hosts... 127.0.0.1 anken-1 127.0.0.2 anken-2 ...curl
$ curl anken-1:80 <html>案件1</html>mysql
$ mysql -h anken-1 -u root -proot Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 3 Server version: 5.7.27 MySQL Community Server (GPL) Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> \qnginx-proxyとの違い
?「nginx-proxyでいいんじゃ?」
nginx-proxyはHTTPにしか使えないと聞きます。(使ったことない)
対して、本記事の方法はHTTP以外にも適用可能です。
macOS固有の問題
Docker Desktop for mac 2.2.x の不具合
Docker Desktop for macでは、2.2.x系にて不具合があり、127.0.0.1(デフォルト)以外のIPアドレスのポートをバインドできなかったようです。
さっさと2.3系に上げましょう。
127.0.0.1 以外のローカルループバックアドレスがデフォルトで使えない
デフォルトで
ping 127.0.0.2
すら通りません。ifconfig
でエイリアス定義する必要があります。下記のようなスクリプトを
.zshrc
などで実行するとよいでしょう:for ((i=2;i<256;i++)) do sudo ifconfig lo0 alias 127.0.0.$i up done
- 投稿日:2020-05-23T00:15:06+09:00
centOS8でDockerコンテナから名前解決ができない話をnftコマンドで解決する
はじめに
centOS8にてDocker動かそうと思ったら下記エラーが出て名前解決ができず、カスタムイメージが作れなかった問題を解決できたのでメモ
$ docker run -ti --rm busybox nslookup google.com nslookup: write to '<ホスト指定のDNS>': No route to host ;; connection timed out; no servers could be reachedTL:DR
結論から言えばCentOS8からデフォルトとなったNFTablesにおいて下記コマンドを実施し、コンテナ内部からのICMP以外のパケットがドロップされないようにする。
nft add rule inet firewalld filter_FWDI_public_allow counter udp dport 53 acceptもしくはもっと簡潔に
--net=host
オプションをつける方法やfirewall-cmd
を使う方法(CentOS8にDockerをインストール。名前解決できなかったのが解消した。)、バックエンドをiptablesに置き換える方法(CentOS8でDocker CEを使うのは(現状は)やめとけという話)などがあるようです。いろいろなアプローチ方法があります。前提条件
筆者の環境は、centos8をVirtualBOXにminimalインストールした下記の通りです。
$ cat /etc/os-release NAME="CentOS Linux" VERSION="8 (Core)" ID="centos" ID_LIKE="rhel fedora" VERSION_ID="8" PLATFORM_ID="platform:el8" PRETTY_NAME="CentOS Linux 8 (Core)" ANSI_COLOR="0;31" CPE_NAME="cpe:/o:centos:centos:8" HOME_URL="https://www.centos.org/" BUG_REPORT_URL="https://bugs.centos.org/" CENTOS_MANTISBT_PROJECT="CentOS-8" CENTOS_MANTISBT_PROJECT_VERSION="8" REDHAT_SUPPORT_PRODUCT="centos" REDHAT_SUPPORT_PRODUCT_VERSION="8"$ nft --version nftables v0.9.0 (Fearless Fosdick)事の成り行きから話すと、まずcentos8にDockerをインストール後(そもそも違うの使えというのが上で挙げた物ですが)、自作イメージをビルドしようとしたら名前解決ができずにエラー。
Dockerコンテナ内からpingはホストのGatewayに通るが、名前解決しようとするとicmp type3 (Destination Unreachable)が帰ってくる。
iptablesでUDPパケットの到達点を地道に調べてみても(参考:Netfilterの処理順序)、POSTROUTING targetに到達する直前にはfilter tableのFORWORD targetにてACCEPTされているハズなのにそこでどこかに消えている。。。解決
centos8になったことでnetfileterに対するインターフェースがiptablesからnftables, firewalldになっているということだったので試しにnftコマンドによってフィルタリングルールを見てみるとrejectしている奴がいました。
$ nft list ruleset | grep -n reject 244: reject with icmpx type admin-prohibited 252: ip6 daddr { ::/96, ::ffff:0.0.0.0/96, 2002::/24, 2002:a00::/24, 2002:7f00::/24, 2002:a9fe::/32, 2002:ac10::/28, 2002:c0a8::/32, 2002:e000::/19 } reject with icmpv6 type addr-unreachable 258: reject with icmpx type admin-prohibited 264: ip6 daddr { ::/96, ::ffff:0.0.0.0/96, 2002::/24, 2002:a00::/24, 2002:7f00::/24, 2002:a9fe::/32, 2002:ac10::/28, 2002:c0a8::/32, 2002:e000::/19 } reject with icmpv6 type addr-unreachable今回hook forwardしているchainを知りたいので、上記のrejectを行うルールを含むchainを見てみると
ADDRESS FAMILY: inet
TABLE : firewalld
CHAIN : filter_FORWARD
のものがどうやらdocker0からのパケットをrejectしてしまっているようです(iptablesで定義されているTABLEとは異なるため、nftじゃないと参照できない)。$ nft list chain inet firewalld filter_FORWARD table inet firewalld { chain filter_FORWARD { type filter hook forward priority 10; policy accept; ct state established,related accept ct status dnat accept iifname "lo" accept ip6 daddr { ::/96, ::ffff:0.0.0.0/96, 2002::/24, 2002:a00::/24, 2002:7f00::/24, 2002:a9fe::/32, 2002:ac10::/28, 2002:c0a8::/32, 2002:e000::/19 } reject with icmpv6 type addr-unreachable jump filter_FORWARD_IN_ZONES_SOURCE jump filter_FORWARD_IN_ZONES jump filter_FORWARD_OUT_ZONES_SOURCE jump filter_FORWARD_OUT_ZONES ct state invalid drop reject with icmpx type admin-prohibited # ここまでにacceptされなければrejectされてしまう } }ちなみにフィルタリングの評価順序についてはこちらの開発元のWikiを参考にすると、priorityの値が小さい順にBase Chain(typeとhookが定められているChain)で評価されていって、dropされない限りは全てのChainを通過するようです。
つまりは上記のchainのなかでrejectされる前のどこかでacceptしてしまえばDNS用のパケットは通過できるようになるハズ(その他の部分ではacceptされるようになっていることを確認済)。
ルールを定めるため、jumpなどからそれっぽいところを辿っていくとfilter_FWDI_public_allowというchainを見つけました。$ nft list chain inet firewalld filter_FORWARD_IN_ZONES table inet firewalld { chain filter_FORWARD_IN_ZONES { iifname "enp0s3" goto filter_FWDI_public goto filter_FWDI_public } }$ nft list chain inet firewalld filter_FWDI_public table inet firewalld { chain filter_FWDI_public { jump filter_FWDI_public_pre jump filter_FWDI_public_log jump filter_FWDI_public_deny jump filter_FWDI_public_allow jump filter_FWDI_public_post meta l4proto { icmp, ipv6-icmp } accept # pingだけ通るようになっていた部分の設定 } }$ nft list chain inet firewalld filter_FWDI_public_allow table inet firewalld { chain filter_FWDI_public_allow { } }二つ目の、filter_FWDI_publicでicmpはacceptしているからpingだけ通るのだなと納得。
そういうわけで冒頭のコマンドを持ってしてDNS用のパケットをここでacceptするようにすると、無事コンテナ内部から名前解決できるようになりました$ nft add rule inet firewalld filter_FWDI_public_allow counter udp dport 53 accept $ nft list chain inet firewalld filter_FWDI_public_allow table inet firewalld { chain filter_FWDI_public_allow { counter packets 0 bytes 0 udp dport domain accept #<-追加されたもの } }$ docker run -ti --rm busybox nslookup google.com Server: <ホスト指定のDNS> Address: <ホスト指定のDNS>:53 Non-authoritative answer: Name: google.com Address: 172.217.25.238 $ nft list chain inet firewalld filter_FWDI_public_allow table inet firewalld { chain filter_FWDI_public_allow { counter packets 2 bytes 112 udp dport domain accept # counterをつけているため、パケットが通過したことが分かる } }ちなみに同様の手順でhttpを許可するとコンテナ内部でパッケージマネージャも動作するようになります。
$ nft add rule inet firewalld filter_FWDI_public_allow tcp dport { 80,443 } accept $ nft list chain inet firewalld filter_FWDI_public_allow table inet firewalld { chain filter_FWDI_public_allow { counter packets 0 bytes 0 udp dport domain accept tcp dport { http, https } accept #<-追加されたもの } } $ docker run -ti --rm alpine apk add openssl fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz (1/1) Installing openssl (1.1.1g-r0) Executing busybox-1.31.1-r9.trigger OK: 6 MiB in 15 packagesめでたしめでたし
追加調査:Netfilterの評価順について
上記で問題は解決しましたが、nftについて少し学んだので合わせてメモ
centos8インストールした状態で、hook forwardに纏わるチェーンは以下のように設定されていました。$ nft list chains | sed 's/^}$/}\n/' | awk 'BEGIN{RS="";FS="\n"} { for(i=0;i<NF;i++){ if( match($i, "chain") && match($(i+1), "forward") ) {print $1, $i, $(i+1)} } }' table ip filter { chain FORWARD { type filter hook forward priority 0; policy drop; table ip6 filter { chain FORWARD { type filter hook forward priority 0; policy accept; table bridge filter { chain FORWARD { type filter hook forward priority -200; policy accept; table ip security { chain FORWARD { type filter hook forward priority 150; policy accept; table ip mangle { chain FORWARD { type filter hook forward priority -150; policy accept; table ip6 security { chain FORWARD { type filter hook forward priority 150; policy accept; table ip6 mangle { chain FORWARD { type filter hook forward priority -150; policy accept; table inet firewalld { chain filter_FORWARD { type filter hook forward priority 10; policy accept;本稿の上部で触れたPriorityの話と照らし合わせると、forwardされるパケットは
bridge filter -> ip(ip6) mangle -> ip(ip6) filter -> inet firewalld -> ip(ip6) security
の順で通過するハズである。
テストのため、下記のようにログを取得するルールを追加する。$ while read AF T C ORD;do > nft insert rule $AF $T $C counter log prefix \""$ORD FORWARD>> "\" > done<<EOF > bridge filter FORWARD 1st > ip mangle FORWARD 2nd > ip6 mangle FORWARD 3rd > ip filter FORWARD 4th > ip6 filter FORWARD 5th > inet firewalld filter_FORWARD 6th > ip security FORWARD 7th > ip6 security FORWARD 8th > EOFログ機能が追加されたことを確認。
$ nft list ruleset | grep -v '^$' | sed 's/^}$/}\n/' | awk 'BEGIN{RS="";FS="\n"} { for(i=0;i<NF;i++){ if( match($i, "chain") && match($(i+1), "forward") && match($(i+2), "log")) {print $1, $i, $(i+1),$(i+2)} } }' table ip filter { chain FORWARD { type filter hook forward priority 0; policy drop; counter packets 0 bytes 0 log prefix "4th FORWARD>> " table ip6 filter { chain FORWARD { type filter hook forward priority 0; policy accept; counter packets 0 bytes 0 log prefix "5th FORWARD>> " table bridge filter { chain FORWARD { type filter hook forward priority -200; policy accept; counter packets 0 bytes 0 log prefix "1st FORWARD>> " table ip security { chain FORWARD { type filter hook forward priority 150; policy accept; counter packets 0 bytes 0 log prefix "7th FORWARD>> " table ip mangle { chain FORWARD { type filter hook forward priority -150; policy accept; counter packets 0 bytes 0 log prefix "2nd FORWARD>> " table ip6 security { chain FORWARD { type filter hook forward priority 150; policy accept; counter packets 0 bytes 0 log prefix "8th FORWARD>> " table ip6 mangle { chain FORWARD { type filter hook forward priority -150; policy accept; counter packets 0 bytes 0 log prefix "3rd FORWARD>> " table inet firewalld { chain filter_FORWARD { type filter hook forward priority 10; policy accept; counter packets 0 bytes 0 log prefix "6th FORWARD>> "hook forwardさせるため、コンテナ内部から各種通信を行う。
まずはコンテナからホストネットワークへの疎通確認(ping)。ここでは上記の中からアドレスファミリーとしてip, inetが選択されるため、その中のテーブルを通過することになる。そのためsyslogには2nd, 4th, 6th, 7thのログが出力されることになる。### in container $ ping 192.168.13.1### in host $ tail -n0 -f /var/log/messages | sed 's/... .. ..:..:../MON DD hh:mm:ss/' MON DD hh:mm:ss centos8 kernel: 2nd FORWARD>> IN=docker0 OUT=enp0s3 PHYSIN=veth8ee9805 MAC=02:42:04:70:8d:9d:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=192.168.13.1 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=10740 DF PROTO=ICMP TYPE=8 CODE=0 ID=1536 SEQ=0 MON DD hh:mm:ss centos8 kernel: 4th FORWARD>> IN=docker0 OUT=enp0s3 PHYSIN=veth8ee9805 MAC=02:42:04:70:8d:9d:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=192.168.13.1 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=10740 DF PROTO=ICMP TYPE=8 CODE=0 ID=1536 SEQ=0 MON DD hh:mm:ss centos8 kernel: 6th FORWARD>> IN=docker0 OUT=enp0s3 PHYSIN=veth8ee9805 MAC=02:42:04:70:8d:9d:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=192.168.13.1 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=10740 DF PROTO=ICMP TYPE=8 CODE=0 ID=1536 SEQ=0 MON DD hh:mm:ss centos8 kernel: 7th FORWARD>> IN=docker0 OUT=enp0s3 PHYSIN=veth8ee9805 MAC=02:42:04:70:8d:9d:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=192.168.13.1 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=10740 DF PROTO=ICMP TYPE=8 CODE=0 ID=1536 SEQ=0次にipv6でのコンテナ間疎通確認。
ここではアドレスファミリーとしてbridge(dockerによって自動生成されたブリッジデバイス間(vethxxxxx)でforwardするため), ip6, inetが選択されるハズなので、本稿で設定した中では 1st, 3rd, 5th, 6th, 8thのhook forwardを経由する。
dockerではipv6はデフォルトでdisableになっているので、こちらに従い各種設定が必要。
バージョンの違いかデフォルトのデバイス(docker0)にipv6のアドレスプールが割り当てられなかったが、直接プールを指定して作成したらなんとかできた。### in host # daemonの設定 $ vim /etc/docker/daemon.json # 下記の設定を書き加える、もしくはデーモン起動オプションに指定 { "ipv6": true } # 設定のリロード $ systemctl reload docker # docker networkの作成 $ docker network create --ipv6 --subnet fe80:0:0:42:ff::/80 mynetv6 # 上記ネット内でコンテナを二台立ち上げる $ docker run --rm -ti --net mynetv6 --name C1 busybox sh $ docker run --rm -ti --net mynetv6 --name C2 busybox sh ### in container C1 $ ping -6 C2### in host $ tail -n0 -f /var/log/messages | sed 's/... .. ..:..:../MON DD hh:mm:ss/' MON DD hh:mm:ss centos8 kernel: 1st FORWARD>> IN=veth08a234c OUT=veth116ee6a MAC=33:33:00:00:00:02:02:42:ac:1f:00:02:86:dd SRC=fe80:0000:0000:0042:00ff:0000:0000:0002 DST=ff02:0000:0000:0000:0000:0000:000 0:0002 LEN=56 TC=0 HOPLIMIT=255 FLOWLBL=0 PROTO=ICMPv6 TYPE=133 CODE=0 MON DD hh:mm:ss centos8 kernel: 3rd FORWARD>> IN=br-7e666194822f OUT=br-7e666194822f PHYSIN=veth08a234c PHYSOUT=veth116ee6a MAC=33:33:00:00:00:02:02:42:ac:1f:00:02:86:dd SRC=fe80:0000:0000:0042:00ff:0000: 0000:0002 DST=ff02:0000:0000:0000:0000:0000:0000:0002 LEN=56 TC=0 HOPLIMIT=255 FLOWLBL=0 PROTO=ICMPv6 TYPE=133 CODE=0 MON DD hh:mm:ss centos8 kernel: 5th FORWARD>> IN=br-7e666194822f OUT=br-7e666194822f PHYSIN=veth08a234c PHYSOUT=veth116ee6a MAC=33:33:00:00:00:02:02:42:ac:1f:00:02:86:dd SRC=fe80:0000:0000:0042:00ff:0000: 0000:0002 DST=ff02:0000:0000:0000:0000:0000:0000:0002 LEN=56 TC=0 HOPLIMIT=255 FLOWLBL=0 PROTO=ICMPv6 TYPE=133 CODE=0 MON DD hh:mm:ss centos8 kernel: 6th FORWARD>> IN=br-7e666194822f OUT=br-7e666194822f PHYSIN=veth08a234c PHYSOUT=veth116ee6a MAC=33:33:00:00:00:02:02:42:ac:1f:00:02:86:dd SRC=fe80:0000:0000:0042:00ff:0000: 0000:0002 DST=ff02:0000:0000:0000:0000:0000:0000:0002 LEN=56 TC=0 HOPLIMIT=255 FLOWLBL=0 PROTO=ICMPv6 TYPE=133 CODE=0 MON DD hh:mm:ss centos8 kernel: 8th FORWARD>> IN=br-7e666194822f OUT=br-7e666194822f PHYSIN=veth08a234c PHYSOUT=veth116ee6a MAC=33:33:00:00:00:02:02:42:ac:1f:00:02:86:dd SRC=fe80:0000:0000:0042:00ff:0000: 0000:0002 DST=ff02:0000:0000:0000:0000:0000:0000:0002 LEN=56 TC=0 HOPLIMIT=255 FLOWLBL=0 PROTO=ICMPv6 TYPE=133 CODE=0というわけで、無事全ての設定したhook forwardを通過し、想定通りの動作をしていることが確かめられました。
終わりに
元の問題はnftablesとdockerの不整合でしたが、どうせならnftables扱えるようになろうという事で取り組み始めました。ネットワークはやはり奥が深いですね。
記事書くに当たりiptables, nftables, firewalldの関係性が曖昧でしたが、netfilterとfirewalldとiptablesとnftablesの関係の記事がわかりやすくまとめてくださっていたので、ここまで来てくださった方がいたら是非ご覧ください。