LoginSignup
9
7

More than 3 years have passed since last update.

Docker composeでnginx・socket.io・redisを構成してみた

Last updated at Posted at 2019-01-31

はじめに

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のイメージから始めます。

docker-compose.yml
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 でこのようなページがみれます。
image.png

ファイルの共有

Nginxのオフィシャルイメージの説明によると、先ほどのサンプルページは(コンテナの中の)/usr/share/nginx/htmlにあることがわかります。そのファイルを変更することで、カスタムページを表示できます。

フォルダとページを作成

$ mkdir nginx-app && cd nginx-app
$ mkdir html && touch html/index.html

ぶっちゃけApacheのサンプルページっぽいものを作ります。

index.html
<h1>It works!</h1>

volumesフィールドを追加

docker-compose.yml
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 に表示されます。
image.png

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
default.conf
server {
    listen       80;
    server_name  localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}
docker-compose.yml
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
app.js
'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}`);
package.json
{
  "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"
  }
}
docker-compose.yml
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 に表示されます。
localhost_8080_node_api.png

Nginxでnode.jsサーバーのproxyを設定

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;
+       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:&nbsp;</b>
  <span id="delay" style="display:inline-block;width:20px;text-align:right">??</span>
  <span>ms&nbsp;</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も変更します。

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 でサーバーのラグを表示してくれるアプリがみれます。
localhost_8080.png

Redisでデータをストック

先ほど我々は Nginx/Socket.io サーバー群を構成しました。データベースも繋いていけば、完璧です。

オフィシャルredisイメージの追加

docker-compose.yml
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サーバーの修正

app.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も編集

index.html
<h1>It Works!</h1>
<p>
  <b>Delay:&nbsp;</b>
  <span id="delay" style="display:inline-block;width:20px;text-align:right">??</span>
  <span>ms&nbsp;</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に送信します。

localhost_8080_の内容.png

参考

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

9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7