Dash by Plot.ly を nginx proxy の背後に置く

python 的世界でグラフを描くとすれば、Matplotlibbokeh を使ってきたが、最近では dash_by_plotly も侮れない。

dash app を nginx の reverse proxy の背後に置く場合の設定で 個人的に手こずったのでメモをしておく。

まず何か適当な dash app を作る。

dash application.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State

app = dash.Dash(__name__)
#app.config.update({
#  'routes_pathname_prefix': '/',
#  'requests_pathname_prefix': '/myapp/'})
app.layout = html.Div([
  html.H1('Hello, Dash World.'),
  dcc.RadioItems(
    id='radio_button',
    options=[{'label': 'ON', 'value': 'ON'},
             {'label': 'OFF', 'value': 'OFF'}],
    value='OFF'
  ),
  html.Div(id='radio_meter'),
])

@app.callback(
  Output('radio_meter', 'children'),
  [Input('radio_button', 'value')]
)
def update_radio_meter(value):
  return value

if __name__ == '__main__':
  app.run_server(debug=True, host='127.0.0.1', port=8050)

これを走らせると、 http://127.0.0.1:8050/ に下図のようなアプリが動く。 ON/OFF のラジオボタンを操作するとその下の行のON/OFFの表示が追随するだけの 簡単なものである。

dash app

これを nginx の reverse proxy に収容するためには、コメントアウトされている app.config.update() の行(7-9行目)を動作させた状態で、 下のような proxy 設定と組み合わせれば良い。

nginx.conf
1
2
3
4
5
6
7
8
9
location /myapp {
  return 302 /myapp/;
}
location /myapp/ {
  proxy_pass http://127.0.0.1:8050/;
  proxy_set_header Host      $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

このnginxで提供しているサイトが https://example.jp/ だとすれば、 https://example.jp/myapp/ にdash appがdeployされるわけだが、 proxy された後に http://127.0.0.1:8050/ に届く request には /myapp/ の path がないとか、(proxy_redirect で調整できるところはともかく)response での /myapp/の有無の調整をしなければまともに動かない。 それをやっているのが routes_pathname_prefix と requests_pathname_prefix ということになる。

この他、app = dash.Dash(__name__, url_base_pathname=’/myapp/’) とする方法も 紹介されているけれども、ぼくの手元では動かなかった。

追記 2018/Oct/12.

url_base_pathname でやる場合はこうするみたい。

nginx.conf for dash app with url_base_pathname.
1
2
3
4
5
6
7
8
9
location /myapp {
  return 302 /myapp/;
}
location /myapp/ {
  proxy_pass http://127.0.0.1:8050/myapp/;    # '/myapp/' 追加
  proxy_set_header Host      $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
dash app invoked with url_base_pathname.
1
2
3
4
app = dash.Dash(__name__, url_base_pathname='/myapp/')   # url_base_pathname 追加
#app.config.update({                                     # app.config.update不要
#  'routes_pathname_prefix': '/',
#  'requests_pathname_prefix': '/myapp/'})

さらに、url_base_pathname を使う場合に、複数ページを切り替えるような dash application はこんな感じでいけそう。(Multi-Page Apps and URL Support 参照)

./app.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import dash
import dash_core_components as dcc
import dash_html_components as html

UBPATH = '/myapp/'

app = dash.Dash(__name__, url_base_pathname=UBPATH)
app.config.suppress_callback_exceptions = True

toc = html.Div([
  dcc.Link('root', href=UBPATH),
  html.Br(),
  dcc.Link('app1', href=UBPATH+'apps/app1'),
  html.Br(),
  dcc.Link('app2', href=UBPATH+'apps/app2'),
])
./index.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Output, Input, State

from app  import app, toc, UBPATH
from apps import app1, app2

app.layout = html.Div([
  dcc.Location(id='url', refresh=False),
  html.Div(id='page-content')
])

@app.callback(Output('page-content', 'children'),
              [Input('url', 'pathname')])
def display_page(pathname):
  if pathname == UBPATH+'apps/app1':
    return app1.layout
  elif pathname == UBPATH+'apps/app2':
    return app2.layout
  else:
    return html.Div([html.Div(['my applications']), toc])

if __name__ == '__main__':
  app.run_server(debug=True, host='127.0.0.1', port=8050)
./apps/app1.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import dash_core_components as dcc

import dash_html_components as html
from dash.dependencies import Output, Input, State

from app import app, toc

layout = html.Div([
  html.H3('App 1'),
  dcc.Dropdown(
    id='app-1-dropdown',
    options=[{'label': 'App 1 - {}'.format(i), 'value': i} for i in ['Orange', 'Red', 'Blue']],
    value='Red',
  ),
  html.Div(id='app-1-display-value'),
  toc,
])

@app.callback(Output('app-1-display-value', 'children'),
              [Input('app-1-dropdown', 'value')])
def display_value(value):
  return 'You have selected "{}"'.format(value)
./apps/app2.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import dash_core_components as dcc

import dash_html_components as html
from dash.dependencies import Output, Input, State

from app import app, toc

layout = html.Div([
  html.H3('App 2'),
  dcc.Dropdown(
    id='app-2-dropdown',
    options=[{'label': 'App 2 - {}'.format(i), 'value': i} for i in ['Orange', 'Red', 'Blue']],
    value='Red',
  ),
  html.Div(id='app-2-display-value'),
  toc,
])

@app.callback(Output('app-2-display-value', 'children'),
              [Input('app-2-dropdown', 'value')])
def display_value(value):
  return 'You have selected "{}"'.format(value)

まあ、その、せっかく app.server に flask のサーバがあるのだから、 URL 変更に追随するのは @app.server.route(‘myapp/apps/app1’) とかで やりたいけど、今はちょっと無理っぽい。 @app.callbackとの組合せがうまく分離できない気がする。 こうすればできるってのがあれば教えてください。m(_._)k

備考

まあ、ちょっとその、もう少し網羅的なマニュアルがあるとありがたいんだけどね。 https://dash.plot.ly の tutorial をこなしたら、あとは Forum (https://community.plot.ly/c/dash) くらいだと思います。 でも、bokeh app よりは楽な気がするので、ぼくはしばらくこっちで頑張ります。

2018/Oct/10頃書いた。