Ravindra BagaleCourses & study guides मराठी Track your progress

Host a Python Flask App on AWS EC2 with Gunicorn and Nginx

Most apps on your phone do not download web pages. They ask a server for data and draw the screen themselves. That server side is an API, and Python with Flask is one of the simplest ways to build one. In this chapter you learn how an API differs from a website, set up Python, pip and a virtual environment on an Amazon Linux 2023 EC2 instance, write a small Flask app, and take it from the test server to a proper setup: gunicorn running it in the background, nginx in front as the reverse proxy, port 5000 closed, and a systemd service that starts it at boot and reloads it when the code changes.

What you'll learn in this class

  • The difference between a website and an API, and how JSON carries data
  • What hosting a Python app needs: python3, pip, a virtual environment, gunicorn and nginx
  • Package managers: yum, pip, npm and composer
  • Creating and activating a virtual environment, and why sudo pip is a mistake
  • Writing requirements.txt and a two-route Flask app.py
  • Testing with the Flask development server and a temporary port 5000 rule
  • Running the app in the background with gunicorn, and what app:app means
  • nginx as the reverse proxy, its worker processes and worker_connections
  • Binding gunicorn to 127.0.0.1 and closing port 5000
  • Adding a JSON API route and reloading gunicorn with kill -HUP
  • A systemd service for gunicorn, and several sites on one nginx
  • Fixing 502 Bad Gateway, the nginx welcome page and nginx -t errors

1. Website or API

Why: a web page built for a laptop screen does not fit an Android phone, an iPhone and a tablet at the same time. Modern apps solve this by asking the server only for data.

What:

  1. A website answers with a whole HTML page: layout, text and images together. The browser just shows it.
  2. An API (Application Programming Interface) answers with data only. Each app, Android, iPhone or web, draws its own screen from that data.
  3. The data usually travels as JSON: key: value pairs inside curly braces { }. Text values go in double quotes, numbers do not, and a comma separates the pairs.

Classroom line

If the server sends back a page, it's a website.

Classroom line

These days, servers send data.

A website sends a whole page; an APIsends only data Website Browser Server <html><body> <h1>Reels</h1> <img src="..."> ...</body></html> The server builds the whole HTML page.Fine for a browser, wrong size for every phone app. API Android, iPhone, web Server {"username": "ravi", "likes": 50, "caption": "on the top"} The server sends only data, as JSON.Each app draws its own screen from the same data.

Figure 1. A website sends a whole HTML page; an API sends only data as JSON, and every app, Android, iPhone or web, draws its own screen from it.

Worked example: one reel in a short-video app. The app asks the server for the next reel and gets:

{
  "username": "ravi",
  "profile_pic": "https://.../ravi.jpg",
  "caption": "on the top",
  "video_url": "https://.../reel42.mp4",
  "likes": 50
}
  1. The app shows username and profile_pic at the top of the screen.
  2. It plays video_url in the middle.
  3. It writes caption and the likes count below the video.

Classroom line

JSON sits inside curly braces.

One reel as JSON, and where the app shows eachvalue { "username": "ravi", "profile_pic": "https://.../ravi.jpg", "caption": "on the top", "video_url": "https://.../reel42.mp4", "likes": 50 } ◯ ravi ▶ video plays here on the top ♥ 50 likes JSON = key:value pairs inside curly braces { }. Text in double quotes, numbers without quotes, a comma between pairs. The app knows the keys and puts each value in its place.

Figure 2. One reel as JSON: the app knows the keys and puts username and profile_pic at the top, the video in the middle, and the caption and likes below it.

In the rest of this chapter you build a small API like this in Python and host it on EC2.

2. What hosting a Python app needs

Why: A Python app is a stack, not one program. Knowing the six pieces tells you what to install and, when the app is down, which piece to check.

What: six pieces, from the bottom up:

  1. python3, the language itself. Python 2 is retired; Python 3 changed enough that the two use different commands, python2 and python3. Amazon Linux 2023 ships only python3.
  2. pip, Python's package manager. It downloads libraries such as Flask.
  3. A virtual environment (venv), a folder that holds one app's libraries, separate from every other app.
  4. The app: app.py, written with Flask, plus requirements.txt that lists the libraries it needs.
  5. gunicorn, the production app server. It is a WSGI server: it runs Python web apps such as Flask and Django. For async frameworks such as FastAPI you would use an ASGI server such as uvicorn.
  6. nginx in front, on port 80, passing requests to gunicorn.

Package managers you will meet:

  1. yum (or dnf) installs Linux packages on Amazon Linux: sudo yum install nginx -y.
  2. pip installs Python libraries: pip install flask.
  3. npm installs Node.js libraries, and composer installs PHP libraries.

Python comes preinstalled on most Linux systems and on macOS; on Windows you install it yourself.

