[PYTHON] Publish your website with responder + Gunicorn + Apache

Introduction

There is a relatively new (October 2018-) Python web framework called responder. The author is the one who made requests etc., and it seems that it is a framework like Flask and Falcon's good points. When I thought about creating a simple web page with Python, I found out about responder and was curious about it, so I decided to use it. As far as I can see, there was no explanation using Apache, so I will post my own construction method instead of a memo. (Please let me know if you have any ...)

environment

It seems that Nginx is easier to build, but I decided to make it as it is because it is a server that originally contained Apache.

By the way, only venv is used for Python environment construction tool. After creating an environment with venv, applying the following direnv configuration file to the working directory is convenient because it will be activated at the same time as cd to the working directory.

.envrc


source <Full path of venv activate file>

STEP0: Write a program that returns a response

The goal is to publish and run the following programs globally. For the program itself, go to the official Quick Start.

main.py


import responder

api = responder.API()


@api.route("/{who}")
def greet_world(req, resp, *, who):
    resp.text = f"Hello, {who}!"


if __name__ == '__main__':
    api.run()

In this case, for example, if you access / world with GET, it will be displayed asHello, world!, Or if you access / testtesttest with GET, it will be displayed as Hello, testtest test!.

STEP1: Run on the responder's built-in server

The responder has Uvicorn built-in as a built-in server. First, try starting with responder (+ Uvicorn).

$ python main.py
INFO: Started server process [693]
INFO: Waiting for application startup.
INFO: Uvicorn running on http://127.0.0.1:5042 (Press CTRL+C to quit)
$ curl http://127.0.0.1:5042/world
Hello, world!

You can see that the server starts automatically and can be connected just by executing the program. When you shut down the server, you can use Ctrl + C as written.

STEP2: Move using Gunicorn

Uvicorn official page It seems that it is better to use Gunicorn for the production environment, so let's move it referring to the official settings. (It's been a while since I tried to write this article, so don't worry, the timestamp is a few months ago.)

$ pip install gunicorn

$ gunicorn -k uvicorn.workers.UvicornWorker main:api
[2019-10-31 09:39:11 +0900] [1227] [INFO] Starting gunicorn 19.9.0
[2019-10-31 09:39:11 +0900] [1227] [INFO] Listening at: http://127.0.0.1:8000 (1227)
[2019-10-31 09:39:11 +0900] [1227] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2019-10-31 09:39:11 +0900] [1230] [INFO] Booting worker with pid: 1230
[2019-10-31 09:39:12 +0900] [1230] [INFO] Started server process [1230]
[2019-10-31 09:39:12 +0900] [1230] [INFO] Waiting for application startup.
$ curl http://127.0.0.1:8000/world
Hello, world!

The arguments when launching Gunicorn are roughly as follows.

---k uvicorn.workers.UvicornWorker: Specify Uvicorn as the worker class --main: api: Specify the module to start. The notation is "module name (program name): variable name ofresponder.API ()"

Create a Gunicorn configuration file

Gunicorn configuration items can also be read from the configuration file. It is convenient to have a configuration file later, so create it. The arguments used in the above command are minimal. Create a configuration file by adding the save location of the log file here.

gunicorn.py


import multiprocessing
import os

name = "gunicorn"

accesslog = "<File name to write access log>"
errorlog = "<File name to write the error log>"

bind = "localhost:8000"

worker_class = "uvicorn.workers.UvicornWorker"
workers = multiprocessing.cpu_count() * 2 + 1
worker_connections = 1024
backlog = 2048
max_requests = 5120
timeout = 120
keepalive = 2

user = "www-data"
group = "www-data"

debug = os.environ.get("DEBUG", "false") == "true"
reload = debug
preload_app = False
daemon = False

