はじめに
Docker Composeは複数のコンテナを使う Docker アプリケーションを、定義・実行するツールです。コマンドを1つ実行するだけで、設定した全てのサービス(Dockerコンテナ)を作成・起動します。
ファイル構成
(Working dir)
+- docker-compose.yml
+- nginx-app/
+- conf.d/
+- default.conf
+- html/
+- index.html
+- node-app/
+- app.js
+- package.json
実行環境のインストール
Dockerのインストールについては docs.docker.com を参照してください。
Docker Composeのインストールについてはこちらを参照してください。
* Docker for Macをインストールした場合、Docker Composeはデフォルト入っています。
下記のコマンドを正しく表示すれば、次へ進めます。
$ docker -v
Docker version 18.03.1-ce, build 9ee9f40
$ docker-compose -v
docker-compose version 1.21.1, build 5a3f1a3
最初のコンテナの作成
NginxのオフィシャルイメージはKitematicを含め、Dockerのデモストレーションにしばしば活用されています。
今回私たちもNginxのイメージから始めます。
version: '2'
services:
nginx:
image: nginx
container_name: nginx-app
ports:
- "8080:80"
docker-compose.yml
の詳しい使い方は下記のページをご覧ください。
http://docs.docker.jp/compose/compose-file.html
上記docker-compose.yml
を作成して、docker-compose up
を実行すれば、http://localhost:8080 でこのようなページがみれます。
ファイルの共有
Nginxのオフィシャルイメージの説明によると、先ほどのサンプルページは(コンテナの中の)/usr/share/nginx/html
にあることがわかります。そのファイルを変更することで、カスタムページを表示できます。
フォルダとページを作成
$ mkdir nginx-app && cd nginx-app
$ mkdir html && touch html/index.html
ぶっちゃけApacheのサンプルページっぽいものを作ります。
<h1>It works!</h1>
volumes
フィールドを追加
version: '2'
services:
nginx:
image: nginx
container_name: nginx-app
ports:
- "8080:80"
+ volumes:
+ - ./nginx-app/html:/usr/share/nginx/html:ro
Docker Composeの再起動
通常、ctrl + c
を押せば、docker composeが停止してくれて、またdocker-compose up
で作り直してくれますが、場合によってうまく停止できない時があります。その時は docker-compose stop
でコンテナを強制終了し、docker-compose up
で再起動します。
再起動できたら、あたかもApacheのようなページが http://localhost:8080 に表示されます。
Nginxの設定の編集
/etc/nginx/nginx.conf
の確認
下記コマンドでNginxのコンテナ(nginx-app)にログインし、nginx.conf
を確認します。
$ docker exec -it nginx-app bash
# ログインした
root@3d5880dbf667:/# cat /etc/nginx/nginx.conf
...
include /etc/nginx/conf.d/*.conf;
...
root@3d5880dbf667:/# ls /etc/nginx/conf.d/
default.conf
つまり/etc/nginx/conf.d
にあるdefault.conf
を読み込んでいることがわかります。
default.conf
を外に出す
$ cd nginx-app/ && mkdir conf.d
$ touch conf.d/default.conf
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
version: '2'
services:
nginx:
image: nginx
container_name: nginx-app
ports:
- "8080:80"
volumes:
- ./nginx-app/html:/usr/share/nginx/html:ro
+ - ./nginx-app/conf.d/default.conf:/etc/nginx/conf.d/default.conf:ro
修正ができたら、Docker Composeを再起動します。
Node.jsのコンテナの実装
いよいよアプリケーション側の実装ができるようになりました。
簡単なnode.jsサーバーの作成
$ mkdir node-app && cd node-app && touch app.js
$ npm init -y
'use strict';
const Koa = require('koa');
const Router = require('koa-router');
const logger = require('koa-logger');
const PORT = process.env['PORT'] || 3030;
const app = new Koa();
const router = new Router();
router
.get('/api', (ctx, next) => {
ctx.body = '<h1>Node API</h1>';
})
app
.use(logger())
.use(router.routes())
.use(router.allowedMethods());
const server = require('http').createServer(app.callback());
server.listen(PORT);
console.log(`Server on localhost:${PORT}`);
{
"name": "node-app",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"ioredis": "^4.5.1",
"koa": "^2.7.0",
"koa-logger": "^3.2.0",
"koa-router": "^7.4.0",
"socket.io": "^2.2.0"
}
}
version: '2'
services:
nginx:
image: nginx
container_name: nginx-app
+ links:
+ - node-app
ports:
- "8080:80"
volumes:
- ./nginx-app/html:/usr/share/nginx/html:ro
- ./nginx-app/conf.d/default.conf:/etc/nginx/conf.d/default.conf:ro
+ node:
+ image: node:10
+ user: node
+ container_name: node-app
+ working_dir: /home/node/app
+ volumes:
+ - ./node-app:/home/node/app
+ environment:
+ - NODE_ENV=development
+ - PORT=3030
+ command: node app.js
+ ports:
+ - "3030:3030" # Debug用
Node.jsのオフィシャルイメージの使い方はこちらを参考してください。
Docker composeを再起動すれば、下記ページが http://localhost:3030/api に表示されます。
Nginxでnode.jsサーバーのproxyを設定
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
+ location /node/ {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_pass http://node-app:3030/; # docker-compose.ymlの設定と一致
+ }
}
Docker composeを再起動すれば、先ほどのNode API
が書いていたページは http://localhost:8080/node/api でも表示されます。
既知のBug
上記の設定で、もしnode.js側のroot(/
)にアクセスしようとする時、必ず http://localhost:8080/node/ を使わなければいけません。
最後のSlashを省略した場合、ポート番号のない http://localhost/node へリダイレクトされちゃいます。
Socket.ioをサーバー側に追加
app.js
に下記のコードを追加
const io = require('socket.io')(server);
io.on('connection', (socket) => {
console.log('connected');
socket
.on('io-ping', (msg) => {
console.log(`io-ping: ${msg}`);
socket.emit('io-pong', msg);
})
})
index.html
にもロジックを追加
<p>
<b>Delay: </b>
<span id="delay" style="display:inline-block;width:20px;text-align:right">??</span>
<span>ms </span>
<button type="button" onclick="ping()">Ping</button>
</p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.slim.js"></script>
<script>
const url = '/';
const socket = io(url, { path: '/node/socket.io' }); // important!
socket
.on('connect', msg => {
console.log('connected');
})
.on('io-pong', msg => {
console.log(msg);
const delay = Date.now() - parseInt(msg);
document.querySelector('#delay').innerHTML = delay;
})
function ping() {
socket.emit('io-ping', Date.now())
}
</script>
PS: 我々はnode.jsサーバーのルートを/node/
に置けたので、ブラウザ側でsocket.ioでアクセスする場合、必ずpath: '/node/socket.io'
を明記しなければいけません。
default.conf
も変更します。
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /node/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ # for websocket
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_cache_bypass $http_upgrade;
proxy_pass http://node-app:3030/; # docker-compose.ymlの設定と一致
}
}
再起動すると、http://localhost:8080 でサーバーのラグを表示してくれるアプリがみれます。
Redisでデータをストック
先ほど我々は Nginx/Socket.io サーバー群を構成しました。データベースも繋いていけば、完璧です。
オフィシャルredisイメージの追加
version: '2'
services:
nginx:
image: nginx
container_name: nginx-app
links:
- node-app
ports:
- "8080:80"
volumes:
- ./nginx-app/html:/usr/share/nginx/html:ro
- ./nginx-app/conf.d/default.conf:/etc/nginx/conf.d/default.conf:ro
node:
image: node:10
+ links:
+ - redis-db
user: node
container_name: node-app
working_dir: /home/node/app
volumes:
- ./node-demo-app:/home/node/app
environment:
- NODE_ENV=development
- PORT=3030
command: node app.js
- ports:
- - "3030:3030" # Debug用
+ redis:
+ image: redis
+ container_name: redis-db
node.jsサーバーの修正
'use strict';
const Koa = require('koa');
const Router = require('koa-router');
const logger = require('koa-logger');
const PORT = process.env['PORT'] || 3003;
const app = new Koa();
const router = new Router();
router
.get('/', (ctx, next) => {
ctx.body = '<h2>Hello World!</h2>';
})
.get('/api', (ctx, next) => {
ctx.body = '<h1>Node API</h1>';
})
.get('/api/:tag', (ctx, next) => {
ctx.body = `<h1>Node API</h1><p>${ctx.params['tag']}</p>`;
})
app
.use(logger())
.use(router.routes())
.use(router.allowedMethods());
const Redis = require('ioredis');
const REDIS_HOST = process.env['REDIS_HOST'] || '127.0.0.1';
const REDIS_PORT = process.env['REDIS_PORT'] ||6379;
const redis = new Redis({ host: REDIS_HOST, port: REDIS_PORT });
const psub = new Redis({ host: REDIS_HOST, port: REDIS_PORT });
redis.config('set', 'notify-keyspace-events', 'Ex');
const psubKey = '__keyevent@0__:expired'; // All key events & spaces in all DB
psub.psubscribe(psubKey, (err, count) => {
if (err) {
console.error('err', err);
}
});
psub.on('pmessage', (pattern, channel, message) => {
console.log('pattern', pattern);
console.log('channel', channel);
console.log('message', message);
});
const server = require('http').createServer(app.callback());
const io = require('socket.io')(server);
io.on('connection', (socket) => {
console.log('connected');
socket
.on('io-ping', (msg) => {
console.log(`io-ping: ${msg}`);
socket.emit('io-pong', msg);
redis.set(`io-ping@${msg}`, msg, 'EX', 10);
})
psub.on('pmessage', (pattern, channel, message) => {
socket.emit('key-expired', message);
});
})
server.listen(PORT);
console.log(`Server on localhost:${PORT}`);
HTMLも編集
<h1>It Works!</h1>
<p>
<b>Delay: </b>
<span id="delay" style="display:inline-block;width:20px;text-align:right">??</span>
<span>ms </span>
<button type="button" onclick="ping()">Ping</button>
</p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.slim.js"></script>
<script>
const url = '/';
const socket = io(url, { path: '/node/socket.io' }); // important!
socket
.on('connect', msg => {
console.log('connected');
})
.on('io-pong', msg => {
console.log(msg);
const delay = Date.now() - parseInt(msg);
document.querySelector('#delay').innerHTML = delay;
})
.on('key-expired', msg => {
console.log(msg);
alert(`key-expired: ${msg}`)
})
function ping() {
socket.emit('io-ping', Date.now())
}
</script>
解説
上記ロジックはこのようなことをしています:
1. HTMLの「Ping」ボタンが押された時、タイムスタンプをRedisに保存します。
2. 10秒後、先ほど保存されたタイムスタンプがExpireして、その旨をHTMLに送信します。
参考
https://github.com/jerrywdlee/docker_demo/tree/master/docker-compose
https://github.com/docker-library/docs/tree/master/nginx
https://medium.com/@spencerfeng/make-node-js-socket-io-angular-nginx-ssl-work-777404835e09