Working with nginx: from index.html to Flask app
Table of Contents
Background and motivation
End goal
Context on tooling
Reproduce locally: nginx web server grabs content from local file
Reproduce locally: nginx web server grabs content from Flask app
Conclusion
Note on tools
Background and motivation
We often only ever encounter web servers like nginx
in a remote setting. I wanted to write a tutorial based on working with nginx
locally to help readers get more comfortable working with the service. Being able to tinker with tools this way helps make debugging fast and fun. IMO, it’s also gratifying to get tools to work locally that are often reserved for staging and production environments.
End goal
My goal is to give you a solid understanding of what’s happening behind the scenes when a web server loads response data from a local file vs. what’s happening when a web server loads response data from a web application.
Context on tooling
- The instructions here are written for MacOS and VSCode. Adjust to your local setup as needed.
- I try to be mindful about the tools I choose to work with. For explanations of why I chose the tools I did, see Note on Tools
Reproduce locally: nginx web server grabs content from local file
First things first: you need to download a copy of nginx
to your local machine. Here’s how I download nginx
and use git to ensure the nginx
configuration files are version-controlled.
» brew install nginx
# I like to add version control to the nginx directory so I can easily revert to the default
# configuration if I make a breaking change.
» cd /usr/local/etc/nginx
» git init
» git add .
» git commit -m "Initial commit: homebrew nginx install"
Verify nginx
is configured properly with nginx -t
:
» nginx -t
nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful
Huzzah! You’ve successfully downloaded nginx
. Now let’s take a closer look at nginx
’s default server configuration. View nginx
’s server config file by running cat /usr/local/etc/nginx/nginx.conf
. Inside, you should see an un-commented server block that looks like:
server {
listen 8080;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
}
Bingo bongo bango. This code tells nginx
to return the content of the /usr/local/var/www/index.html
file when processing requests to the server’s homepage (/
) at localhost:8080.
Let me explain how that works; it’s not obvious. Nested within the location block, you’ll find the line root html;
. root
is an nginx
directive used to set a given path (here, html
) as docroot. This begs the question: well where is the html
directory? It’s not in /usr/local/etc/nginx
.
To figure out where the html
directory lives, you need to see if nginx
has any prefixes configured. Run nginx -V
to view nginx’s configure arguments. You’ll see there that the value of the --prefix
flag is /usr/local/Cellar/nginx/<version-number>
. This tells you to look for the html
directory at /usr/local/Cellar/nginx/<version-number>/html
. It’s there! Confusingly, though, that directory is symlinked to ../../../var/www
, or /usr/local/var/www
. This means that nginx
’s docroot is actually set to /usr/local/var/www
. If you look in that directory, you’ll see a couple html files, including index.html
.
Look at the contents of /usr/local/var/www/index.html
. You’ll see a block of HTML, inside which is the header, “Welcome to nginx!”
Start nginx
by running brew services start nginx
. Visit localhost:8080 in a browser. You should see the “Welcome to nginx!” message. Woo!
You can change the content that nginx
serves at localhost:8080 by modifying the index.html
file in /usr/local/var/www/
directly, or by creating a new .html
file in the same directory and changing the index directive within the server location block (in /usr/local/etc/nginx/nginx.conf
) to point to your new file instead: index <new-file-name>.html
.
Congrats! If you’ve come this far, you have successfully replicated the first of our two local setups: how to get a web server to serve content coming from an .html
file already loaded to its local filesystem.
Reproduce locally: nginx web server grabs content from Flask app
By this point, you’ve got a copy of nginx
running on your local machine and you know how to customize responses to homepage requests. It’s time to take things to the next level – configure nginx
to serve content produced by a Flask web application.
Step 1: get a flask web app running locally
Let’s put nginx
aside for now and focus on the singular task of getting a Flask web app running on your local machine.
You can create a super simple Flask app by following Flask’s Quickstart doc. You should end up with a hello.py
file like this:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, world!</p>"
Run this app locally with the command flask --app hello run
. Your terminal output will look like:
* Serving Flask app 'hello'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Visit http://localhost:5000/
in a browser and verify you see the “Hello, world!” response.
This is a victory; you’ve used Flask to run a local server and respond to HTTP requests. Pause here to let it soak in.
Step 2: get the flask web app to run locally using a production-ready server
Since we’re developing locally, swapping out Flask’s default WSGI1 server for a production-appropriate one is not strictly necessary. However, I still want to show you how to do it because it’s fairly straightforward and it’s a good learning opportunity.
Running flask --app hello run
returns the message: “WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.” But how to find a production WSGI server?
Flask’s Deploying to Production doc gives you several self-hosted options. This tutorial uses gunicorn
. All that’s required to make this change is for you to install gunicorn
and update the command you’re using to run the Flask app. Make sure to install gunicorn
and run gunicorn
commands from your Flask app’s virtual environment.
# I like to use the --reload flag so that gunicorn will restart workers alongside
# any code changes. Note this flag is meant for development use only.
gunicorn --workers=4 --reload hello:app
# If you see an error in your terminal output about worker timeout,
# "[CRITICAL] WORKER TIMEOUT", add a timeout flag to your gunicorn command:
gunicorn --workers=4 --reload --timeout=120 hello:app
Your terminal output will look like:
[<timestamp>] [<pid>] [INFO] Starting gunicorn 22.0.0
[<timestamp>] [<pid>] [INFO] Listening at: http://127.0.0.1:8000
[<timestamp>] [<pid>] [INFO] Using worker: sync
[<timestamp>] [<pid>] [INFO] Booting worker with pid:
Visit http://localhost:8000/
– note the port has changed from 5000 to 8000 – in a browser and verify you see the “Hello, world!” response.
By this point, you should have two different processes running locally: a production-ready WSGI server (gunicorn
) serving Flask web app content, and a web server (nginx
) serving index.html
content. The final step to getting nginx
to serve content from your Flask app is telling nginx
how to act as a proxy.
Step 3: configure nginx as a proxy
In order for nginx
to respond with content originating from your Flask web app when you make requests to localhost:8080, nginx
needs to be able to forward requests to Flask, process Flask’s response data, then pass that response data on to the HTTP client at localhost:8080. It sounds like that could be a headache to configure, but nginx
has a directive specifically designed to do this: proxy_pass
! Read more about proxy_pass
here.
Remember in this section when you told nginx
to use whatever was in the /usr/local/var/www/index.html
file as its response to homepage requests? Go back to that same section of code inside the /usr/local/etc/nginx/nginx.conf
file and update the location block to instead proxy requests to where gunicorn
is running: localhost:8000:
location / {
proxy_pass http://localhost:8000;
}
This single line of code configures nginx
to pass requests to the proxied server at localhost:8000 (gunicorn
in our case), fetch the response, and send the response back to the HTTP client.
Step 4: watch nginx return a response generated from your Flask web app
In the nginx web server grabs content from local file section, you started nginx
as a background service, then visited localhost:8080 in a browser to see the response “Welcome to nginx!”.
Go back to localhost:8080 in a browser and refresh the page. You should now see “Hello, world!”, which means that you’ve successfully configured nginx
to process and return responses from the Flask web app. Cowabunga!
Conclusion
In this tutorial, we configured nginx
to proxy requests between an HTTP client and a Flask web application, all through local development. While the nginx
configuration and Flask app are much simpler than versions you’ll encounter in workplace settings, having a local reproduction of production-like architecture gives you a sandbox for tinkering with nginx
without the risk of production downtime.
Note on tools
- I chose
nginx
because I find its documentation easier to navigate thanhttpd
’s, and because of the service’s expressed commitment to the open source community. - I chose to work with Python because I want to practice coding in my non-dominant language.
- I chose to work with Flask because it’s a lightweight web framework I’ve used before.
- I chose AWS because it’s the cloud provider I assume the majority of readers will be familiar with.