See Official Docs for each item. The setting value is imitated from that of [Reference site](#Reference site).

To apply this configuration file and launch Gunicorn, use the following command.

$ gunicorn --config gunicorn.py main:api

STEP3: Run Apache as a reverse proxy server

Finally, set up to connect through Apache. Set to proxy to http: // localhost: 8000 where Gunicorn is waiting when you connect to http://example.com/ where Apache is running.

Apache side settings for proxying to Gunicorn

First, create a configuration file for the reverse proxy.

/etc/apache2/conf-available/responder.conf


ProxyRequests Off
ProxyPass "/" "http://localhost:8000/"
ProxyPassReverse "/" "http://localhost:8000/"

Enable the configuration file & proxy related modules.

$ sudo a2enconf responder
$ sudo a2enmod proxy_http proxy

Reload after confirming that the configuration file is described correctly.

$ sudo apache2ctl configtest
Syntax OK

$ sudo systemctl reload apache2ctl

Gunicorn autostart settings

To make it easier to start automatically, set Gunicorn startup to be managed by systemd. First, create a configuration file.

/etc/systemd/system/webapp.service


[Unit]
Description=gunicorn - responder
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=<gunicorn.py and main.Full path of the directory where py is located>
ExecStart=<Full path of Gunicorn> --config <gunicorn.full path of py> main:api
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

I will also explain the items that are difficult to understand.

--File name: .service is fine. An example of using it in a command is systemctl restart <set service name>. -[Service]-User, Group: I'm using Apache, but I'm not sure if this is correct ... You may also want to change the owner of the files under Working Directory. -[Service]-ExecStart: This is the full path of the command in "Create a Gunicorn configuration file" in STEP2.

After creating, set the service to start and start automatically.

$ sudo systemctl start webapp.service
$ sudo systemctl enable webapp.service

Just in case, make sure it works properly.

$ sudo systemctl status webapp.service
● webapp.service - gunicorn - responder
   Loaded: loaded (/etc/systemd/system/webapp.service; enabled; vendor preset: enable
   Active: active (running) 
(Omitted below)

Both startup and automatic startup have been successful.

Complete!

If the settings are correct, you should be able to connect with curl or a browser.

$ curl http://example.com/world
Hello, world!

unsolved? problem

For some reason, the html Content-Type header disappears when passing through Apache. (When you start the server from Gunicorn, it is attached properly. Mysterious phenomenon) As a tentative measure, I'm adding code that forces Content-Type: text / html; charset = UTF-8 when returning a response.

Bonus note

By the way, I would like to write down the grammar of responder, which I personally found useful / googled and hard to find.

How to write routing

There seems to be more routing (and processing) than just the beginning.

Create class+Set up routing collectively later.py


import responder

api = responder.API()


class Who:
	def on_get(self, req, resp, *, who):
		#When GET, this process is done automatically
    	resp.text = f"Hello, {who}!"
    
    async def on_post(self, req, resp, *, who):
    	#At the time of POST, this process is done automatically
    	data = await req.media()
    	resp.text = f"{data}"

#Routing settings
api.add_route("/{who}", Who)

if __name__ == '__main__':
    api.run()

Is the writing style at the beginning Flask style, and the writing style just written Falcon style? I myself wrote in "Description using ʻon_get` etc. in the class + Routing setting with the decorator", but maybe it's a bad idea ...

Add Jinja2 filter

It can be used when you want to define a static file path or write a process that is repeated many times.

jinja_myfilter.py


def css_filter(path):
	return f"./static/css/{path}"


def list_filter(my_list):
	return_text = "<ul>\n"
	for l in my_list:
		return_text += f"<li> {l} </li>\n"
	return_text += "</ul>"
	return return_text

main.py


import responder
import jinja_myfilter

api = responder.API()
#Add filter
# v1.For x
api.jinja_env.filters.update(
	css = jinja_myfilter.css_filter, 
	html_list = jinja_myfilter.list_filter
)
# v2.For x(2020/05/12 Addendum)
# (Because there is an underscore_env seems to be treated as an internal value,
#I couldn't find any other way to specify it by looking at the source ...)
api.templates._env.filters.update(
	css = jinja_myfilter.css_filter, 
	html_list = jinja_myfilter.list_filter
)

@api.route("/")
def greet_world(req, resp):
	param = ["Item 1", "Item 2"]
	resp.content = api.template("index.html", param=param)

if __name__ == '__main__':
	api.run()

index.html


<link rel="stylesheet" type="text/css" href="{{ 'form.css' | css }}">
<!--It is processed by Jinja2 and becomes as follows
  <link rel="stylesheet" type="text/css" href="./static/css/form.css">
-->

{% autoescape false %}
{{ param | html_list }}
{% endautoescape %}
<!--It is processed by Jinja2 and becomes as follows
  <ul>
  <li>Item 1</li>
  <li>Item 2</li>
  </ul>
-->

If a character string containing an html tag is returned, automatic escaping will work unless it is enclosed in {% autoescape false%} to {% endautoescape%}. However, if the parameter you are passing is user-input, the parameter will be output without being escaped, so be careful. Is it safe to process it using html.escape () etc. in the filter?

Reference site

How to deploy Responder with Uvicorn or Gunicorn-I want to talk about technology and bodge For an introduction to Python responder ... Preliminary research --Qiita Launch your application with Django + Nginx + Gunicorn | WEB Curtain Call [[1st] Let's create a machine learning web application using Responder and Keras [Overview] – Light Code Co., Ltd.](https://rightcode.co.jp/blog/information-technology/responder- keras-make-machine-learning-web-appsz9) Jinja2's custom filter to convert line breaks to --- A diary aimed at deprogramming beginners with Google App Engine + Python

Recommended Posts

Publish your website with responder + Gunicorn + Apache
Operate your website with Python_Webbrowser
Publish your own Python library with Homebrew
Try making a simple website with responder and sqlite3
Visualize fluctuations in numbers on your website with Datadog
Getting started with apache2