なになれ

IT系のことを記録していきます

MEAN StackをDocker Composeで動かす

前回はMEAN StackをHeroku上で動かしてみました。
hi1280.hatenablog.com

今回はMEAN Stackアプリに手を加えて、Dockerに対応させます。
Dockerにすれば、他の環境への移行も楽になるのではと思います。
Docker Composeを使って、nginx+Node.js+MongoDBの環境でMEAN Stackアプリを動かします。

プログラム一式はこちら
github.com

Docker Composeを使って、以下の3つのコンテナで構成します。

  • nginxでAngularを動かす(フロントエンドアプリ)
  • Node.jsでExpressを動かす(バックエンドアプリ)
  • MongoDBを動かす(DB)

Angularビルド環境用のDockerfile

AngularビルドのためにAngular CLI環境のDockerfileを作ります。

Dockerfile

FROM node:8.11.3-alpine
LABEL authors="hi1280"
RUN npm install -g @angular/cli@6.0.8
RUN npm cache clean --force

Node.jsの環境にAngular CLIをインストールしているだけというシンプルなモノです。

Angularアプリ実行用のDockerfile

Angularをビルドして、それをそのままDockerコンテナに含めると容量がかなり大きくなってしまいます。
そのため、Angularビルドとnginx上でのAngularアプリの実行を分けます。
これはDockerのマルチステージビルドの機能を使えば、実現できます。

Use multi-stage builds | Docker Documentation

マルチステージビルドを用いたDockerfileを作ります。

Dockerfile

