Vue アプリケーションを Docker コンテナにして外部に公開する


雑なので細かい部分は端折ったり説明を端折ったりしてます。よって不正確な部分がありますがご了承ください(定型文)。

[→ ご了承する] [やだ]

以下本編。

前書き

やること

  • Vue アプリケーションを Vue CLI でビルドする(楽なので)
  • ビルドは Docker コンテナ内部でやる(環境汚染防止)
  • コンテナのイメージは GitHub + Docker Hub でやる(CI/CD)
  • 内部では Docker のコンテナで動く fastify を動かす
  • 外部にはホストで直接動く nginx で公開する(リバースプロキシ)

できること

  • GitHub にタグ付きでプッシュすれば勝手にコンテナイメージができあがる
  • どうにかしてコンテナを更新すれば(コマンドだけで済む)中身も更新できる
  • つまり、コードだけ書けば後は定形作業で済むようにできる。

やらないこと

  • GitHub にプッシュするとコンテナも自動更新されるようにする(やり方がわからない)
  • リバースプロキシもコンテナにする(コンテナ内の certbot 知見がない)
  • この記事をわかりやすく書く(無理)

Vue アプリを準備する

$ yarn global add @vue/cli@next # @next しないと Vue3 入らない??
$ vue create <project name>     # フォルダ先作ってその中で実行すると面倒だよね

してよしなにする。 Vue2 でもいいならややこしいことはしないで済むけど、いずれにせよあまり関係はない。Vue2でもVue3でもビルドできればなんでもいい。

とりあえずビルドして静的ファイルを書き出す

$ cd <project name> # つまりプロジェクトフォルダ内に行くならなんだっていい
$ yarn build        # 多分 dist/ に書き出されると思うべ

してよしなにする。
中身はどうでもよくてとにかくウェブサーバで開けるようなファイルが出揃えばいい。

ビルドファイルを外部に流せるようにする

dist/ 内を公開するには色々方法がある。nginx するとか express するとか。
ここでは TypeScript で動く fastify を使いまーす!!!

$ yarn add -D fastify fastify-static typescript ts-node

TS 関連でエラー出て解決できなかったら、どうにかして以下のファイルを .js にするんだ。
なお、このコードはfastify のドキュメントにあるサンプルコードを適量改変したものである。

import * as path from 'path'

import fastify from 'fastify'
import fastify_static from 'fastify-static'

const server = fastify()

server.register(fastify_static, {
    root: path.join(__dirname, 'dist')
})

server.listen(8080, '0.0.0.0', (error, address) => {
    if (error) {
        console.error(error)
        process.exit(1)
    }

    console.log(`Server listening at ${address}`);
})

説明は長くなるので端折るけど、dist/ に吐かれたぽっと出のファイルをポート8080で外部に吐き出すようにする。まあこいつを nginx でプロキシしてやって外部の HTTPS/443 番を仲介してやるわけだ。

あと真面目な知見があり、

  1. Vue CLI で生成される tsconfig.jsonmoduleesnext だと死ぬ。なので commonjs にする。こうすると node 風に色々やってくれて成功がカモンする。書き換えてもビルド自体は通ったので深く考え込まないことにした。
  2. ↑ の TS fastify コードの 0.0.0.0 を書かないと localhost だか 127.0.0.1 になる。これだとコンテナ内部でリッスンする場合うまくいかないっぽい。でも 0.0.0.0 だとセキュリティ問題があるらしく不吉な気がする。気にしないことにした。

そしたら、こうする。

# 1. ファイル名はご自由に
# 2. package.json の scripts に書いとくと便利だと思うよ
# 3. ところで ts-node は TypeScript を直接実行できる node ラッパでございます
$ yarn ts-node server.ts

ここまでやると、とりあえずローカルで http://localhost:8080 なりで Vue アプリを開けるはず。
開けなかったら何らかが間違っている。気合で。

Dockerfile 作る

# Alpine Linux だとイメージが軽い
FROM node:14-alpine

# 作業フォルダ
WORKDIR /azure

# /node_modules をコンテナ内で完結させる
COPY package.json yarn.lock ./
RUN yarn install

# ソースコードをコンテナの中に引っ張ってビルド
COPY . ./
RUN yarn build

# 外部に fastify の 8080 番を公開する
# TODO: これいる?
EXPOSE 8080

# ts-node server.ts => yarn start
CMD ["yarn", "start"]

# TODO: /node_modules はもういらないので、消せばイメージ容量を削減できる
# NOTE: ちなみにイメージ容量は 500 MiB ぐらいだった

