Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

Docker で最速で nginxサーバーを立てる

サンプルリポジトリ

記事で紹介した内容をリポジトリにあげてあります。よろしければ参考にしてください。

github.com

nginx イメージを使う

たったこれだけでカレントディレクトリのpublicフォルダを公開するウェブサーバーを建てられます。

$ docker run -d -p 8080:80 -v $PWD/public:/usr/share/nginx/html:ro nginx

しかし、単にサーバーを立てるだけならわざわざ docker を使わずにもっと短くできます((python -m http.server 8080 ./public で代用できます))。せっかく Nginx を使うのですから、設定ファイルを使いたいですよね。設定ファイルをマウントするには次のようにします。

$ docker run -d -p 8080:80 -v $PWD/public:/usr/share/nginx/html:ro \
  -v $PWD/nginx.conf:/etc/nginx/nginx.conf:ro \
  -v $PWD/conf.d:/etc/nginx/conf.d:ro nginx

しかし、これだけでは立ち上がる Nginx の prefix が /etc/nginx になってしまうので、prefix を /usr/share/nginx にして起動します。

$ docker run -d -p 8080:80 -v $PWD/public:/usr/share/nginx/html:ro \
  -v $PWD/nginx.conf:/etc/nginx/nginx.conf:ro \
  -v $PWD/conf.d:/etc/nginx/conf.d:ro nginx nginx -p /usr/share/nginx -g 'daemon off;'

さらに --restart=always を付ければ再起動後も自動で立ち上がるようになります。

docker-compose を使う

上のコマンドを docker-compose.yml で書くと次のようになります。

version: '3'
services:
  web:
    image: nginx:alpine
    ports:
      - 8080:80
    volumes:
      - ./public:/usr/share/nginx/html:ro
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./conf.d:/etc/nginx/conf.d:ro
    command: nginx -p /usr/share/nginx -g 'daemon off;'

これなら restart: always をつけることで再起動後も自動で立ち上がるようにできます。

あとは docker-compose.yml と同階層でdocker-compose up -dを実行するとサーバーが立ち上がります。

リバースプロキシ

ネットワークをたててその中で完結させたい場合は以下のようにすると簡潔です。

$ docker create network -d bridge my-network
$ docker run -d -p 8080:80 -v $PWD/public:/usr/share/nginx/html:ro \
  --net my-network \
  -v $PWD/nginx.conf:/etc/nginx/nginx.conf:ro \
  -v $PWD/conf.d:/etc/nginx/conf.d:ro nginx \
  nginx -p /usr/share/nginx -g 'daemon off;'

docker-compose.yml を使うと以下になります。

version: '3'
services:
  web:
    image: nginx:alpine
    ports:
      - 8080:80
    volumes:
      - ./public:/usr/share/nginx/html:ro
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./conf.d:/etc/nginx/conf.d:ro
    command: nginx -p /usr/share/nginx -g 'daemon off;'

networks:
  default:
    external:
      name: my-network

external で指定してあるのは、このコンテナでネットワークを立ててしまうと、このコンテナを起点に依存関係ができてしまうためです。ネットワークを作るかわりに external で既存のネットワークを使えば、このコンテナは他のコンテナの起動状況に関わらず立ち上げることができます。また、他のコンテナもこのコンテナの起動状況に依存せずに済みます。

他のコンテナは、外部とはこのリバースプロキシを介して通信できるので、portsの代わりにexposeでポートをネットワーク内に公開するだけで良いです。

auth_requestをするときは変数に注意

3 時間くらいハマったのでメモしておきます。

ハマった状況

以下のようなコードを書いたら、変数が上書きされてうまくいきませんでした。

location /private {
    auth_request /auth/is_logged_in;

    resolver 127.0.0.11;
    set $upstream_server private;
    proxy_pass http://$upstream_server;
}
location = /auth/is_logged_in {
    resolver 127.0.0.11;
    set $upstream_server auth_server;
    proxy_pass http://$upstream_server;
    internal;
}

