Mono Docker で ASP.NET MVC (.NET Framework 4.7.2)

# これはなに？
.NET Framework 4.7.2 でビルドしたASP.NET MVC アプリを、Linux Dockerで動かそうと試した備忘録です。

# 作戦
+ Monoのイメージは[公式のもの](https://hub.docker.com/_/mono/)を利用
+ ASP.NETアプリは fastcgi-mono-server4 で動かす
+ Nginxでホスティングし、fastcgi-mono-server4 とは unix domain socketで通信させる
+ fastcgi-mono-server4 は Supervisorでサービス起動する
+ NginxとMono(fastcgi-mono-server4)のコンテナは分ける
+ Monoのバージョンはlatestではなく、5.18.1.28で検証
+ データベースは今は考えない

##### 完成した docker-compose 一式は[こちら（Github）](https://github.com/kirishu/mono-docker)

# サンプルアプリ
まずは VisualStudio でサンプルアプリを作ります。
ASP.NET MVCプロジェクトの雛形に用意されているサンプルにちょっとページを追加して、環境変数などを表示するようにしました。
![aspinfo_on_vs.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177379/826032d4-b788-a395-a824-81b8cc7a903d.png)
（どっかで見たことのあるデザインですが・・・）
このアプリを「発行」して、一式をLinuxサーバに転送します。


# Mono Dockerイメージの調査
公式のDockerイメージがナンボのもんか知るためにシェルに入って調査します。
※サンプルアプリは、/home/mono-docker/app/aspinfo に配置しています

```bash:ホスト
# docker run -v /home/mono-docker/app:/home/monoapp -p 9000:9000 -i -t mono:5.18.1.28 /bin/bash
```

## xsp4
MonoのWebサーバ （xsp と fastcgi-mono-server）は含まれていないようなので、インストールします。
ついでにSupervisor と vim も入れます。

```bash:コンテナ
# apt update
# apt -y install mono-xsp4 mono-fastcgi-server4 supervisor vim
```


簡易サーバ xsp を使ってアプリの動作を確認します。

```bash:コンテナ
# cd /home/monoapp/aspinfo
# xsp4
xsp4
Listening on address: 0.0.0.0
Root directory: /home/app/aspinfo
Listening on port: 9000 (non-secure)
Hit Return to stop the server.
```

この状態でLAN内のWebブラウザから `http://Linuxホスト:9000` を叩いてページが出ることを確認します。

## Supervisor
次にSupervisorの設定と起動の確認を行います。
実はここで大ハマリしました。

Nginxとunix socket通信を行うには、お互いの実行ユーザがsocketに対して書き込み権限がある必要があります。
普通にSupervisorからfastcgiを起動すると、root所有の755でsocketが作成されるため、Nginxが通信できなくなります。
socketファイルは、DockerfileのCMDで Supervisorを起動したあとに作成されるため、後からパーミッションを変更することもできません。
PHP-FPMのように、socketファイルのパーミッションを指定できればよいのですが、fastcgi-mono-serverにはそんな機能が無いのです。

仕方ないので、socketが作られるディレクトリを777で作成し、Nginxの実行ユーザと同じIdを持つユーザを作成し、fastcgi-mono-server はこのユーザで起動するようにします。
※Nginxのコンテナはいじりたくないので、Mono側がNginxのユーザに合わせるようにしています

Nginx の Docker は、[ユーザid が "101"の"nginx"ユーザで実行されるようです](https://hub.docker.com/_/nginx)。

> Since 1.17.0, both alpine- and debian-based images variants use the same user and group ids to drop the privileges for worker processes:
> 
> $ id
> uid=101(nginx) gid=101(nginx) groups=101(nginx)

なのでこれと同じユーザを作ります。

```shell:コンテナ
# useradd -u 101 -U -s /sbin/nologin -d /home/monoapp -m nginx
```

<br />
また、socketファイル用ディレクトリを777で作成します。

```shell:コンテナ
# mkdir -p /var/run/xsp4
# chmod 777 /var/run/xsp4
```

<br />
これを受けて、fastcgi-mono-server 起動用の設定ファイルを作成します。

```config:コンテナ：/etc/supervisor/conf.d/xsp4.conf
[supervisord]
nodaemon=true

[program:xsp4]
directory=/home/monoapp/aspinfo
environment=MONO_IOMAP=all
command=fastcgi-mono-server4 /applications=/:/home/monoapp/aspinfo/ /filename=/var/run/xsp4/mono-xsp4.socket /socket=unix
user=nginx
autostart=true
autorestart=true
```

<br />
Supervisorの試運転

```shell
# supervisord &
[1] 775
/usr/lib/python2.7/dist-packages/supervisor/options.py:298: UserWarning: Supervisord is running as root and it is
 searching for its configuration file in default locations (including its current working directory); you probably
 want to specify a "-c" argument specifying an absolute path to a configuration file for improved security.
  'Supervisord is running as root and it is searching '
2019-08-16 10:17:06,882 CRIT Supervisor running as root (no user in config file)
2019-08-16 10:17:06,884 INFO Included extra file "/etc/supervisor/conf.d/xsp4.conf" during parsing
2019-08-16 10:17:06,916 INFO RPC interface 'supervisor' initialized
2019-08-16 10:17:06,918 CRIT Server 'unix_http_server' running without any HTTP authentication checking
2019-08-16 10:17:06,920 INFO supervisord started with pid 775
2019-08-16 10:17:07,933 INFO spawned: 'xsp4' with pid 778
2019-08-16 10:17:08,942 INFO success: xsp4 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)

# ps axu
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.3  18152  3248 pts/0    Ss   09:29   0:00 /bin/bash
root       775  0.7  1.9  57920 20212 pts/0    S    10:17   0:00 /usr/bin/python /usr/bin/supervisord
nginx      778  0.5  2.8 293076 28932 pts/0    Sl   10:17   0:00 /usr/bin/mono /usr/lib/mono/4.5/fastcgi-mono-server4.ex
root       783  0.0  0.2  36636  2856 pts/0    R+   10:19   0:00 ps axu
```

うまく動きました　:sunny:

# docker-compose

これらを踏まえて、MonoとNginxのコンテナを作成します。
ディレクトリ構成はこんな感じ
![directory.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177379/8ec2668e-debf-e28b-7076-2bcc8d6b365a.png)

[サンプルアプリも含めた一式はこちら](https://github.com/kirishu/mono-docker)

## docker-compose.yml

Nginxは alpine ベースを使ってみました。
イメージのサイズは21.2MBとそこそこ軽量です。(1.17.3時点の値)

```yaml:docker-compose.yml
version: '3'
services:
  mono:
    build: ./mono
    volumes:
      - ./app:/home/monoapp
      - xsp4socket:/var/run/xsp4
  nginx:
    image: nginx:alpine
    ports:
      - 80:80
    depends_on:
      - mono
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
      - ./nginx/fastcgi_params:/etc/nginx/fastcgi_params
      - ./app:/home/monoapp
      - xsp4socket:/var/run/xsp4
volumes:
  xsp4socket:
```

## mono

調査した操作をDockerfileに記述し、イメージを作成します。
Supervisorの設定ファイルをコピーし、CMDで起動します。

```dockerfile:mono/Dockerfile
FROM mono:5.18.1.28

RUN apt update \
    && apt -y install mono-xsp4 mono-fastcgi-server4 supervisor \
    && useradd -u 101 -U -s /sbin/nologin -d /home/monoapp -m nginx \
    && mkdir -p /var/run/xsp4 \
    && chmod 777 /var/run/xsp4 \
    && mkdir -p /etc/mono/registry \
    && chmod 777 /etc/mono/registry

COPY ./xsp4.conf /etc/supervisor/conf.d/

CMD ["/usr/bin/supervisord"]
```

```config:mono/xsp4.conf
[supervisord]
nodaemon=true

[program:xsp4]
directory=/home/monoapp/aspinfo
environment=MONO_IOMAP=all
command=fastcgi-mono-server4 /applications=/:/home/monoapp/aspinfo/ /filename=/var/run/xsp4/mono-xsp4.socket /socket=unix
user=nginx
autostart=true
autorestart=true
```

## Nginx

サイトの設定ファイル。
fastcgi-mono-server と unix domain socket で通信を行います。

```conf:nginx/default.conf
server {
        listen 80;
        root /home/monoapp/aspinfo;
        server_name _;

        location / {
                index index.html index.htm default.aspx Default.aspx;

                fastcgi_pass unix:/var/run/xsp4/mono-xsp4.socket;
                include /etc/nginx/fastcgi_params;
                client_max_body_size 10M;  # upload limit
        }

        # favicon.ico
        location = /favicon.ico {
                log_not_found off;
                access_log off;
        }
}
```

<br />
fastcgi-mono-server用の設定を追記したパラメータファイル

```conf:nginx/fastcgi_params
fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;
fastcgi_param  HTTPS              $https if_not_empty;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;


# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

# for Mono configration
fastcgi_param  PATH_INFO "";
fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
```



# 起動

```shell:ホスト
# docker-compose up -d --build

```

<br />
うまく行けば、サンプリアプリが動きます。
ちゃんと Nginx経由 / unix OS で動いていると確認できます　:sunny:
![aspinfo_on_mono.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177379/5bc8045b-30ee-1fcf-7326-c862f2080da0.png)

# 感想
+ ニッチなソリューションなため、情報が少なくて困りました
+ とっとと.NET Coreに移行したいです