Ravindra BagaleCourses & study guides

Chapter 10: Dynamic Website Hosting (PHP, Python, Node.js with MySQL)

10.4 Python (Flask) with MySQL

Ekdum simple aahe — the pattern is the same as PHP, only the app server changes. Type these commands with me.

Install Python tools

# Amazon Linux 2023 / CentOS Stream 9
sudo yum install -y python3 python3-pip git
# Ubuntu
sudo apt install -y python3 python3-venv python3-pip git

Create the app in /opt/flaskapp

We keep apps in /opt/<app> owned by the login user. (On CentOS, SELinux may block systemd from running programs stored inside home directories, so /opt avoids that problem on every OS.)

sudo mkdir -p /opt/flaskapp && sudo chown $USER:$USER /opt/flaskapp
cd /opt/flaskapp
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install flask pymysql cryptography gunicorn
pip freeze > requirements.txt
cat > /opt/flaskapp/.env <<'EOF'
DB_HOST=127.0.0.1
DB_USER=appuser
DB_PASS=App@Pass123
DB_NAME=appdb
EOF
chmod 600 /opt/flaskapp/.env
cat > /opt/flaskapp/app.py <<'EOF'
import os
import pymysql
from flask import Flask, jsonify, redirect, render_template_string, request

app = Flask(__name__)

def get_db():
    return pymysql.connect(
        host=os.getenv("DB_HOST", "127.0.0.1"),
        user=os.getenv("DB_USER", "appuser"),
        password=os.getenv("DB_PASS", ""),
        database=os.getenv("DB_NAME", "appdb"),
        cursorclass=pymysql.cursors.DictCursor,
    )

PAGE = """
<!DOCTYPE html><html><head><meta charset="utf-8"><title>Flask + MySQL</title></head>
<body style="font-family:Arial;max-width:700px;margin:30px auto">
<h1>Students (Flask + MySQL)</h1>
<table border="1" cellpadding="6"><tr><th>ID</th><th>Name</th><th>City</th></tr>
{% for s in rows %}<tr><td>{{ s.id }}</td><td>{{ s.name }}</td><td>{{ s.city }}</td></tr>{% endfor %}
</table>
<h3>Add student</h3>
<form method="post" action="/add">
  <input name="name" placeholder="Name" required>
  <input name="city" placeholder="City" required>
  <button>Add</button>
</form>
</body></html>
"""

@app.route("/")
def index():
    conn = get_db()
    with conn, conn.cursor() as cur:
        cur.execute("SELECT id, name, city FROM students ORDER BY id")
        rows = cur.fetchall()
    return render_template_string(PAGE, rows=rows)

@app.route("/add", methods=["POST"])
def add():
    conn = get_db()
    with conn, conn.cursor() as cur:
        cur.execute("INSERT INTO students (name, city) VALUES (%s, %s)",
                    (request.form["name"], request.form["city"]))
        conn.commit()
    return redirect("/")

@app.route("/api/students")
def api_students():
    conn = get_db()
    with conn, conn.cursor() as cur:
        cur.execute("SELECT id, name, city FROM students")
        return jsonify(cur.fetchall())

@app.route("/health")
def health():
    return {"status": "ok"}

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=False)
EOF

Test it manually

cd /opt/flaskapp && source venv/bin/activate
set -a && source .env && set +a            # load variables from .env into this shell
gunicorn --bind 127.0.0.1:5000 app:app &   # start in background for a quick test
sleep 2
curl -s http://127.0.0.1:5000/api/students
kill %1                                     # stop the test server
deactivate

Run with Gunicorn as a systemd service

sudo tee /etc/systemd/system/flaskapp.service > /dev/null <<EOF
[Unit]
Description=Flask app served by Gunicorn
After=network.target mariadb.service mysql.service

[Service]
User=$USER
Group=$USER
WorkingDirectory=/opt/flaskapp
EnvironmentFile=/opt/flaskapp/.env
ExecStart=/opt/flaskapp/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:5000 app:app
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo service flaskapp start
sudo systemctl enable flaskapp
sudo service flaskapp status
curl -s http://127.0.0.1:5000/health

(Here the here-doc delimiter EOF is not quoted, so $USER is replaced by your user name — ec2-user or ubuntu.)