3. Launch the server and check Python

Why: Everything below runs on this server. Checking the Python version first avoids installing libraries that need a newer Python.

How:

  1. Launch an EC2 instance: Amazon Linux 2023, t2.micro or t3.micro, your key pair, an 8 GiB gp3 disk, and a security group that allows SSH (22) from My IP and HTTP (80) from anywhere.
  2. Connect from your laptop:

    ssh -i mykey.pem ec2-user@PUBLIC_IP
    
  3. Check Python and install pip:

    python3 -V
    sudo yum install python3-pip -y
    pip3 -V
    
  4. python3 -V prints a version such as Python 3.9.x. Typing just python gives command not found, because the command is python3.

Classroom line

First let's check whether Python is there.

Classroom line

Type just python and you get 'command not found'.

4. Create a virtual environment

Why: one server often runs several Python apps. App A may need Flask 3.1 and app B Django 4.2 with different versions of the same helper libraries. Installed together, they clash. The system Python is also used by the operating system and yum, so app libraries do not belong there.

What: a venv is a folder with its own python, its own pip and its own libraries. The venv module is built into python3; no extra package is needed.

How:

mkdir ~/pythonapp
cd ~/pythonapp
python3 -m venv myenv
ls
source myenv/bin/activate
  1. ls now shows a folder called myenv.
  2. After source, the prompt starts with (myenv). From now on python and pip mean the ones inside myenv.
  3. deactivate leaves the venv.
  4. Inside a venv, never use sudo pip or sudo python: sudo runs as root with root's PATH and skips the venv, so the libraries land in the system Python.
  5. If pip install says Permission denied, the folder was created with sudo. Give it back to your user, then run pip install without sudo:
    sudo chown -R ec2-user:ec2-user ~/pythonapp
    

Classroom line

Run ls; you should see a folder called myenv.

Classroom line

When the prompt looks like this, the venv is active.

A virtual environment gives eachapp its own libraries One EC2 server System Python /usr/bin/python3 Used by the OS and yum.Do not install applibraries here. App A ~/pythonapp/myenv python, pip, Flask 3.1,gunicorn App B ~/shopapp/myenv python, pip, Django 4.2,its own versions No clash: each venv is a folder. python3 -m venv myenv # create (builtinto python3)source myenv/bin/activate # promptstarts with (myenv)pip install -r requirements.txt # nosudo inside a venv

Figure 3. Each app gets its own venv folder with its own python, pip and libraries, so Flask in one app and Django in another never clash, and the system Python stays clean.

5. Write requirements.txt and app.py

Why: requirements.txt lets anyone install exactly the libraries the app needs with one command. app.py is the app itself.

How: inside ~/pythonapp, with the venv active:

  1. nano requirements.txt and type two lines:

    Flask
    gunicorn
    
  2. nano app.py and type:

    from flask import Flask
    
    app = Flask(__name__)
    
    @app.route('/')
    def home():
        return 'Hello from Python on EC2'
    
    @app.route('/hi')
    def hi():
        return 'Hi! This is the second page.'
    
    if __name__ == '__main__':
        app.run(host='0.0.0.0', port=5000)
    

What each part does:

  1. app = Flask(__name__) creates the Flask app object, named app.
  2. @app.route('/') connects a URL path to the function below it. / is the bare address with no page name; /hi is http://ADDRESS/hi.
  3. Python uses indentation to group code. The return lines must be indented the same way, with spaces; one extra space gives an IndentationError.
  4. host='0.0.0.0' accepts requests arriving on any of the server's addresses; port=5000 is the port the test server listens on.

Classroom line

Python cares about spaces; one extra space and it won't run.

Classroom line

Just "/" means only the IP, no page name.

6. Install the libraries and test with the Flask server

Why: Running the app with Flask's own server first proves the code and libraries work, before gunicorn and nginx add more places for something to go wrong.

How:

  1. Try to run the app before installing anything:

    python3 app.py
    

    It fails with ModuleNotFoundError: No module named 'flask'. The same error appears whenever the venv is not active.

  2. Install the libraries into the venv, and record the exact versions:

    pip install -r requirements.txt
    pip freeze > requirements.txt
    

    pip freeze writes lines such as Flask==3.1.x, so the next server gets the same versions.

  3. Start the test server:

    python3 app.py
    

    It prints Running on http://127.0.0.1:5000 and a warning: "This is a development server. Do not use it in a production deployment." The Flask server is for testing only.

  4. In the EC2 console, add a temporary inbound rule to the security group: Custom TCP, port 5000, source 0.0.0.0/0.

  5. Open http://PUBLIC_IP:5000 and http://PUBLIC_IP:5000/hi in the browser. Plain http://PUBLIC_IP does not work yet, because nothing serves port 80.
  6. The terminal is now busy: the server runs in the foreground. Press Ctrl+C and it stops, and the site goes down with it. Closing the SSH window stops it too.

