Serving a Dynamic Website with AWS EC2

For building simple websites such as this one, you can use static website generators like React, Jekyll, or Hugo. But for building more sophisticated websites with forms, user login, and database interaction, you'll need to develop a web app with a backend using one of many frameworks, including .NET, Java, Ruby on Rails, and more. For this tutorial, we'll focus on Django, one of the most popular frameworks for developing Python web apps. The purpose of this post is to walk you through deploying a working Python web app to a production server. We'll focus on Django, but the steps are similar for the other Python frameworks, such as Flask and FastAPI.

This blog post is part of a series of posts about how to deploy a Django app to AWS. If you haven't deployed your server yet, follow the instructions in Serving a Static Website from EC2. This post also assumes you have a working Django web app already. If not, follow the steps in the Django tutorial, then come back to this page .

During development, you can test your web app locally using python manage.py runserver, but when it's time to deploy your code to a server you'll need to use a different tool for serving the app. This is because tools like runserver are optimized for refreshing content quickly during development, not for serving multiple users efficiently and reliably. For this blog post, we'll deploy our website using a tool called Gunicorn (an app server) to run our Python code, which will communicate with NGINX (our web server we set up previously) to serve pages to our users. Note that Gunicorn will run any kind of WSGI app, which includes Flask, Django, and FastAPI, but we'll stick with Django for the examples.

Also note that tools like AWS's Elastic Beanstalk are also available for automatically deploying Django projects, but are generally more costly and hide a lot of implementation details. Instead, for this tutorial we'll deploy our app server by hand.

01

01  Install Gunicorn

Inside your Django project, look for a file called wsgi.py—it should have automatically been placed there when you created your Django project for the first time. Note down the name of the folder this wsgi.py file is in, e.g. myappname/. Let's install Gunicorn and point it to this file to run your app.

  1. Clone your Django project to your EC2 machine. Install any dependencies if needed with pip install -r requirements.txt.
  2. On your EC2 machine, run sudo apt install gunicorn.
  3. Run the following:
    sudo su
    cd location/of/your/django/project
    gunicorn --workers 4 --bind 0:8000 myappname.wsgi:application

Side note: we've passed a few config options to Gunicorn here. If we don't want to keep passing them to Gunicorn every time, we simply point Gunicorn to a config file containing these options instead, and run it with gunicorn -c /path/to/config.py. Here's a sample config file with more options for convenience:

"""Gunicorn config file"""

# Django WSGI application path in pattern MODULE_NAME:VARIABLE_NAME
wsgi_app = "myappname.wsgi:application"
# The granularity of Error log outputs
loglevel = "debug"
# The number of worker processes for handling requests
workers = 4
# The socket to bind
bind = "0.0.0.0:8000"
# Restart workers when code changes (development only!)
reload = False
# Write access and error info to /var/log
accesslog = errorlog = "/var/log/gunicorn/gunicorn.log"
# Redirect stdout/stderr to log file
capture_output = True
# Daemonize the Gunicorn process (detach & enter background)
daemon = True
# Kill and restart workers silent for more than this many seconds
timeout = 10
# Restart workers after this many requests
max_requests = 1200
# Stagger reloading of workers to avoid restarting at the same time
max_requests_jitter = 90

Now that Gunicorn is successfully launched and running on port 8000, we simply need to hook it up to NGINX so that it can be served at port 443, which is where HTTPS traffic is served. Modify your /etc/nginx/sites-enabled/mywebsitename.com file to look like this:

server {
    listen 80;
    server_name mywebsitename.com www.mywebsitename.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name mywebsitename.com www.mywebsitename.com;

    location / {
        include proxy_params;
        proxy_pass http://localhost:8000;
    }

    ssl_certificate /etc/letsencrypt/live/mywebsitename.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mywebsitename.com/privkey.pem;
}

02

02  Next Steps

Optionally, you can set up a process manager like supervisor to automatically start gunicorn whenever the server boots and automatically restart it if the process ever crashes. Create a file with sudo privileges called /etc/supervisor/conf.d/gunicorn.conf and add the following:

[program:django_gunicorn]
directory=/home/ubuntu/website/
command=/usr/bin/gunicorn -c /home/ubuntu/.gunicorn/config.py
autostart=true
environment=PROCURESPARK_DEBUG="false"
autorestart=true
stdout_logfile=/var/log/supervisor/django-gunicorn-out.log
stderr_logfile=/var/log/supervisor/django-gunicorn-err.log

Note that whenever you want to deploy new changes, you'll have to restart gunicorn via supervisor:

sudo systemctl restart supervisor

You did it! You now have an EC2 machine serving your Django app. This is good enough for a personal project, but if you want to launch a business that supports hundreds or thousands of users, there's still a long road ahead: static storage and databases and load balancing, oh my! Perhaps best saved for another blog post. Until then, happy serving!