こういうのを作って docker build -t app . なりでイメージをビルドしてみる。
ビルドできればいいんだ。出来なかったら頑張って……。

GitHub + Docker Hub する

GitHub も Docker Hub も登録処理は端折るし、連携も端折る。
どちらも難しくはないはずなので、勢いとアイデアロールでクリティカルを出す。

方針としては:

  • Docker Hub にイメージ用のリポジトリを作る
  • ソースコードは GitHub のリポジトリから横流しするようにする
  • master ブランチにプッシュないしタグの更新でイメージを作るようにする

まあ、細かいとこは頑張ってもらいまして、イメージのビルド設定で:

  • BUILD RULES を足す(わからなくて一敗)
  • Source Type = Tag
  • Source = /^[0-9.]+$/
  • Tag = {sourceref}

あたりでいかがですか。ちなみに master のほうは同じことをブランチ版ですればいいので端折る。
後は適当にビルドを待って(遅い)成功すればいいんだ。出来なかったらサイゼリア行くといいと思う。

Docker でコンテナを動かす

$ docker run \
  --name <container name>              # コンテナ名
  -p 8080:8080 \                       # コンテナ内の8080番をホストの8080番にする
  -d <username>/<image name>:<version> # よしなに

docker ps とかして動いてるっぽいならいいんだ。
なんならダメ押しで curl http://localhost:8080 とかいかがですか。
要は今まで OS の上で直接動いてたんが Docker かまして動いてるだけなのでまあ。

ウェブサーバ公開 feat. nginx

うまくやれれば、うまくいく。

Let's Encrypt で「暗号化しよう」する

余談。 Let's Encrypt 滅茶苦茶打ちづらいので別の名前にしてほしい。
特に ' が地獄で Shift 押さなきゃとか地獄すぎる。ご勘弁願いたい。

以下はcertbot の取説からの抜粋なので適当に流し読みで……

# Ubuntu focal fossa 観
$ sudo snap install --classic certbot # apt でもいいんかな?
$ sudo ln -s /snap/bin/certbot /usr/bin/certbot
$ sudo certbot --nginx

取説を見るに、ネット上に転がっている情報とは違ってこのやり方なら自動で更新作業が入るように見える。systemctl list-timers したら確かにそれらしい定期ジョブが入っていた。

心配なら sudo crontab -e して certbot renew が実行されるようにする。

nginx の設定

説明略。以下のことを実現する:

  • HTTP できたら HTTPS にリダイレクトする
  • Let's Encrypt で HTTPS
  • nginx が 80/443 番で待ち受けてリクエストきたら Docker の 8080 番に横流し
$ sudo vim /etc/nginx/sites-enabled/default
server {
  listen 80;

  if ( $http_x_forwarded_proto != 'https' ) {
    return 301 https://$host$request_uri;
  }
}

server {
  listen 80 default_server;
  listen [::]:80 default_server;

  listen 443 ssl default_server;
  listen [::]:443 ssl default_server;

  # パスは読み替えて
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  # ここも読み替えてね
  server_name www.example.com

  proxy_set_header Host               $host;
  proxy_set_header X-Real-IP          $remote_addr;
  proxy_set_header X-Forwarded-Host   $host;
  proxy_set_header X-Forwarded-Server $host;
  proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;

  location / {
    proxy_pass http://localhost:8080/;
  }
}
$ sudo nginx -t # 設定ファイルのテスト
$ sudo systemctl restart nginx

DNS 設定

私の場合 Cloudflare で雑にやったけど、何となく自鯖に宛てた A レコードを立てたりすればいいのではないか。どうでもいいけど固定 IP が取れない場合は ddclient とかの DDNS クライアントを cron で回してうまくやる手法がある。 ddclient は Cloudflare で DDNS できる。

~終~

curl https://example.com とかしてみて見えればいいんじゃないか?
見えなかったらこの記事を読み直してみるか、それでもわからなかったら調べてもらって……

終わりに。

普通に GitHub リポジトリを設定すれば nginx な鯖のてけとーなフォルダで git cloneyarn ts-node server.ts すれば Docker 部分を全部すっとばせる。

ここで Docker せずに単純に git pullyarn buildyarn start すれば更新作業も Docker でいらなくなる。しかしビルドをデプロイ先でやるのは気持ち悪いし、負荷もある。後 node_modules だか yarn だかがグローバル環境に生えてくるので環境汚染が加速して SDGs とか地球によくない。

ゆえ、Docker は非常によろしい。ライブラリ層より上を全部コンテナの中に押し込める。グローバル環境で考えることはない。Docker Engine さえ動けば最下層のホスト OS はなんでもいい。最高だ。