init (#120)
Signed-off-by: Grant Birkinbine <grant.birkinbine@gmail.com>
This commit is contained in:
parent
a42a8531ab
commit
4480b64e58
104
nginx-wsgi-flask/README.md
Normal file
104
nginx-wsgi-flask/README.md
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# Compose Sample Application
|
||||||
|
|
||||||
|
## NGINX Reverse Proxy -> WSGI -> Python/Flask Backend
|
||||||
|
|
||||||
|
Project structure:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├── docker-compose.yaml
|
||||||
|
├── flask
|
||||||
|
│ ├── app.py
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ ├── requirements.txt
|
||||||
|
│ └── wsgi.py
|
||||||
|
└── nginx
|
||||||
|
├── default.conf
|
||||||
|
├── Dockerfile
|
||||||
|
├── nginx.conf
|
||||||
|
└── start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
[_docker-compose.yaml_](docker-compose.yaml)
|
||||||
|
|
||||||
|
```yml
|
||||||
|
services:
|
||||||
|
nginx-proxy:
|
||||||
|
build: nginx
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
flask-app:
|
||||||
|
build: flask
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
The compose file defines an application with two services `nginx-proxy` and `flask-app`.
|
||||||
|
When deploying the application, docker-compose maps port 80 of the web service container to port 80 of the host as specified in the file.
|
||||||
|
|
||||||
|
Make sure port 80 on the host is not being used by another container, otherwise the port should be changed.
|
||||||
|
|
||||||
|
## Deploy with docker-compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker-compose up -d
|
||||||
|
Creating network "nginx-wsgi-flask_default" with the default driver
|
||||||
|
Building flask-app
|
||||||
|
...
|
||||||
|
Building nginx-proxy
|
||||||
|
...
|
||||||
|
Creating nginx-wsgi-flask_flask-app_1 ... done
|
||||||
|
Creating nginx-wsgi-flask_nginx-proxy_1 ... done
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected result
|
||||||
|
|
||||||
|
Listing containers must show two containers running and the port mapping as below:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker ps
|
||||||
|
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||||
|
bde3f29cf571 ...nginx-proxy "/docker-entrypoint.…" About a minute ago Up About a minute (healthy) 0.0.0.0:80->80/tcp ...nginx-proxy_1
|
||||||
|
86c44470b547 ...flask-app "gunicorn -w 3 -t 60…" About a minute ago Up About a minute (healthy) 5000/tcp, 0.0.0.0:8000->8000/tcp ...flask-app_1
|
||||||
|
```
|
||||||
|
|
||||||
|
After the application starts, navigate to `http://localhost:80` in your web browser or run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl localhost:80
|
||||||
|
Hello World!
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop and remove the containers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker-compose down
|
||||||
|
Stopping nginx-wsgi-flask_nginx-proxy_1 ... done
|
||||||
|
Stopping nginx-wsgi-flask_flask-app_1 ... done
|
||||||
|
Removing nginx-wsgi-flask_nginx-proxy_1 ... done
|
||||||
|
Removing nginx-wsgi-flask_flask-app_1 ... done
|
||||||
|
Removing network nginx-wsgi-flask_default
|
||||||
|
```
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
By following the steps above, you will have an NGINX Reverse Proxy and a Flask backend. The general traffic flow will look like the following:
|
||||||
|
|
||||||
|
`Client -> NGINX -> WSGI -> Flask`
|
||||||
|
|
||||||
|
### NGINX
|
||||||
|
|
||||||
|
With this deployment model, we use NGINX to proxy and handle all requests to our Flask backend. This is a powerful deployment model as we can use NGINX to cache responses or even act as an application load balancer between multiple Flask backends. You could also integrate a Web Application Firewall into NGINX to protect your Flask backend from attacks.
|
||||||
|
|
||||||
|
### WSGI
|
||||||
|
|
||||||
|
WSGI (Web Server Gateway Interface) is the interface that sits in between our NGINX proxy and Flask backend. It is used to handle requests and interface with our backend. WSGI allows you to handle thousands of requests at a time and is highly scalable. In this `docker-compose` sample, we use Gunicorn for our WSGI.
|
||||||
|
|
||||||
|
### Flask
|
||||||
|
|
||||||
|
Flask is a web development framework written in Python. It is the "backend" which processes requests.
|
||||||
|
|
||||||
|
A couple of sample endpoints are provided in this `docker-compose` example:
|
||||||
|
|
||||||
|
* `/` - Returns a "Hello World!" string.
|
||||||
|
* `/cache-me` - Returns a string which is cached by the NGINX reverse proxy. This demonstrates an intermediary cache implementation.
|
||||||
|
* `/info` - Returns informational headers about the request. Some are passed from NGINX for added client visibility.
|
30
nginx-wsgi-flask/docker-compose.yaml
Normal file
30
nginx-wsgi-flask/docker-compose.yaml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
version: "3.7"
|
||||||
|
services:
|
||||||
|
nginx-proxy:
|
||||||
|
build: nginx
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./nginx/default.conf:/tmp/default.conf
|
||||||
|
environment:
|
||||||
|
- FLASK_SERVER_ADDR=flask-app:8000
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
- flask-app
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl --silent --fail localhost:80/health-check || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
command: /app/start.sh
|
||||||
|
flask-app:
|
||||||
|
build: flask
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- '8000:8000'
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl --silent --fail localhost:8000/flask-health-check || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
command: gunicorn -w 3 -t 60 -b 0.0.0.0:8000 app:app
|
32
nginx-wsgi-flask/flask/Dockerfile
Normal file
32
nginx-wsgi-flask/flask/Dockerfile
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
FROM python:3.9.2-alpine
|
||||||
|
|
||||||
|
# upgrade pip
|
||||||
|
RUN pip install --upgrade pip
|
||||||
|
|
||||||
|
# get curl for healthchecks
|
||||||
|
RUN apk add curl
|
||||||
|
|
||||||
|
# permissions and nonroot user for tightened security
|
||||||
|
RUN adduser -D nonroot
|
||||||
|
RUN mkdir /home/app/ && chown -R nonroot:nonroot /home/app
|
||||||
|
RUN mkdir -p /var/log/flask-app && touch /var/log/flask-app/flask-app.err.log && touch /var/log/flask-app/flask-app.out.log
|
||||||
|
RUN chown -R nonroot:nonroot /var/log/flask-app
|
||||||
|
WORKDIR /home/app
|
||||||
|
USER nonroot
|
||||||
|
|
||||||
|
# copy all the files to the container
|
||||||
|
COPY --chown=nonroot:nonroot . .
|
||||||
|
|
||||||
|
# venv
|
||||||
|
ENV VIRTUAL_ENV=/home/app/venv
|
||||||
|
|
||||||
|
# python setup
|
||||||
|
RUN python -m venv $VIRTUAL_ENV
|
||||||
|
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
RUN export FLASK_APP=app.py
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
# define the port number the container should expose
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD ["python", "app.py"]
|
27
nginx-wsgi-flask/flask/app.py
Normal file
27
nginx-wsgi-flask/flask/app.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from flask import Flask, request, jsonify
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def hello():
|
||||||
|
return "Hello World!"
|
||||||
|
|
||||||
|
@app.route('/cache-me')
|
||||||
|
def cache():
|
||||||
|
return "nginx will cache this response"
|
||||||
|
|
||||||
|
@app.route('/info')
|
||||||
|
def info():
|
||||||
|
|
||||||
|
resp = {
|
||||||
|
'connecting_ip': request.headers['X-Real-IP'],
|
||||||
|
'proxy_ip': request.headers['X-Forwarded-For'],
|
||||||
|
'host': request.headers['Host'],
|
||||||
|
'user-agent': request.headers['User-Agent']
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(resp)
|
||||||
|
|
||||||
|
@app.route('/flask-health-check')
|
||||||
|
def flask_health_check():
|
||||||
|
return "success"
|
2
nginx-wsgi-flask/flask/requirements.txt
Normal file
2
nginx-wsgi-flask/flask/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Flask==1.1.1
|
||||||
|
gunicorn==20.0.4
|
5
nginx-wsgi-flask/flask/wsgi.py
Normal file
5
nginx-wsgi-flask/flask/wsgi.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from app import app
|
||||||
|
import os
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host='0.0.0.0', port=os.environ.get("FLASK_SERVER_PORT"), debug=True)
|
32
nginx-wsgi-flask/nginx/Dockerfile
Normal file
32
nginx-wsgi-flask/nginx/Dockerfile
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
FROM nginx:1.19.7-alpine
|
||||||
|
|
||||||
|
# Add bash for boot cmd
|
||||||
|
RUN apk add bash
|
||||||
|
|
||||||
|
# Add nginx.conf to container
|
||||||
|
COPY --chown=nginx:nginx nginx.conf /etc/nginx/nginx.conf
|
||||||
|
COPY --chown=nginx:nginx start.sh /app/start.sh
|
||||||
|
|
||||||
|
# set workdir
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# permissions and nginx user for tightened security
|
||||||
|
RUN chown -R nginx:nginx /app && chmod -R 755 /app && \
|
||||||
|
chown -R nginx:nginx /var/cache/nginx && \
|
||||||
|
chown -R nginx:nginx /var/log/nginx && \
|
||||||
|
chmod -R 755 /var/log/nginx; \
|
||||||
|
chown -R nginx:nginx /etc/nginx/conf.d
|
||||||
|
RUN touch /var/run/nginx.pid && chown -R nginx:nginx /var/run/nginx.pid
|
||||||
|
|
||||||
|
# # Uncomment to keep the nginx logs inside the container - Leave commented for logging to stdout and stderr
|
||||||
|
# RUN mkdir -p /var/log/nginx
|
||||||
|
# RUN unlink /var/log/nginx/access.log \
|
||||||
|
# && unlink /var/log/nginx/error.log \
|
||||||
|
# && touch /var/log/nginx/access.log \
|
||||||
|
# && touch /var/log/nginx/error.log \
|
||||||
|
# && chown nginx /var/log/nginx/*log \
|
||||||
|
# && chmod 644 /var/log/nginx/*log
|
||||||
|
|
||||||
|
USER nginx
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "'daemon off;'"]
|
29
nginx-wsgi-flask/nginx/default.conf
Normal file
29
nginx-wsgi-flask/nginx/default.conf
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
proxy_cache_path /tmp/cache levels=1:2 keys_zone=cache:10m max_size=500m inactive=60m use_temp_path=off;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://$FLASK_SERVER_ADDR;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /cache-me {
|
||||||
|
proxy_pass http://$FLASK_SERVER_ADDR;
|
||||||
|
proxy_cache cache;
|
||||||
|
proxy_cache_lock on;
|
||||||
|
proxy_cache_valid 200 30s;
|
||||||
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||||
|
proxy_cache_revalidate on;
|
||||||
|
proxy_cache_background_update on;
|
||||||
|
expires 20s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /health-check {
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
return 200 "success";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
50
nginx-wsgi-flask/nginx/nginx.conf
Normal file
50
nginx-wsgi-flask/nginx/nginx.conf
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
worker_processes auto;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# Define the format of log messages.
|
||||||
|
log_format main_ext '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||||
|
'"$host" sn="$server_name" '
|
||||||
|
'rt=$request_time '
|
||||||
|
'ua="$upstream_addr" us="$upstream_status" '
|
||||||
|
'ut="$upstream_response_time" ul="$upstream_response_length" '
|
||||||
|
'cs=$upstream_cache_status' ;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main_ext;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
# Enable Compression
|
||||||
|
gzip on;
|
||||||
|
|
||||||
|
# Disable Display of NGINX Version
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
# Size Limits
|
||||||
|
client_body_buffer_size 10K;
|
||||||
|
client_header_buffer_size 1k;
|
||||||
|
client_max_body_size 8m;
|
||||||
|
large_client_header_buffers 2 1k;
|
||||||
|
|
||||||
|
# # SSL / TLS Settings - Suggested for Security
|
||||||
|
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
# ssl_session_timeout 15m;
|
||||||
|
# ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
|
# ssl_prefer_server_ciphers on;
|
||||||
|
# ssl_session_tickets off;
|
||||||
|
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
|
||||||
|
}
|
2
nginx-wsgi-flask/nginx/start.sh
Normal file
2
nginx-wsgi-flask/nginx/start.sh
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
envsubst '$FLASK_SERVER_ADDR' < /tmp/default.conf > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'
|
Loading…
Reference in New Issue
Block a user