Classroom line

It accepts requests on port 5000 from any IP in the world.

Classroom line

Foreground means I can't do anything else in this terminal.

Classroom line

Press Ctrl+C and the server stops.

Three ways to run the app python3 app.py Flask development server. Foreground: the terminal is busy. Ctrl+C orclosing SSH stops it. For testing only. gunicorn --daemon Production WSGI server. Background: the terminal is free. Surviveslogout, not a reboot. You stop and restart it by hand. systemd service gunicorn run by the OS. Starts at boot, restarts on crash. sudo service gunicorn restart The proper way for a live app.

Figure 4. Three ways to run the app: the Flask development server in the foreground for testing, gunicorn with --daemon in the background, and gunicorn as a systemd service that starts at boot, the proper way for a live app.

7. Run the app in the background with gunicorn

Why: a live app must keep running after you log out, and must handle many requests at once. gunicorn does both.

How: with the venv active, inside ~/pythonapp:

gunicorn --bind 0.0.0.0:5000 --workers 2 --daemon app:app
curl http://127.0.0.1:5000
curl http://127.0.0.1:5000/hi
  1. app:app means "the file app.py, the Flask object called app inside it".
  2. --bind sets the address and port. Without it gunicorn listens on 127.0.0.1:8000, its default.
  3. --workers 2 starts two worker processes under one master process.
  4. --daemon runs it in the background: the prompt comes back, and the app keeps running after you log out.
  5. curl tests the app from the server itself, without a browser.
  6. If SSH disconnects, the venv is no longer active in the new session. Run cd ~/pythonapp and source myenv/bin/activate again before any python, pip or gunicorn command.

Classroom line

--daemon means run in the background.

Classroom line

Re-activating is a must after SSH reconnects.

8. Put nginx in front as the reverse proxy

Why: visitors should use plain http://ADDRESS on port 80 (and later 443 for HTTPS), not port 5000. nginx is built to face the internet: it handles thousands of connections, serves static files, and later terminates HTTPS.

What:

  1. nginx is a reverse proxy: like a front desk, it receives every request and passes app requests to gunicorn, the back office, with proxy_pass. The visitor only ever talks to nginx.
  2. nginx runs a few worker processes; worker_processes auto starts one per CPU. Each worker runs an event loop that juggles many connections at once, up to worker_connections, which Amazon Linux's nginx.conf sets to 1024.
nginx is the front desk; gunicorn is the backoffice Visitors Hundreds ofbrowsers andapps at once nginx, the reverse proxy (port 80/443) Receives every request first.Serves static files itself; passes apprequests to gunicorn (proxy_pass). worker event loop 1024 conn. worker event loop 1024 conn. worker event loop 1024 conn. gunicorn (app server) 127.0.0.1:5000 Its workers run yourFlask code and returnthe answer to nginx. worker_processes auto: one nginx worker per CPU. Each worker'sevent loop juggles many connections at once;Amazon Linux's nginx.conf sets worker_connections 1024 perworker.

Figure 5. nginx is the front desk: it receives every request on port 80 or 443, serves static files itself and passes app requests to gunicorn on 127.0.0.1:5000. Each nginx worker's event loop handles up to worker_connections connections, 1024 in Amazon Linux's nginx.conf.

How:

  1. Install and start nginx:

    sudo yum install nginx -y
    sudo service nginx start
    
  2. Open the config with sudo nano /etc/nginx/nginx.conf. Inside the existing server { ... } block, after the root line, add:

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    
  3. Every directive ends with ; and every { needs its }. Check the file, then reload:

    sudo nginx -t
    sudo service nginx reload
    
  4. nginx -t must say syntax is ok and test is successful. If it shows an error, nginx keeps running with the old config; fix the line it names and test again.

  5. Open http://PUBLIC_IP without a port: the Flask app answers through nginx.

Classroom line

Don't forget the semicolon here.