# Angular Build
FROM hi1280/angular-cli:latest as build-stage
WORKDIR /usr/src/app
COPY ["package.json", "package-lock.json*", "./"]
RUN npm install --production --silent
COPY . .
RUN npm run build
# nginx
FROM nginx:alpine
RUN rm -rf /usr/share/nginx/html/*
COPY --from=build-stage /usr/src/app/dist/mean-example  /usr/share/nginx/html
COPY ./nginx /etc/nginx/conf.d
CMD ["nginx", "-g", "daemon off;"]

COPY --from=build-stageでビルド後の成果物のみをコピーしています。
これにより、最小限のファイルサイズになります。
AngularでビルドしたHTMLリソースをnginxで動かすDockerコンテナになります。

Express実行用のDockerfile

Expressを動かすDockerfileを作ります。

Dockerfile

FROM node:8.11.3-alpine
WORKDIR /usr/src/app
COPY ["server.js", "/src/server/package.json", "/src/server/package-lock.json", "./"]
COPY ["/src/server", "./src/server"]
RUN npm install --production --silent
EXPOSE 3000
ENV NODE_ENV production
CMD [ "node", "server.js" ]

Node.jsの環境を公式のイメージから取得して、Expressを動かすだけのDockerコンテナになります。

Docker Composeで各コンテナを連携

これまでに用意した各Dockerコンテナを連携します。

docker-compose.yml

version: '2.1'

services:
  frontend:
    build:
      context: .
      dockerfile: Dockerfile-frontend
    ports:
      - 80:80
    environment:
      - APP_HOST=backend
      - APP_PORT=3000
    command: /bin/sh -c "envsubst '$$APP_HOST$$APP_PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"
    depends_on:
      - backend
  backend:
    build:
      context: .
      dockerfile: Dockerfile-backend
    ports:
      - 3000:3000
    depends_on:
      - db
    environment: 
      - MONGODB_URI=mongodb://db:27017/my-heroes
  db:
    image: mongo:3.4.16-jessie
    restart: always
    volumes: 
      - /data/db:/data/db

URLにサービス名を指定することで、各サービスにアクセスすることができます。
mongodb://db:27017/my-heroesといった形です。
depends_onを指定することで、サービスを立ち上げるために事前に起動が必要なサービスを指定できます。
これにより、サービスを起動する順番を制御することができます。

Docker Composeにおけるnginxの設定変更

環境変数を用いて動的にnginxの設定を変更しています。
nginxの公式Dockerイメージにそのやり方が記載されています。(Using environment variables in nginx configurationという箇所です)
https://hub.docker.com/_/nginx/
こちらも参考にしました。
Docker上のNginxのconfに環境変数(env)を渡すたったひとつの全く優れてない方法(修正:+優れている方法)

default.conf.template

server {
    listen       80;
    server_name  localhost;

    location /api/ {
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_pass http://${APP_HOST}:${APP_PORT}/api/;
    }

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

}

${APP_HOST}と${APP_PORT}が環境変数で定義した値に変わります。
Expressのサービス名にアクセスする設定になります。

まとめ

公式のDockerイメージを活用すれば、プログラムをコピーして、起動コマンドを実行するといった形で、Dockerコンテナ化できるので難しいことはあまりないかなと思います。
ハマったら、コンテナのシェルに入ってデバッグするといったことができると良いです。

MEAN StackをHeroku上で動かす

MEAN Stackを運用するサービスを検討していましたが、Herokuが良いだろうという結論になり、実際に使ってみました。
Herokuを選んだ決め手は以下のとおりです。

  • Node.jsに対応している
  • MongoDBがアドオンで簡単に使える

Herokuのドキュメントを参考にして、自分なりにやってみました。
Create a Web App and RESTful API Server Using the MEAN Stack | Heroku Dev Center

MEAN Stackの構成

MEAN Stackのアプリを用意しました。
github.com Node.jsでExpressのAPIサーバとAngularで作成したフロントのコンテンツの両方を動かす形です。
これをHerokuでそのまま動かそうという算段です。

プログラムの参考元はこちら github.com

Herokuへのデプロイ

Heroku CLIをインストールします。
The Heroku CLI | Heroku Dev Center

Herokuのアプリ環境を作成します。

$ heroku create
...
Creating app... done, ⬢ secret-scrubland-78144
https://secret-scrubland-78144.herokuapp.com/ | https://git.heroku.com/secret-scrubland-78144.git

gitの設定にherokuというリポジトリが追加されています。

$ git config --list
...
remote.heroku.url=https://git.heroku.com/secret-scrubland-78144.git
remote.heroku.fetch=+refs/heads/*:refs/remotes/heroku/*

デプロイを行うには、このリポジトリにPushします。

$ git push heroku master

MongoDBの追加

HerokuにMongoDBを作成するには、mLabのアドオンを追加します。

$ heroku addons:create mongolab

DBの接続情報はMONGODB_URIという環境変数で取得できます。
今回のプログラムでは既にこの環境変数を使用して、MongoDBに接続するようになっています。

Heroku環境にアクセス

$ heroku open

こちらが今回作成したデモ環境です。
https://secret-scrubland-78144.herokuapp.com/

まとめ

Herokuを使えばMEAN Stackの運用環境を簡単に作成できることが分かりました。

PWA BuilderでPWA化してみてPWAを学ぶ

PWA BuilderというPWA化するためのToolを試してみた記録です。
ざっくりとPWAとは何なのかが分かる内容になっていると思います。

今回は以下のサンプルを利用しています。
Freelancer - One Page Theme - Start Bootstrap

PWA化したデモサイトはこちらです。
Freelancer - Start Bootstrap Theme

PWAとは

PWAはProgressive Web Appsのことです。
PWA Builderの説明によると、以下の内容を含んだものがPWAと言えると思います。

  • Web App Manifestを使用してモバイル端末等からインストールおよび起動されたときの見た目や動作を制御している
  • Service Workerを使用してオフライン対応がされている

マニフェストファイルを作成する

PWA Builderの手順に沿って説明します。
まずはGenerate Manifestでの手順です。
PWA Builderを使うと以下のマニフェストファイルが作成されます。

manifest.json

{
    "dir": "ltr",
    "lang": "Japanese",
    "name": "Freelancer - Start Bootstrap Theme",
    "scope": "/",
    "display": "standalone",
    "start_url": "https://arched-photon-204013.firebaseapp.com/",
    "short_name": "Freelancer",
    "theme_color": "transparent",
    "description": "",
    "orientation": "any",
    "background_color": "transparent",
    "related_applications": [],
    "prefer_related_applications": false,
    "icons": [
        {
            "src": "img/d2248ed4-ab6f-84da-fe68-5962b432d4ac.webPlatform.png",
            "sizes": "48x48",
            "type": "image/png"
        },
        {
            "src": "img/b1d7d8f5-1b9b-a7e9-e644-ba9db81e4d93.webPlatform.png",
            "sizes": "1240x600",
            "type": "image/png"
        },
        {
            "src": "img/4f8afd0d-e3e4-1f11-4f22-3bd7ebfb0c61.webPlatform.png",
            "sizes": "300x300",
            "type": "image/png"
        },
        {
            "src": "img/9c2802bf-a937-d809-fd3a-52f1059a43d8.webPlatform.png",
            "sizes": "150x150",
            "type": "image/png"
        },
        {
            "src": "img/fe805efb-f87e-ff12-eb44-86cc76225d06.webPlatform.png",
            "sizes": "88x88",
            "type": "image/png"
        },
        {
            "src": "img/bd3ec8f1-9ef0-de1f-3d55-093be35a1b5b.webPlatform.png",
            "sizes": "24x24",
            "type": "image/png"
        },
        {
            "src": "img/335ca134-e557-a127-dd7a-f1af5ee35285.webPlatform.png",
            "sizes": "50x50",
            "type": "image/png"
        },
        {
            "src": "img/e19531de-b9a8-0cf7-cc34-0d91b68a0339.webPlatform.png",
            "sizes": "620x300",
            "type": "image/png"
        },
        {
            "src": "img/01b4b12b-d6f8-5e7c-897b-642ae94809b2.webPlatform.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "img/a220fe60-f154-a4b0-2645-7d962bdffd78.webPlatform.png",
            "sizes": "144x144",
            "type": "image/png"
        },
        {
            "src": "img/f699c6fb-cd5b-58a0-6e9a-352f1520876c.webPlatform.png",
            "sizes": "96x96",
            "type": "image/png"
        },
        {
            "src": "img/809fd415-acc1-e2b3-39fb-70e2e867e0e9.webPlatform.png",
            "sizes": "72x72",
            "type": "image/png"
        },
        {
            "src": "img/233576f2-1bb0-e4cd-c0a3-bd0e4605f3c9.webPlatform.png",
            "sizes": "36x36",
            "type": "image/png"
        },
        {
            "src": "img/c1007a03-b2e6-e063-b1a6-e809c502ee0d.webPlatform.png",
            "sizes": "1024x1024",
            "type": "image/png"
        },
        {
            "src": "img/c84d6d5e-051e-1555-dca8-c80c1c2a3181.webPlatform.png",
            "sizes": "180x180",
            "type": "image/png"
        },
        {
            "src": "img/df1a52c0-a9dd-8923-51c4-8fd1ab582afb.webPlatform.png",
            "sizes": "152x152",
            "type": "image/png"
        },
        {
            "src": "img/19a904d2-30d1-720c-52b6-c44bd97cf378.webPlatform.png",
            "sizes": "120x120",
            "type": "image/png"
        },
        {
            "src": "img/3235cb0f-c9a5-2a68-d389-9168a8afe050.webPlatform.png",
            "sizes": "76x76",
            "type": "image/png"
        },
        {
            "src": "img/android-launchericon-512-512.png",
            "sizes": "512x512",
            "type": "image/png"            
        }
    ]
}

注目する設定としては、displayプロパティの指定でネイティブアプリを起動するような形でWebアプリを起動することができます。
pwabuilder.comからマニフェストファイルを作成できる対象は公開されているサイトになります。
今回はFirebase上にデプロイしてサイトを公開しています。

Service WorkerのJavaScriptファイルを作成する

Build Service Workerでの手順です。
Cache-first networkを選択します。
以下のファイルが作成されます。

pwabuilder-sw-register.js

//This is the service worker with the Cache-first network

//Add this below content to your HTML page, or add the js file to your page at the very top to register service worker
if (navigator.serviceWorker.controller) {
  console.log('[PWA Builder] active service worker found, no need to register')
} else {

//Register the ServiceWorker
  navigator.serviceWorker.register('pwabuilder-sw.js', {
    scope: './'
  }).then(function(reg) {
    console.log('Service worker has been registered for scope:'+ reg.scope);
  });
}

index.htmlで読み込むJavaScriptファイルになります。
ServiceWorkerを登録するために利用します。

pwabuilder-sw.js

//This is the service worker with the Cache-first network

var CACHE = 'pwabuilder-precache';
var precacheFiles = [
      /* Add an array of files to precache for your app */
      'vendor/bootstrap/css/bootstrap.min.css',
      'vendor/font-awesome/css/font-awesome.min.css',
      'vendor/magnific-popup/magnific-popup.css',
      'css/freelancer.min.css',
      'img/profile.png',
      'img/portfolio/cabin.png',
      'img/portfolio/cake.png',
      'img/portfolio/circus.png',
      'img/portfolio/game.png',
      'img/portfolio/safe.png',
      'img/portfolio/submarine.png',
      'vendor/jquery/jquery.min.js',
      'vendor/bootstrap/js/bootstrap.bundle.min.js',
      'vendor/jquery-easing/jquery.easing.min.js',
      'vendor/magnific-popup/jquery.magnific-popup.min.js',
      'js/jqBootstrapValidation.js',
      'js/contact_me.js',
      'js/freelancer.min.js',
      'manup.min.js'
    ];