上の設定では、/private の proxy_pass が/auth/is_logged_in のステータスがいくつかに関わらず常にhttp://privateではなくhttp://auth_serverとなります。なんということでしょう…Nginx の変数の仕様は複雑怪奇ですね。

解決策

そもそも同じ変数名にしなければ良いです。

location /private {
    auth_request /auth/is_logged_in;

    resolver 127.0.0.11;
    set $private_server private;
    proxy_pass http://$private_server;
}
location = /auth/is_logged_in {
    resolver 127.0.0.11;
    set $auth_server auth_server;
    proxy_pass http://$auth_server;
    internal;
}

定数を定義できればこんな問題も起きなかったはずです。定数がほしいですね。

というか、評価順が直感的でなくて厄介です。動作から推察するに、おそらく次のような評価順です。

  1. /private をすべて評価
  2. $upstream_server が private になる
  3. /auth/is_logged_in を評価
  4. $upstream_server が auth_server になる
  5. ログインが確認できたら/private の proxy_pass を見てリバースプロキシ

直感的には「auth_requestが評価されたらただちにログイン判定」になりそうなものですが。なんじゃこりゃ…

dependencies と devDependencies の使い分け

混乱しがちだったのでまとめました。

なぜ使い分けたいのか?

ウェブアプリケーション開発においては、本番環境で不要なパッケージをインストールせずに済むからです。npm には dependencies に書かれたパッケージのみインストールする機能があるので、これを使ってインストール時間を短くできます。

また、本番環境に不要なものを置くのはセキュリティの観点から良くありません。

ライブラリ開発の場合は異なる

ライブラリ開発ではウェブアプリケーション開発と比べて、devDependencies の意味合いがやや異なります。devDependencies には、配布パッケージに含めるべきでないパッケージを書きます。ウェブアプリケーション開発ではビルド時間が縮むくらいの違いでした。しかし、ライブラリ開発では配布パッケージに含まれるか否かに関わるので、使い分けがとても重要です。

開発環境でしか使わないものは devDependencies

大抵、本番環境ではリポジトリをクローンしてきてビルドをするだけです。そのため、ESLint や Prettier は本番環境で使わないものは devDependencies に書きます。反対に、本番環境でも使いたいものは dependencies に書きます。

  • dependencies に書くパッケージ
    • ビルドに必要なパッケージ(webpack、TypeScript、Babelなど)
    • 使用するライブラリ・フレームワーク(React や Vue)
  • devDependencies に書くパッケージ
    • Linter・Formatter(ESLint、Prettier 関連のパッケージ)
    • テストフレームワーク(Jest など)

ただし、TypeScript には注意してください。TypeScript は build でも test でも使います。そのため、TypeScript関連のパッケージは一概に dependencies、devDependencies のどちらに含まれるか断定できません。一例をあげます。

  • @types/jset: devDependencies(テストでしか使わないため)
  • @types/react: dependencies(ビルド時に使うため)
  • typescript: dependencies(ビルド時に使うため)

さらに: 開発環境でデプロイする場合

実は、わざわざ本番環境でクローン&ビルドせずとも、開発環境でビルドしたものをそのまま本番環境へデプロイできます*1。この場合、そもそも本番環境でパッケージのインストールをしないので、そこまで使い分けを意識する必要はありません。

ただし、CI/CDをしている場合は注意が必要です。この場合は結局、本番環境でクローン&ビルドしているのと変わりません。そのため、上で紹介したように dependencies と devDependencies を使い分ける必要があります。

ライブラリ開発について

ライブラリ開発の場合は多少異なりますが、考え方としてはほぼ同じです。

  • devDependencies: 開発時に必要なもの(ビルドツール含む)
  • dependencies: publish 後に必要なもの(require しているパッケージ)

ビルド関連ツールも devDependencies に含まれる点が異なっています。

まとめ

まず、ウェブアプリケーション開発とライブラリ開発に大別されます。

