| name | flask-webapp-deployment |
| description | Deploy Flask web applications with nginx reverse proxy, systemd services, SQLite, and remote management APIs. Use when deploying Python web apps, creating admin panels, setting up reverse proxies, or building management APIs. |
| triggers | ["deploy flask app","nginx reverse proxy","create admin panel","web application deployment","systemd service","remote command api","sqlite database"] |
Flask Web Application Deployment
Complete guide for deploying Flask applications with production-ready configuration.
Architecture Overview
Client → Nginx (port 80/443) → Flask (port 8080) → SQLite
Step 1: Flask Application Structure
Basic App Template
from flask import Flask, request, jsonify, send_from_directory
import sqlite3
import os
app = Flask(__name__, static_folder='static')
DB = '/opt/myapp/data.db'
def get_db():
conn = sqlite3.connect(DB)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db()
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)''')
conn.commit()
conn.close()
init_db()
@app.route('/health')
def health():
return jsonify({'status': 'ok'})
@app.route('/api/data', methods=['GET', 'POST'])
def api_data():
if request.method == 'GET':
conn = get_db()
items = [dict(r) for r in conn.execute("SELECT * FROM users").fetchall()]
conn.close()
return jsonify({'items': items})
else:
data = request.json
conn = get_db()
conn.execute("INSERT INTO users(username) VALUES(?)", (data['username'],))
conn.commit()
conn.close()
return jsonify({'message': 'created'})
@app.route('/admin')
def admin_page():
return send_from_directory('admin', 'index.html')
@app.route('/admin/<path:filename>')
def admin_static(filename):
return send_from_directory('admin', filename)
@app.route('/download/<filename>')
def download(filename):
return send_from_directory('/opt/myapp/files', filename)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
Admin Panel HTML Template
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,sans-serif;background:#f0f2f5;min-height:100vh}
.login{max-width:360px;margin:100px auto;padding:30px;background:#fff;border-radius:16px;box-shadow:0 4px 20px rgba(0,0,0,.08)}
.header{background:linear-gradient(,,);:;: ;:flex;:space-between;:center}
{:flex;:;: solid ;:auto}
{: ;:pointer;:nowrap;:;:;: solid transparent}
{:;:;:}
{:;:;: auto}
{:;:;:;:;: (,,,.)}
,{: ;: solid ;:;:}
{:(,,);:;:none;:pointer}
🔐 Admin
Login
⚡ Dashboard
Logout
📊 Stats
📋 Data
Step 2: Nginx Reverse Proxy
Configuration File
# /etc/nginx/conf.d/myapp.conf
server {
listen 80;
server_name _;
location / {
proxy_pass http://127.0.0.1:8080;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
Apply Configuration
sudo nginx -t
sudo nginx -s reload
Step 3: Systemd Service
Service File
[Unit]
Description=My Flask Application
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python3 /opt/myapp/app.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Commands
sudo tee /etc/systemd/system/myapp.service > /dev/null << 'EOF'
[Unit]
Description=My Flask Application
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python3 /opt/myapp/app.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myapp
Step 4: Remote Command Execution API
Implementation
import subprocess
@app.route('/api/exec', methods=['POST'])
def api_exec():
data = request.json
token = data.get('token', '')
if token != 'your-secret-token':
return jsonify({'error': 'unauthorized'}), 401
cmd = data.get('cmd', '')
try:
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = proc.communicate(timeout=120)
return jsonify({
'stdout': stdout.decode('utf-8', errors='replace'),
'stderr': stderr.decode('utf-8', errors='replace'),
'code': proc.returncode
})
except Exception as e:
return jsonify({'error': str(e)}), 500
Usage
curl -X POST http://server/api/exec \
-H "Content-Type: application/json" \
-d '{"token":"your-secret-token","cmd":"ls -la"}'
Step 5: User Authentication System
Database Schema
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
balance INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS recharge_codes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
times INTEGER DEFAULT 100,
used_by TEXT,
used_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
API Endpoints
@app.route('/api/register', methods=['POST'])
def register():
d = request.json
u, p = d.get('username','').strip(), d.get('password','').strip()
if not u or not p:
return jsonify({'error':'Username and password required'}), 400
conn = get_db()
if conn.execute("SELECT 1 FROM users WHERE username=?",(u,)).fetchone():
conn.close()
return jsonify({'error':'Username exists'}), 400
conn.execute("INSERT INTO users(username,password) VALUES(?,?)",(u,p))
conn.commit()
conn.close()
return jsonify({'message':'Registered','username':u})
@app.route('/api/login', methods=['POST'])
def login():
d = request.json
u, p = d.get('username','').strip(), d.get('password','').strip()
conn = get_db()
row = conn.execute("SELECT * FROM users WHERE username=? AND password=?",(u,p)).fetchone()
conn.close()
if not row:
return jsonify({'error':'Invalid credentials'}), 401
return jsonify({:,:u,:row[]})
():
d = request.json
u = d.get(,).strip()
code = d.get(,).strip().upper()
conn = get_db()
rc = conn.execute(,(code,)).fetchone()
rc:
conn.close()
jsonify({:}),
conn.execute(,(rc[],u))
conn.execute(,(u,code))
conn.commit()
bal = conn.execute(,(u,)).fetchone()[]
conn.close()
jsonify({:,:rc[],:bal})
Step 6: Email Verification (SMTP)
When you need to send verification codes or notification emails:
import smtplib
from email.mime.text import MIMEText
import random, string
from datetime import datetime, timedelta
SMTP_SERVER = 'smtp.qq.com'
SMTP_PORT = 465
SMTP_USER = 'QQ号@qq.com'
SMTP_PASS = '授权码'
def send_email(to_addr, subject, body):
try:
smtp = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT)
smtp.login(SMTP_USER, SMTP_PASS)
msg = MIMEText(body, 'plain', 'utf-8')
msg['Subject'] = subject
msg['From'] = SMTP_USER
msg['To'] = to_addr
smtp.sendmail(SMTP_USER, [to_addr], msg.as_string())
smtp.quit()
return True
except Exception as e:
print(f'发送邮件失败: {e}')
return False
@app.route('/api/forgot-password', methods=['POST'])
def forgot_password():
email = request.json.get('email', '').strip()
code = ''.join(random.choices(string.digits, k=6))
expires_at = datetime.now() + timedelta(minutes=10)
conn.execute("INSERT INTO password_resets(email, code, expires_at) VALUES(?, ?, ?)",
(email, code, expires_at.isoformat()))
conn.commit()
email_body =
send_email(email, , email_body):
jsonify({: })
:
jsonify({: }),
Important: Never return the verification code in the API response (was common in testing, breaks security in production).
Other SMTP servers: 163邮箱 (smtp.163.com:465), Gmail (smtp.gmail.com:587 TLS).
Step 7: Tampermonkey Userscript
Basic Template
(function() {
'use strict';
const API_BASE = 'http://your-server.com';
let username = GM_getValue('username', '');
function createUI() {
const panel = document.createElement('div');
panel.style.cssText = `
position: fixed; top: 10px; right: 10px; z-index: 99999;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; padding: 16px; border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3); font-size: 14px;
min-width: 280px; font-family: -apple-system, sans-serif;
`;
if (username) {
panel.innerHTML = `
<div style="font-weight:bold;font-size:16px;margin-bottom:12px">⚡ My App</div>
<div>👤 ${username}</div>
<button onclick="myapp.logout()" style="margin-top:8px;padding:8px 12px;background:rgba(255,255,255,0.2);border:none;color:white;border-radius:6px;cursor:pointer">Logout</button>
`;
} else {
panel. = ;
}
..(panel);
}
() {
({
: ,
: + path,
: { : },
: .(data),
: () {
(, .(res.));
},
: () {
(err, );
}
});
}
. = {
: () {
u = .()..();
p = .()..();
(, { : u, : p }, () {
(data && data.) {
username = u;
(, u);
location.();
} {
(data ? data. : );
}
});
},
: () {
(, );
location.();
}
};
();
})();
Step 8: Multi-Server Control via SSH Key Forwarding
When you need to control multiple servers from one API endpoint:
Setup SSH Keys
ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N "" -q
echo "$(cat /root/.ssh/id_ed25519.pub)" >> /root/.ssh/authorized_keys
cat >> /root/.ssh/config << 'EOF'
Host hz-server
HostName TARGET_IP
User root
IdentityFile /root/.ssh/id_ed25519
StrictHostKeyChecking no
EOF
chmod 600 /root/.ssh/config
Execute Remote Commands via API
curl -X POST http://server/api/exec \
-H "Content-Type: application/json" \
-d '{"token":"your-token","cmd":"hostname"}'
curl -X POST http://server/api/exec \
-H "Content-Type: application/json" \
-d '{"token":"your-token","cmd":"ssh hz-server \"hostname && uptime\""}'
Security: Restrict API Port to Specific IPs
In Alibaba Cloud security group, create rule:
- Protocol: Custom TCP
- Port: 9090/9090 (or your API port)
- Authorization object: YOUR_IP/32
- This ensures only your IP can access the management API
Step 9: User-Facing Frontend (Separate from Admin)
When you need both an admin panel AND a user-facing site:
@app.route('/admin')
def admin_page():
return send_from_directory('admin', 'index.html')
@app.route('/user')
def user_page():
return send_from_directory('user', 'index.html')
Keep them in separate directories (admin/ and user/) with separate HTML files.
Step 9: Announcement & Price Management
For SaaS-style apps with announcements and pricing:
conn.execute('''CREATE TABLE IF NOT EXISTS announcements (
id INTEGER PRIMARY KEY,
title TEXT,
content TEXT
)''')
conn.execute('''CREATE TABLE IF NOT EXISTS prices (
id INTEGER PRIMARY KEY,
times INTEGER,
price REAL
)''')
@app.route('/api/announcement')
def get_announcement():
conn = get_db()
row = conn.execute("SELECT title, content FROM announcements ORDER BY id DESC LIMIT 1").fetchone()
conn.close()
if row:
return jsonify({"title": row["title"], "content": row["content"]})
return jsonify({"title": "", "content": ""})
@app.route('/admin/api/announcement', methods=['GET', 'POST'])
def admin_announcement():
conn = get_db()
if request.method == "POST":
d = request.json
conn.execute("DELETE FROM announcements")
conn.execute("INSERT INTO announcements(title, content) VALUES(?, ?)", (d["title"], d["content"]))
conn.commit(); conn.close()
return jsonify({"message": "公告已更新"})
else:
row = conn.execute("SELECT title, content FROM announcements ORDER BY id DESC LIMIT 1").fetchone()
conn.close()
return jsonify({: row[], : row[]} row {: , : })
Common Pitfalls
1. Port Already in Use
sudo ss -tlnp | grep :8080
sudo kill -9 $(fuser 8080/tcp 2>/dev/null)
ps aux | grep "python3 app.py" | grep -v grep
sudo kill -9 <PID>
2. Nginx Configuration Conflicts
sudo nginx -t 2>&1 | grep conflicting
sudo rm /etc/nginx/conf.d/default.conf
3. SQLite Database Locked
fuser /opt/myapp/data.db
PRAGMA journal_mode=WAL;
⚠️ Pitfall: Flask Route Ordering (CRITICAL)
Routes must be defined at module level, NOT inside if __name__ == '__main__':. The if block only runs when executed directly, not when imported by systemd/gunicorn. Always put all @app.route decorators before the if block.
Symptoms: Routes return 404 even though they exist in the file. No error in logs. Flask starts normally.
Detection: grep -n "@app.route" app.py — if any route appears AFTER if __name__, it's broken.
Fix: Move all routes before if __name__. When appending routes to an existing file, always insert BEFORE the if __name__ block, not after.
Common mistake: Using cat >> app.py << 'EOF' to add new routes — this appends AFTER if __name__. Instead, use sed -i '/if __name__/i \新路由代码' app.py or rewrite the file.
Pitfall: Duplicate Route Definitions
When patching app.py multiple times, duplicate @app.route('/path') definitions cause silent failures. Always check with:
grep -c "@app.route('/user')" app.py
🔴 Pitfall: File Corruption When Patching Python
When using find-and-replace on Python files (patch tool, sed, etc.), replacements that span function boundaries can corrupt the file. Common scenario: replacing a function CALL that sits adjacent to a function DEFINITION destroys the colon, indentation, or body.
Example of corruption:
def init_db():
conn = get_db()
...
init_db()
def init_db()
conn = get_db()
Prevention: For complex multi-line changes, rewrite the entire file rather than surgical replacements. Always verify syntax after any patch:
python3 -c "import py_compile; py_compile.compile('app.py', doraise=True)"
Recovery: If corrupted, either restore from backup (cp app.py.bak app.py) or rewrite the file completely.
Pitfall: File Download Route
When serving userscripts or static files through Flask, add explicit routes with correct MIME types:
@app.route('/chaoxing.user.js')
def download_script():
return send_from_directory('/opt/app', 'chaoxing.user.js', mimetype='application/javascript')
Do NOT rely on nginx to serve these — Flask's send_from_directory is more reliable when the file is in the app directory.
5. Systemd Service Won't Start
sudo journalctl -u myapp -f
ls -la /opt/myapp/app.py
Security Checklist
Quick Deploy Script
#!/bin/bash
APP_DIR="/opt/myapp"
APP_NAME="myapp"
mkdir -p $APP_DIR
cp app.py $APP_DIR/
cp -r admin $APP_DIR/
sudo tee /etc/systemd/system/$APP_NAME.service > /dev/null << EOF
[Unit]
Description=My Flask Application
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=$APP_DIR
ExecStart=/usr/bin/python3 $APP_DIR/app.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo tee /etc/nginx/conf.d/$APP_NAME.conf > /dev/null << 'EOF'
server {
listen 80;
server_name _;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
EOF
sudo systemctl daemon-reload
sudo systemctl enable $APP_NAME
sudo systemctl start $APP_NAME
sudo nginx -t && sudo nginx -s reload
echo "✅ Deployed! Access at http://your-server/admin"