diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index cd274679..955a1354 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -40,7 +40,7 @@ You can use the Docker image like below:
```
# SQLite3
-$ docker run -it --rm -p 8080:8080 -v `PWD`:/app -w /app optuna-dashboard sqlite:///db.sqlite3
+$ docker run -it --rm -p 8080:8080 -v `PWD`:/app -w /app sqlite:///db.sqlite3
```
### Running dashboard server
diff --git a/Dockerfile b/Dockerfile
index 392e1dd9..bc5b4150 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -15,7 +15,7 @@ FROM python:3.8-buster AS python-builder
WORKDIR /usr/src
RUN pip install --upgrade pip setuptools
-RUN pip install --progress-bar off PyMySQL[rsa] psycopg2-binary
+RUN pip install --progress-bar off PyMySQL[rsa] psycopg2-binary gunicorn
ADD ./setup.cfg /usr/src/setup.cfg
ADD ./setup.py /usr/src/setup.py
@@ -32,5 +32,5 @@ RUN mkdir /app
WORKDIR /app
EXPOSE 8080
-ENTRYPOINT ["optuna-dashboard", "--port", "8080", "--host", "0.0.0.0"]
+ENTRYPOINT ["optuna-dashboard", "--port", "8080", "--host", "0.0.0.0", "--server", "gunicorn"]
diff --git a/README.md b/README.md
index 8f4865cf..d68e9556 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,23 @@ Listening on http://localhost:8080/
Hit Ctrl-C to quit.
```
+
+
+Running optuna-dashboard with Gunicorn
+
+optuna-dashboard uses [wsgiref](https://docs.python.org/3/library/wsgiref.html) module
+which is provided as a Python standard library. But it has not been reviewed for security
+issues, so not suitable for the production use. You can run optuna-dashboard with Gunicorn
+more secure and/or more fast.
+
+```console
+$ pip install gunicorn
+$ optuna-dashboard sqlite:///db.sqlite3 --server gunicorn
+```
+
+
+
+
More command line options
@@ -43,15 +60,32 @@ positional arguments:
storage DB URL (e.g. sqlite:///example.db)
optional arguments:
- -h, --help show this help message and exit
- --port PORT port number (default: 8080)
- --host HOST hostname (default: 127.0.0.1)
- --version, -v show program's version number and exit
- --quiet, -q quiet
+ -h, --help show this help message and exit
+ --port PORT port number (default: 8080)
+ --host HOST hostname (default: 127.0.0.1)
+ --server {wsgiref,gunicorn}
+ server (default: wsgiref)
+ --version, -v show program's version number and exit
+ --quiet, -q quiet
```
+
+
+Python Interface
+
+**`run_server(storage: Union[str, BaseStorage], host: str = 'localhost', port: int = 8080) -> NoReturn`**
+
+Start running optuna-dashboard and blocks until the server terminates.
+This function uses wsgiref module which is not intended for the production use.
+
+**`wsgi(storage: Union[str, BaseStorage]) -> WSGIApplication`**
+
+This function exposes WSGI interface for people who want to run on the
+production-class WSGI servers like Gunicorn or uWSGI.
+
+
## Using an official Docker image
@@ -102,20 +136,6 @@ You can walk-through trials by filtering and sorting.

-## Python Interface
-
-### `run_server(storage: Union[str, BaseStorage], host: str = 'localhost', port: int = 8080) -> NoReturn`
-
-Start running optuna-dashboard and blocks until the server terminates.
-This function uses wsgiref module which is not intended for the production
-use. If you want to run optuna-dashboard more secure and/or more fast,
-please use WSGI server like Gunicorn or uWSGI via `wsgi()` function.
-
-### `wsgi(storage: Union[str, BaseStorage]) -> WSGIApplication`
-
-This function exposes WSGI interface for people who want to run on the
-production-class WSGI servers like Gunicorn or uWSGI.
-
## Submitting patches
If you want to contribute, please check [Developers Guide](./DEVELOPMENT.md).
diff --git a/optuna_dashboard/cli.py b/optuna_dashboard/cli.py
index 146b3218..87f43dfe 100644
--- a/optuna_dashboard/cli.py
+++ b/optuna_dashboard/cli.py
@@ -1,6 +1,8 @@
import argparse
import os
+from typing import NoReturn
+from bottle import Bottle
from bottle import run
from optuna.storages import BaseStorage
from optuna.storages import RDBStorage
@@ -11,9 +13,38 @@ from .app import create_app
AUTO_RELOAD = os.environ.get("OPTUNA_DASHBOARD_AUTO_RELOAD") == "1"
+SERVER_CHOICES = ["wsgiref", "gunicorn"]
-def main() -> None:
+def run_wsgiref(app: Bottle, host: str, port: int, quiet: bool) -> NoReturn: # type: ignore
+ run(
+ app,
+ host=host,
+ port=port,
+ server="wsgiref",
+ quiet=quiet,
+ reloader=AUTO_RELOAD,
+ )
+
+
+def run_gunicorn(app: Bottle, host: str, port: int, quiet: bool) -> NoReturn: # type: ignore
+ # See https://docs.gunicorn.org/en/latest/custom.html
+
+ from gunicorn.app.base import BaseApplication
+
+ class _Application(BaseApplication):
+ def load_config(self) -> None:
+ self.cfg.set("bind", f"{host}:{port}")
+ if quiet:
+ self.cfg.set("loglevel", "error")
+
+ def load(self) -> Bottle:
+ return app
+
+ _Application().run()
+
+
+def main() -> NoReturn:
parser = argparse.ArgumentParser(description="Real-time dashboard for Optuna.")
parser.add_argument("storage", help="DB URL (e.g. sqlite:///example.db)", type=str)
parser.add_argument(
@@ -22,6 +53,12 @@ def main() -> None:
parser.add_argument(
"--host", help="hostname (default: %(default)s)", default="127.0.0.1"
)
+ parser.add_argument(
+ "--server",
+ help="server (default: %(default)s)",
+ default="wsgiref",
+ choices=SERVER_CHOICES,
+ )
parser.add_argument("--version", "-v", action="version", version=__version__)
parser.add_argument("--quiet", "-q", help="quiet", action="store_true")
args = parser.parse_args()
@@ -33,7 +70,12 @@ def main() -> None:
storage = RDBStorage(args.storage)
app = create_app(storage)
- run(app, host=args.host, port=args.port, quiet=args.quiet, reloader=AUTO_RELOAD)
+ if args.server == "wsgiref":
+ run_wsgiref(app, args.host, args.port, args.quiet)
+ elif args.server == "gunicorn":
+ run_gunicorn(app, args.host, args.port, args.quiet)
+ else:
+ raise Exception("must not reach here")
if __name__ == "__main__":