A black terminal with nano open on nginx.conf: server {, listen 80;, listen [::]:80;, server_name _;, root /usr/share/nginx/html;, then location /{, proxy_pass http://localhost:5000; and a closing brace.

Whiteboard: The default server block in nginx.conf with a location / block passing requests on to port 5000. Writing 127.0.0.1 instead of localhost avoids nginx trying the IPv6 address ::1 first.

The proxy_set_header lines pass the real visitor address and the original host name on to the app; without them every request seems to come from nginx itself.

Close port 5000: now that nginx is in front, nobody outside needs port 5000.

  1. Stop the test gunicorn (section 9 shows how to find its PID) and start it bound to the server only:

    gunicorn --bind 127.0.0.1:5000 --workers 2 --daemon app:app
    
  2. Delete the port 5000 rule from the security group. Keep 22 from My IP, 80, and 443 if you add HTTPS.

  3. http://PUBLIC_IP still works; http://PUBLIC_IP:5000 no longer answers.
Security group: while testing, and oncenginx is in front Testing with the Flask server Inbound rules 22 My IP SSH 80 0.0.0.0/0 HTTP 5000 0.0.0.0/0 temporary test Live, behind nginx Inbound rules 22 My IP SSH 80 0.0.0.0/0 HTTP 443 0.0.0.0/0 HTTPS 5000 0.0.0.0/0 rule deleted gunicorn on 0.0.0.0:5000 answersanyone who reaches port 5000. gunicorn on 127.0.0.1:5000: only nginx reaches it.

Figure 6. While testing, the security group has a temporary rule for port 5000; once nginx is in front, the 5000 rule is deleted and gunicorn binds 127.0.0.1:5000, so only nginx can reach it.

How a request reaches your Python app Browser GET /getusers nginx :80 reverse proxy gunicorn 127.0.0.1:5000 Flask app.py get_users() 80 Private: 127.0.0.1 only, port 5000 closed 1. The browser opens http://PUBLIC_IP/getusers.The security group lets port 80 in. 2. nginx receives it and, by location / { proxy_passhttp://127.0.0.1:5000; }, passes it on. 3. A gunicorn worker runs the Flask function for/getusers. 4. The JSON goes back the same way: gunicorn, nginx,browser. nginx faces the internet; gunicorn and the app stay private on127.0.0.1.

Figure 7. The browser reaches nginx on port 80; nginx passes the request to gunicorn on 127.0.0.1:5000; a gunicorn worker runs the Flask function, and the JSON goes back the same way.

9. Add an API route and reload gunicorn

Why: an API is just a route that returns data. Adding one shows the most common surprise with a background server: new code does nothing until the server reloads it.

How:

  1. Add a route to app.py, above the if __name__ line:

    @app.route('/getusers')
    def get_users():
        return {"username": "ravi", "likes": 50, "caption": "on the top"}
    
  2. Flask turns a returned dictionary into JSON and sets the application/json content type.

  3. Open http://PUBLIC_IP/getusers: it shows 404 Not Found. gunicorn loaded app.py when its workers started, and it is still running the old code.
  4. Find the gunicorn processes:

    ps aux | grep gunicorn
    ps -ef | grep gunicorn
    
  5. ps aux lists every process: a all users, u user-oriented columns, x processes without a terminal too. ps -ef adds the parent PID column: the master is the gunicorn process whose parent is not another gunicorn; the workers are its children.

  6. Reload the code gracefully, using the master's PID:

    kill -HUP 4101
    
  7. HUP makes the master start new workers with the new code and stop the old ones after they finish their requests. Open /getusers again: it returns {"caption":"on the top","likes":50,"username":"ravi"}.

  8. To stop gunicorn, run kill 4101 (TERM): it shuts down cleanly. Use kill -9 only if it is stuck.

Classroom line

Suppose you need a new API; what will you do?

Classroom line

How to find it? Run ps aux.

A plain white browser page showing one line of JSON: {"username":"ravi","likes":50,"caption":"on the top"}.

Whiteboard: The /getusers route answering with JSON. This app returned a ready-made JSON string; a returned dictionary comes back with its keys sorted, as in step 7.

New code needs a gunicorn reload 1. You add /getusers to app.py, but the running gunicornstill has the old code curl -s -o /dev/null -w "%{http_code}\n"http://127.0.0.1:5000/getusers 404 Python loads app.py once, when the workers start. Saving the filechanges nothing yet. 2. Find the master process and send it HUP ps -ef | grep [g]unicorn ec2-user 4101 1 ... gunicorn --bind 127.0.0.1:5000--daemon app:app ec2-user 4102 4101 ... gunicorn --bind 127.0.0.1:5000--daemon app:app kill -HUP 4101 The master is the one whose parent (third column) is not anothergunicorn. HUP starts new workers with the new code and stops the oldones gently. kill 4101 stops it; kill -9 only if stuck. 3. The new route answers curl http://127.0.0.1:5000/getusers {"caption":"on the top","likes":50,"username":"ravi"} With systemd: sudo service gunicorn reload does the same HUP for you.

Figure 8. A new route gives 404 because the running workers hold the old code; find the master PID with ps, send it HUP, and the new workers answer with the JSON.

10. Run gunicorn as a systemd service

Why: a --daemon gunicorn does not start again after a reboot or a crash, and you must hunt for PIDs to manage it. A systemd service fixes all three. This is the proper way to run a live app.

How:

  1. Stop the --daemon gunicorn with kill <master PID>.
  2. Create the unit file with sudo nano /etc/systemd/system/gunicorn.service:

    [Unit]
    Description=Gunicorn for pythonapp
    After=network.target
    
    [Service]
    User=ec2-user
    Group=ec2-user
    WorkingDirectory=/home/ec2-user/pythonapp
    ExecStart=/home/ec2-user/pythonapp/myenv/bin/gunicorn --workers 2 --bind 127.0.0.1:5000 app:app
    ExecReload=/bin/kill -s HUP $MAINPID
    Restart=on-failure
    
    [Install]
    WantedBy=multi-user.target
    
  3. ExecStart uses the full path to the venv's gunicorn, so no activate is needed. There is no --daemon: systemd itself keeps it in the background.

  4. Load, enable at boot, and start it:

    sudo systemctl daemon-reload
    sudo systemctl enable gunicorn
    sudo service gunicorn start
    sudo service gunicorn status
    
  5. After a code change: sudo service gunicorn reload (the same HUP as section 9). After changing the unit file: sudo systemctl daemon-reload, then sudo service gunicorn restart.

  6. Read its logs with sudo journalctl -u gunicorn.

11. Several sites on one nginx

Why: One EC2 server can host several apps and sites. Separate server { } blocks keep them apart, so you don't pay for one server per site.

What: one nginx can serve many sites, each in its own server { } block.

  1. The first block in /etc/nginx/nginx.conf has server_name _;. The _ is a catch-all name: this default block answers requests whose host name matches no other block, for example a bare IP.
  2. Add one block per site, each with its own server_name and its own proxy_pass or root:

    server {
        listen 80;
        server_name api.example.com;
        location / {
            proxy_pass http://127.0.0.1:5000;
        }
    }
    
  3. A second Python app runs on another port, such as 127.0.0.1:5001, with its own venv and service.

  4. Run sudo nginx -t and sudo service nginx reload after each change.

12. When something goes wrong

  1. python: command not found: use python3.
  2. ModuleNotFoundError: No module named 'flask': activate the venv, then pip install -r requirements.txt.
  3. Permission denied during pip install: the folder was made with sudo; run sudo chown -R ec2-user:ec2-user ~/pythonapp, then pip install without sudo.
  4. Commands fail after SSH reconnects: cd ~/pythonapp and source myenv/bin/activate again.
  5. http://PUBLIC_IP:5000 does not open while testing: check host='0.0.0.0' and the port 5000 rule in the security group.
  6. The nginx welcome page instead of your app: the location / { proxy_pass ...; } block is missing or outside the server block, or nginx was not reloaded.
  7. 502 Bad Gateway: nginx works but gunicorn does not answer. Start gunicorn, or fix the port in proxy_pass.
  8. nginx -t says unexpected end of file, expecting "}": a closing } is missing. Add it, run sudo nginx -t, then reload.
  9. nginx -t says invalid number of arguments: a ; is missing at the end of the line it names.
  10. A new route gives 404: the old code is still loaded. Run kill -HUP <master PID> or sudo service gunicorn reload.
Python app on EC2: what you see, what to check python: command not found → Amazon Linux 2023 has python3: use python3 -V, python3 -m venv ModuleNotFoundError: No module named 'flask' → Activate the venv, then pip install -r requirements.txt Permission denied during pip install → Folder made with sudo: sudo chown -R ec2-user:ec2-user ~/pythonapp Commands fail after SSH reconnects → cd ~/pythonapp, then source myenv/bin/activate again http://IP:5000 does not open → Testing only: bound to 0.0.0.0? Port 5000 in the security group? "Welcome to nginx" instead of the app → location / { proxy_pass ...; } missing, or nginx not reloaded 502 Bad Gateway → gunicorn is not running, or proxy_pass has the wrong port nginx -t: unexpected end of file, expecting "}" → A closing } is missing: add it, nginx -t, reload nginx -t: invalid number of arguments → A ; is missing at the end of the line it names A new route gives 404 → Old code still loaded: kill -HUP &lt;master PID&gt; or service reload Logs: sudo tail /var/log/nginx/error.log, and sudo journalctl -u gunicornfor the service

Figure 9. Python app on EC2: what you see and what to check, from python3 and the venv to the security group, proxy_pass, 502, nginx -t messages and reloading gunicorn. The logs are in /var/log/nginx/error.log and in journalctl -u gunicorn.

Ravindra Bagale's Tip

Keep three habits. Work inside a venv and never type sudo before pip. Run sudo nginx -t before every nginx reload, so a typo never takes the site down. And once nginx is in front, bind gunicorn to 127.0.0.1 and delete the port 5000 rule: the fewer ports open to the internet, the fewer ways in.

When you're done practising, stop the EC2 instance.

13. The whole class on one page

  1. A website returns whole HTML pages; an API returns data as JSON, key: value pairs in { }, and each app draws its own screen.
  2. Hosting Python needs python3, pip, a venv, the app, gunicorn (WSGI) and nginx. yum, pip, npm and composer are package managers.
  3. On Amazon Linux 2023: python3 -V, sudo yum install python3-pip -y. The command is python3, not python.
  4. python3 -m venv myenv, source myenv/bin/activate, prompt (myenv). Never sudo pip; fix Permission denied with chown.
  5. requirements.txt lists the libraries; pip install -r requirements.txt; pip freeze pins versions.
  6. Flask: @app.route('/'), indentation matters, app.run(host='0.0.0.0', port=5000). The Flask server is for testing only.
  7. gunicorn --bind 127.0.0.1:5000 --workers 2 --daemon app:app; without --bind it uses 127.0.0.1:8000. Re-activate the venv after SSH reconnects.
  8. nginx is the reverse proxy: location / { proxy_pass http://127.0.0.1:5000; }, then sudo nginx -t and sudo service nginx reload. Workers with event loops, worker_connections 1024.
  9. Behind nginx, close port 5000 in the security group.
  10. New code needs a reload: ps aux | grep gunicorn, kill -HUP <master PID>; kill stops it, kill -9 only if stuck.
  11. The proper way: a systemd unit with the venv's gunicorn, systemctl enable, sudo service gunicorn start|reload|restart, journalctl -u gunicorn.
  12. server_name _; is the catch-all default block; add a server { } block per site.

14. Hands-on lab: a Flask app behind gunicorn and nginx

Why this lab. This lab builds the stack from the bottom up and tests each layer with curl before adding the next: Python, the virtual environment, Flask, gunicorn, then nginx in front.

  1. Check Python and install pip
    • Command: python3 -V && sudo yum install python3-pip -y
    • You should see: Python 3.x, then Complete!.
    • Check: pip3 -V prints a version.
    • Why: Amazon Linux ships python3 but not pip. On this server the command is python3, not python.
  2. Create the app folder and a virtual environment
    • Command: mkdir ~/pythonapp && cd ~/pythonapp && python3 -m venv myenv && source myenv/bin/activate
    • You should see: The prompt starts with (myenv).
    • Check: which python → /home/ec2-user/pythonapp/myenv/bin/python
    • Why: Each app gets its own libraries, so two apps needing different Flask versions don't clash.
  3. Write requirements.txt and app.py
    • Do: nano requirements.txt with the two lines Flask and gunicorn; nano app.py with the two-route app from section 5.
    • Check: ls → app.py myenv requirements.txt
    • Why: requirements.txt lets anyone install exactly these libraries with one command.
  4. Install the libraries
    • Command: pip install -r requirements.txt
    • You should see: Download lines ending in Successfully installed ... flask ... gunicorn ....
    • Check: pip list shows Flask and gunicorn.
    • Why: Inside (myenv) they install into this app only, and no sudo is needed.
  5. Run it with gunicorn in the background
    • Command: gunicorn --bind 127.0.0.1:5000 --workers 2 --daemon app:app
    • You should see: Nothing.
    • Check: curl http://127.0.0.1:5000 → Hello from Python on EC2; curl http://127.0.0.1:5000/hi → Hi! This is the second page.
    • Why: app:app means the object app in app.py. 127.0.0.1 keeps port 5000 private, so only nginx on the same server can reach it; --daemon keeps it running after you log out.
  6. Install nginx as the reverse proxy
    • Command: sudo yum install nginx -y
    • You should see: Complete!
    • Check: nginx -v prints a version.
    • Why: Visitors use port 80. nginx takes their requests there and passes them to gunicorn on 5000.
  7. Add the proxy settings
    • Do: In the server { } block of /etc/nginx/nginx.conf, add the location / { proxy_pass http://127.0.0.1:5000; ... } block from section 8.
    • Check: sudo nginx -t → syntax is ok and test is successful.
    • Why: proxy_pass is the line that forwards each request. Testing before starting catches a missing semicolon.
  8. Start nginx and open the app
    • Command: sudo service nginx start && curl http://localhost
    • You should see: Hello from Python on EC2
    • Check: With HTTP allowed in the security group, http://PUBLIC_IP/hi shows the second page in a browser.
    • Why: The same answer on port 80 proves nginx forwards to gunicorn. Port 5000 never needs to be opened to the internet.
  9. Make it survive a reboot
    • Do: Create the gunicorn systemd service from section 10, then run sudo systemctl daemon-reload and sudo systemctl enable gunicorn.
    • Check: After a stop and start of the instance, curl http://localhost still answers (also sudo systemctl enable nginx).
    • Why: A --daemon gunicorn does not come back after a reboot or a crash; systemd restarts it for you. Stop the --daemon copy first so the two don't fight over port 5000.

15. Try at home

Try at home

Task 1: the app

  1. Launch an Amazon Linux 2023 instance, install pip, create ~/pythonapp with a venv, and write requirements.txt and app.py.
  2. Run python3 app.py once without the venv active and read the error. Then activate it, install, and open port 5000 temporarily.

Task 2: gunicorn and nginx

  1. Run the app with gunicorn in the background and test it with curl.
  2. Install nginx, add the location block, run sudo nginx -t, reload, and open the site on port 80.
  3. Remove one ;, run sudo nginx -t, and read the message. Put it back.
  4. Bind gunicorn to 127.0.0.1:5000 and delete the port 5000 rule.

Task 3: an API and a service

  1. Add /getusers, see the 404, then reload gunicorn with kill -HUP and see the JSON.
  2. Stop gunicorn and see the 502 Bad Gateway page.
  3. Create the systemd unit, enable it, reboot the instance, and check that the app answers without you starting anything.

Recap

In short

  1. An API sends data as JSON instead of whole pages, so every app can draw its own screen.
  2. A venv keeps each app's libraries apart; create it with python3 -m venv, and never use sudo pip inside it.
  3. The Flask server is for testing; gunicorn runs the app for real, and app:app names the file and the object.
  4. nginx in front is the reverse proxy; gunicorn binds 127.0.0.1:5000, and port 5000 stays closed.
  5. sudo nginx -t before every reload catches missing ; and }.
  6. New code needs kill -HUP on the master or sudo service gunicorn reload; a systemd service starts gunicorn at boot and restarts it after a crash.

Practice Tasks

About this scenario

Cape Town Safaris is a tourism business in Cape Town. Thabo Nkosi looks after its IT and cloud work. They count money in ZAR and run their AWS resources in the af-south-1 Region. In the tasks below you do real jobs for them using what this lesson teaches. All names, account IDs, IP addresses and numbers are made up for practice.

0 / 12

Easy Restart a service after a change

Why: Config changes only apply after the service restarts or reloads.

What to do: Restart the nginx service.

How:
  1. Find the Linux command that does this job.
  2. Add the options (like -r or -h) the task needs.
  3. Add the file names, folders or users in the right order.
  4. Use sudo if the job changes system files or users. Click Check answer.

Easy Check if a service is running

Why: When a site is down, first check whether the web server process is running.

What to do: Show the status of the httpd service.

How:
  1. Find the Linux command that does this job.
  2. Add the options (like -r or -h) the task needs.
  3. Add the file names, folders or users in the right order.
  4. Use sudo if the job changes system files or users. Click Check answer.

Easy Install a package on Ubuntu

Why: Ubuntu uses apt. Package names differ: on Ubuntu Apache is apache2.

What to do: Install the apache2 package on Ubuntu without being asked to confirm.

How:
  1. Find the Linux command that does this job.
  2. Add the options (like -r or -h) the task needs.
  3. Add the file names, folders or users in the right order.
  4. Use sudo if the job changes system files or users. Click Check answer.

Easy Install a package on Amazon Linux

Why: dnf is the package manager on Amazon Linux 2023. -y answers yes for you.

What to do: Install the httpd package on Amazon Linux 2023 without being asked to confirm.

How:
  1. Find the Linux command that does this job.
  2. Add the options (like -r or -h) the task needs.
  3. Add the file names, folders or users in the right order.
  4. Use sudo if the job changes system files or users. Click Check answer.

Easy Test a website from the server

Why: curl shows whether the web server answers, even before DNS is ready.

What to do: Request http://localhost and show only the response headers.

How:
  1. Find the Linux command that does this job.
  2. Add the options (like -r or -h) the task needs.
  3. Add the file names, folders or users in the right order.
  4. Use sudo if the job changes system files or users. Click Check answer.

Easy Download code from GitHub

Why: git clone copies the whole repository to the server, ready to deploy.

What to do: Clone https://github.com/capetownsafari/website.git into the current folder.

How:
  1. Find the Linux command that does this job.
  2. Add the options (like -r or -h) the task needs.
  3. Add the file names, folders or users in the right order.
  4. Use sudo if the job changes system files or users. Click Check answer.

Medium Test the nginx config

Why: A test catches typos before a restart takes your site down.

What to do: Check the nginx configuration files for errors.

How:
  1. Find the Linux command that does this job.
  2. Add the options (like -r or -h) the task needs.
  3. Add the file names, folders or users in the right order.
  4. Use sudo if the job changes system files or users. Click Check answer.

Medium Start a service now and at boot

Why: enable makes it start after reboot; --now also starts it right away.

What to do: Enable the nginx service and start it immediately, in one command.

How:
  1. Find the Linux command that does this job.
  2. Add the options (like -r or -h) the task needs.
  3. Add the file names, folders or users in the right order.
  4. Use sudo if the job changes system files or users. Click Check answer.

Medium Open port 80 to the world

Why: Visitors can reach your website only if the security group allows HTTP.

What to do: Allow inbound TCP port 80 from 0.0.0.0/0 on security group sg-022b36ab6b244c3b2.

How:
  1. Read the task and decide which AWS service it is about.
  2. Write aws, then the service name, then the operation (the action word).
  3. Add each needed flag (--name) followed by its value from the task.
  4. Flags can be in any order. Click Check answer.

Medium CloudFormation: web security group

Why: Opening only 443 keeps the server safer.

What to do: Template with an AWS::EC2::SecurityGroup that allows TCP 443 from 0.0.0.0/0. Add a GroupDescription.

How:
  1. Start from the top-level keys the task names.
  2. Indent child keys with 2 spaces (no tabs).
  3. Fill in each property with the value from the task.
  4. Click Check answer. Key order does not matter.

Medium Create a security group

Why: A security group is the firewall around your instance.

What to do: Create a security group named capetownsafari-web-sg with description "Web servers" in vpc-0f3cafcf24deadbee.

How:
  1. Read the task and decide which AWS service it is about.
  2. Write aws, then the service name, then the operation (the action word).
  3. Add each needed flag (--name) followed by its value from the task.
  4. Flags can be in any order. Click Check answer.

Hard Enable an nginx site on Ubuntu

Why: On Ubuntu, a site is enabled by a symbolic link from sites-available to sites-enabled.

What to do: Create a symbolic link to /etc/nginx/sites-available/capetownsafari inside /etc/nginx/sites-enabled/.

How:
  1. Find the Linux command that does this job.
  2. Add the options (like -r or -h) the task needs.
  3. Add the file names, folders or users in the right order.
  4. Use sudo if the job changes system files or users. Click Check answer.

Revision, comparison and interview cards

Revise this lesson in a few minutes. Save the cards on your phone or download them as PNG files. The QR code on each card opens this lesson.

Revision card: Python virtual environment. Installing libraries with sudo pip mixes them with the system Python and can break yum. A virtual environment (venv) is a private folder of Python libraries for one app. Create it, activate it, then pip install from requirements.txt. Setup:
Revision card: Python virtual environment · Download PNG
Revision card: Run Flask with gunicorn. The Flask development server is slow, single-user and stops when SSH closes. gunicorn is a production server for Python apps. app:app means file app.py, object app. Bind it to 127.0.0.1 so only Nginx can reach it, and run it in the background. Command:
Revision card: Run Flask with gunicorn · Download PNG
Revision card: Nginx as a reverse proxy for Flask. gunicorn should not face the internet, and users expect port 80. A reverse proxy receives requests on 80 and passes them to the app on 5000. Add proxy_pass, nginx -t, reload, then remove the port 5000 SG rule. Location:
Revision card: Nginx as a reverse proxy for Flask · Download PNG
Revision card: A systemd service for gunicorn. After a reboot, a gunicorn started by hand is gone and the site shows 502. A systemd service is a small file that tells Linux how to start, stop and restart a program. Create /etc/systemd/system/flask.service, then enable --now. Commands:
Revision card: A systemd service for gunicorn · Download PNG
Comparison: Website vs API. Website: Pages made for people to read in a browser. API: Data made for other programs to use.
Comparison: Website vs API · Download PNG
Comparison: Flask dev server vs gunicorn. Flask dev server: The built-in server you start with flask run. gunicorn: A production WSGI server that runs Flask with many workers.
Comparison: Flask dev server vs gunicorn · Download PNG
Interview card: Why is sudo pip install a mistake?. It writes into the system Python that yum and AWS tools depend on. A wrong version can break system commands. Use a venv instead:
Interview card: Why is sudo pip install a mistake? · Download PNG
Interview card: What does app:app mean in gunicorn?. Module app (file app.py) : object app (the Flask instance). gunicorn must know which file and which object to run. main.py with server = Flask(__name__) › main:server.
Interview card: What does app:app mean in gunicorn? · Download PNG
Interview card: Why bind gunicorn to 127.0.0.1?. So only programs on the same server, like Nginx, can reach it. 0.0.0.0 would expose the app port to the internet. Bind and close port 5000 in the SG.
Interview card: Why bind gunicorn to 127.0.0.1? · Download PNG
Interview card: How do you reload gunicorn after a code change?. Send HUP to the master process, or restart its systemd service. Workers restart with the new code without dropping the port. Commands:
Interview card: How do you reload gunicorn after a code change? · Download PNG

More cards: Ravi Sir's Cards.