//Install stage sets up the cache-array to configure pre-cache content
self.addEventListener('install', function(evt) {
  console.log('The service worker is being installed.');
  evt.waitUntil(precache().then(function() {
    console.log('[ServiceWorker] Skip waiting on install');
      return self.skipWaiting();

  })
  );
});


//allow sw to control of current page
self.addEventListener('activate', function(event) {
console.log('[ServiceWorker] Claiming clients for current page');
      return self.clients.claim();

});

self.addEventListener('fetch', function(evt) {
  console.log('The service worker is serving the asset.'+ evt.request.url);
  if(!evt.request.url.includes('https')){
    return;
  }
  evt.respondWith(fromCache(evt.request).catch(fromServer(evt.request)));
  evt.waitUntil(update(evt.request));
});


function precache() {
  return caches.open(CACHE).then(function (cache) {
    return cache.addAll(precacheFiles);
  });
}


function fromCache(request) {
  //we pull files from the cache first thing so we can show them fast
  return caches.open(CACHE).then(function (cache) {
    return cache.match(request).then(function (matching) {
      return matching || Promise.reject('no-match');
    });
  });
}


function update(request) {
  //this is where we call the server to get the newest version of the 
  //file to use the next time we show view
  return caches.open(CACHE).then(function (cache) {
    return fetch(request).then(function (response) {
      return cache.put(request, response);
    });
  });
}

function fromServer(request){
  //this is the fallback if it is not in the cache to go to the server and get it
return fetch(request).then(function(response){ return response})
}

ServiceWorkerによるキャッシュを行うJavaScriptファイルになります。
precacheFilesという配列にキャッシュを行うファイルを手動で記述する必要があります。

PWAとして構築する

WebでDownloadを選択します。

Web App Manifestの設定

  • manifest.jsonとiconのイメージを配置します。
  • manifest.jsonを読み込みます。
<link rel="manifest" href="manifest.json"></link>
  • Web App Manifestのpolyfillを適用します。
<script src="manup.js"></script>

ファイルの場所
https://github.com/boyofgreen/manUp.js/

Service Workerの設定

  • pwabuilder-sw-register.jsを読み込みます。
<script src="pwabuilder-sw-register.js"></script>

結果

Android端末にインストールしてみました。
オフライン時でも表示ができています。

f:id:hi1280:20180618214555g:plain