ウェブアプリケーション開発

  • 開発環境でデプロイする場合:
    • 特に必要なし
  • CircleCI等でデプロイする場合、本番環境でクローン&ビルドする場合:
    dependencies
    ビルド時に使うもの
    devDependencies
    テスト時に使うもの、linter、formatter

ライブラリ開発

dependencies
配布物に含めるべきもの(require するパッケージなど)
devDependencies
開発時に使うもの(ビルド、テスト、linter、formatter)

*1:というより、本番環境でビルドせずに済むので、できることなら開発環境でデプロイできるようにするべきです

Defx.nvim でプレビューウィンドウを自動的に閉じる

Defx.nvim には floating window を使ったイカしたプレビュー機能があります。しかし、何も設定をしていないと、defx を閉じたあともプレビューウィンドウは残ります。この記事は autocmd を活用してプレビューをうまく消そうという記事です。

Defx を閉じたタイミングを取得する

Defx.nvim のウィンドウを閉じると同時にプレビューウィンドウを閉じるには、ウィンドウが閉じたことを検知する必要があります。ウィンドウが閉じたときに発火するイベントはいくつかあります。

  • BufLeave
  • BufHidden
  • BufEnter(ほかのウィンドウに入った=前のウィンドウを離れたとみなせる)

Defx は独自に DefxClosed のようなイベントを発火しないので、これらを使わなければなりません。今回はBufHidden を使います。

BufHidden が発火したときに pclose (プレビューウィンドウを閉じるコマンド)を実行すればやりたいことが実現できます。

(BufLeave ではプレビューウィンドウを開いたタイミングにも発火するのでうまくいきません。また、BufEnter は defx のウィンドウが閉じられたのかを検知できません)

autocmd BufHidden \[defx\]* pclose

これで defx#do_action('quit') などで閉じればプレビューウィンドウも消えます。

ついでに: Gitのプロジェクトルートで defx を開く

あまりに記事のボリュームが少ないので、ついでに全く関係ない Tips をいくつか盛っておきます。

Defx はデフォルトでは現在のディレクトリを表示します。しかし、プロジェクトルート(.gitなどのある位置)で開いたほうが便利ですよね?というわけで、 DefxProjectFile() という関数を作りました。といってもほとんど fzf のリポジトリにある ProjectFiles を参考に書いただけですが。

nnoremap <silent> ,f :Defx `system('git rev-parse --show-toplevel 2> /dev/null')[:-2]` -search=`expand('%:p')`<CR>

ウィンドウが defx のみになったら vim を終了

quit をしてもファイラーウィンドウが残ってしまうのが嬉しいことはほとんどないので、ファイラーウィンドウのみになったら quit を実行するようにしてみました。

function! s:previewWindowOpened() abort
    for nr in range(1, winnr('$'))
        if getwinvar(nr, "&pvw") == 1
            return 1
        endif
    endfor
    return 0
endfunction

autocmd WinEnter \[defx\]* if winnr('$') == 1 || winnr('$') == 2 && <SID>previewWindowOpened() | quit | endif

WinEnter が発火したとき、 defx のウィンドウのみ、あるいは defx ウィンドウとプレビューウィンドウのみだったら終了します。

previewWindowOpened関数はこちらを参考にして、一部動かなかった箇所を修正しています。

.eslintrcを別フォルダに移す

ルートディレクトリにコンフィグファイルがちらばっていると気になりませんか?僕はとても気になります。そこで、設定ファイルを別ディレクトリへ移す方法をご紹介します。

package.jsonに追記するだけで良い

たとえば.config/eslintrc.jsへ移動させたとします。そしたら、以下をpackage.jsonへ記述します。

{
  ...
  "eslintConfig": {
    "extends": "./.config/eslintrc.js"
  },
  ...
}

基本的にはこれだけでOKです。

原理

extendsをつかってファイルを指定すると、そのファイルに書かれた設定が読み込まれます。今回でいえば.config/eslintrc.jsを使うように設定してあるので、これまでと同様に扱うことができます。

上の設定はエディタのプラグインともうまく協調します。また、すでに書いたnpm-scriptsのeslint部分をいじる必要もありません。