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の設定
<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端末にインストールしてみました。
オフライン時でも表示ができています。