gunicorn
This commit is contained in:
@@ -6,6 +6,7 @@ PYTORRENT_DEBUG=0
|
||||
PYTORRENT_POLL_INTERVAL=1.0
|
||||
PYTORRENT_WORKERS=16
|
||||
PYTORRENT_GEOIP_DB=data/GeoLite2-City.mmdb
|
||||
PYTORRENT_ALLOW_UNSAFE_WERKZEUG=0
|
||||
|
||||
# Retention / Smart Queue
|
||||
PYTORRENT_TRAFFIC_HISTORY_RETENTION_DAYS=90
|
||||
|
||||
17
README.md
17
README.md
@@ -37,6 +37,23 @@ python app.py
|
||||
|
||||
Domyślnie: `http://127.0.0.1:8090`.
|
||||
|
||||
## Uruchomienie produkcyjne
|
||||
|
||||
Preferowany wariant bez deweloperskiego Werkzeug:
|
||||
|
||||
```bash
|
||||
. venv/bin/activate
|
||||
gunicorn --worker-class gthread --workers 1 --threads 32 --bind 0.0.0.0:8090 --access-logfile - --error-logfile - wsgi:app
|
||||
```
|
||||
|
||||
Note: aplikacja zostaje przy `async_mode="threading"`, więc WebSocket, `start_background_task`, kolejka operacji i poller działają w tym samym modelu co wcześniej.
|
||||
|
||||
Alternatywy przeanalizowane, ale nie wdrożone:
|
||||
|
||||
- `eventlet` przez Gunicorn: działa z Flask-SocketIO, ale wymaga green threads i monkey-patching; większe ryzyko regresji dla operacji plikowych/SCGI.
|
||||
- `gevent` przez Gunicorn: dobry wariant produkcyjny, ale wymaga dodatkowych zależności i testów zgodności.
|
||||
- wiele workerów Gunicorn: wymaga Redis/RabbitMQ/Kafka jako message queue dla Socket.IO, więc nie jest zamiennikiem 1:1.
|
||||
|
||||
## Profil SCGI
|
||||
|
||||
Przykład:
|
||||
|
||||
11
app.py
11
app.py
@@ -1,7 +1,14 @@
|
||||
from pytorrent import create_app, socketio
|
||||
from pytorrent.config import HOST, PORT, DEBUG
|
||||
from pytorrent.config import ALLOW_UNSAFE_WERKZEUG, DEBUG, HOST, PORT
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == "__main__":
|
||||
socketio.run(app, host=HOST, port=PORT, debug=DEBUG, allow_unsafe_werkzeug=True)
|
||||
# Note: This entrypoint is kept for local development; production should use gunicorn via wsgi:app.
|
||||
socketio.run(
|
||||
app,
|
||||
host=HOST,
|
||||
port=PORT,
|
||||
debug=DEBUG,
|
||||
allow_unsafe_werkzeug=ALLOW_UNSAFE_WERKZEUG,
|
||||
)
|
||||
|
||||
@@ -8,20 +8,18 @@ Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
#User=root
|
||||
#Group=root
|
||||
User=pytorrent
|
||||
Group=pytorrent
|
||||
WorkingDirectory=/opt/pyTorrent
|
||||
Environment="PYTHONUNBUFFERED=1"
|
||||
EnvironmentFile=/opt/pyTorrent/.env
|
||||
ExecStart=/opt/pyTorrent/venv/bin/python /opt/pyTorrent/app.py
|
||||
# Note: production starts through Gunicorn, so Socket.IO keeps WebSocket/thread support without unsafe Werkzeug.
|
||||
ExecStart=/opt/pyTorrent/venv/bin/gunicorn --worker-class gthread --workers 1 --threads 32 --bind ${PYTORRENT_HOST}:${PYTORRENT_PORT} --access-logfile - --error-logfile - wsgi:app
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
KillSignal=SIGINT
|
||||
TimeoutStopSec=20
|
||||
|
||||
# opcjonalnie
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
|
||||
|
||||
@@ -7,6 +7,14 @@ from dotenv import load_dotenv
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
load_dotenv(BASE_DIR / ".env")
|
||||
|
||||
|
||||
def _env_bool(name: str, default: bool = False) -> bool:
|
||||
value = os.getenv(name)
|
||||
if value is None:
|
||||
return default
|
||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
SECRET_KEY = os.getenv("PYTORRENT_SECRET_KEY", "dev-change-me")
|
||||
DB_PATH = Path(os.getenv("PYTORRENT_DB_PATH", str(BASE_DIR / "data" / "pytorrent.sqlite3")))
|
||||
if not DB_PATH.is_absolute():
|
||||
@@ -14,7 +22,9 @@ if not DB_PATH.is_absolute():
|
||||
|
||||
HOST = os.getenv("PYTORRENT_HOST", "0.0.0.0")
|
||||
PORT = int(os.getenv("PYTORRENT_PORT", "8090"))
|
||||
DEBUG = os.getenv("PYTORRENT_DEBUG", "0") == "1"
|
||||
DEBUG = _env_bool("PYTORRENT_DEBUG", False)
|
||||
# Note: Keep Werkzeug opt-in only for explicit local/dev use, never by default in services.
|
||||
ALLOW_UNSAFE_WERKZEUG = _env_bool("PYTORRENT_ALLOW_UNSAFE_WERKZEUG", DEBUG)
|
||||
POLL_INTERVAL = float(os.getenv("PYTORRENT_POLL_INTERVAL", "1.0"))
|
||||
WORKERS = int(os.getenv("PYTORRENT_WORKERS", "16"))
|
||||
GEOIP_DB = Path(os.getenv("PYTORRENT_GEOIP_DB", str(BASE_DIR / "data" / "GeoLite2-City.mmdb")))
|
||||
|
||||
@@ -180,7 +180,13 @@ body {
|
||||
opacity: .78;
|
||||
}
|
||||
.shortcut { font-size: .78rem; color: var(--bs-secondary-color); padding: .15rem .5rem; }
|
||||
.content { min-width: 0; min-height: 0; display: grid; grid-template-rows: 1fr 255px; }
|
||||
.content {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr) 255px;
|
||||
position: relative;
|
||||
}
|
||||
.table-wrap { overflow: auto; contain: content; }
|
||||
.torrent-table { margin: 0; white-space: nowrap; table-layout: auto; }
|
||||
.torrent-table thead th { position: sticky; top: 0; z-index: 2; background: var(--bs-tertiary-bg); border-bottom: 1px solid var(--bs-border-color); user-select: none; }
|
||||
@@ -195,7 +201,13 @@ body {
|
||||
.virtual-spacer td { padding: 0 !important; border: 0 !important; }
|
||||
.empty { height: 120px; text-align: center; vertical-align: middle; color: var(--bs-secondary-color); }
|
||||
.progress.thin { height: 7px; min-width: 130px; margin-bottom: 1px; background: rgba(255,255,255,.08); }
|
||||
.details { min-height: 0; overflow: hidden; background: rgba(var(--bs-secondary-bg-rgb), .78); }
|
||||
.details {
|
||||
grid-row: 2;
|
||||
grid-column: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: rgba(var(--bs-secondary-bg-rgb), .78);
|
||||
}
|
||||
.detail-pane { height: 210px; overflow: auto; padding: .65rem; }
|
||||
.loading-line { display: flex; align-items: center; gap: .5rem; color: var(--bs-secondary-color); padding: .75rem; }
|
||||
.muted-pane { color: var(--bs-secondary-color); }
|
||||
@@ -373,10 +385,31 @@ body {
|
||||
background: rgba(var(--bs-secondary-bg-rgb), .85);
|
||||
}
|
||||
.badge-degraded { background: #f59e0b !important; color: #111 !important; }
|
||||
body.mobile-mode .table-wrap { display: none !important; }
|
||||
body.mobile-mode #mobileList { display: block !important; }
|
||||
body.mobile-mode .content { grid-template-rows: 1fr 210px; }
|
||||
/* Note: Manual mobile mode is defined once here; media queries below only adapt breakpoints. */
|
||||
body.mobile-mode .table-wrap,
|
||||
body.mobile-mode .details {
|
||||
display: none !important;
|
||||
}
|
||||
body.mobile-mode #mobileList {
|
||||
display: block !important;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
body.mobile-mode .content {
|
||||
display: grid !important;
|
||||
grid-template-rows: minmax(0, 1fr) !important;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
body.mobile-mode .torrent-table { display: none; }
|
||||
body.mobile-mode .main-grid {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.nav-btn span { display: none; }
|
||||
}
|
||||
@@ -437,7 +470,6 @@ body.mobile-mode .torrent-table { display: none; }
|
||||
opacity: .72;
|
||||
}
|
||||
.path-row::before{content:'\f07b';font-family:'Font Awesome 6 Free';font-weight:900;color:var(--bs-warning)}
|
||||
body.mobile-mode #mobileList{min-height:0;height:100%;overflow:auto;display:block!important}
|
||||
body.mobile-mode .mobile-card{display:block}.mobile-card .mobile-actions button{min-width:34px}
|
||||
#toolSmart .form-label{font-size:.75rem;color:var(--bs-secondary-color);margin-bottom:.2rem}
|
||||
#toolSmart .btn{padding:.25rem .55rem;border-radius:.5rem;white-space:nowrap}
|
||||
@@ -462,10 +494,6 @@ body.mobile-mode .mobile-card{display:block}.mobile-card .mobile-actions button{
|
||||
.empty-mini{padding:.7rem .8rem;border:1px dashed var(--bs-border-color);border-radius:.7rem;color:var(--bs-secondary-color);background:rgba(var(--bs-secondary-bg-rgb),.35)}
|
||||
.label-manager-row{display:flex;align-items:center;justify-content:space-between;gap:.5rem;border:1px solid var(--bs-border-color);border-radius:.65rem;padding:.4rem .5rem;margin-bottom:.4rem;background:rgba(var(--bs-secondary-bg-rgb),.35)}
|
||||
.tool-tab i{margin-right:.25rem;opacity:.82}
|
||||
body.mobile-mode .content{display:grid!important;grid-template-rows:minmax(0,1fr)!important;min-height:0;overflow:hidden}
|
||||
body.mobile-mode .details{display:none!important}
|
||||
body.mobile-mode #mobileList{display:block!important;height:100%!important;min-height:220px;overflow:auto;position:relative;z-index:2;padding-bottom:1rem}
|
||||
body.mobile-mode .main-grid{min-height:0;overflow:hidden}
|
||||
@media (max-width:640px){.history-card{padding:.5rem}#trafficHistoryChart,#trafficSpeedChart{height:320px}.statusbar{font-size:.75rem;gap:.6rem}.mobile-list{padding:.45rem}.mobile-card{margin-bottom:.45rem}}
|
||||
|
||||
/* Requested fixes: clean progress, mobile auto list, pagers, rTorrent config, peers refresh */
|
||||
@@ -473,7 +501,6 @@ body.mobile-mode .main-grid{min-height:0;overflow:hidden}
|
||||
.torrent-progress .progress-bar{min-width:0!important;position:relative;transition:width .25s ease,background-color .25s ease}
|
||||
.torrent-progress>span{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;line-height:1;color:var(--bs-body-color);text-shadow:none;white-space:nowrap;pointer-events:none}
|
||||
.torrent-progress .progress-bar+span{color:var(--bs-body-color)}
|
||||
body.mobile-mode #mobileList{display:block!important}
|
||||
@media (max-width:700px){
|
||||
body:not(.desktop-mode) .table-wrap{display:none!important}
|
||||
body:not(.desktop-mode) #mobileList{display:block!important;min-height:260px;height:100%;overflow:auto}
|
||||
@@ -560,14 +587,9 @@ body.mobile-mode #mobileList{display:block!important}
|
||||
padding: .75rem;
|
||||
background: var(--bs-tertiary-bg);
|
||||
}
|
||||
/* Stable main layout: bulk actions overlay the list area, details stay pinned at the bottom. */
|
||||
.content {
|
||||
position: relative;
|
||||
grid-template-rows: minmax(0, 1fr) 255px !important;
|
||||
}
|
||||
/* Note: Bulk actions overlay the list area; base .content/.details rules keep the layout pinned. */
|
||||
#bulkBar { grid-row: 1; grid-column: 1; align-self: start; }
|
||||
#tableWrap, #mobileList { grid-row: 1; grid-column: 1; min-height: 0; }
|
||||
.details { grid-row: 2; grid-column: 1; min-height: 0; }
|
||||
.bulk-bar:not(.d-none) + .table-wrap { padding-top: 38px; }
|
||||
@media (max-width: 900px) {
|
||||
.bulk-bar { gap: .3rem; }
|
||||
|
||||
@@ -4,3 +4,4 @@ python-dotenv>=1.0
|
||||
geoip2>=4.8
|
||||
psutil>=5.9
|
||||
simple-websocket>=1.0
|
||||
gunicorn>=22.0
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
[Unit]
|
||||
Description=pyTorrent web UI for rTorrent
|
||||
After=network.target
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/pytorrent
|
||||
Environment="PYTHONUNBUFFERED=1"
|
||||
EnvironmentFile=/opt/pytorrent/.env
|
||||
ExecStart=/opt/pytorrent/venv/bin/python /opt/pytorrent/app.py
|
||||
# Note: threaded Gunicorn preserves Flask-SocketIO background tasks without running Werkzeug in production.
|
||||
ExecStart=/opt/pytorrent/venv/bin/gunicorn --worker-class gthread --workers 1 --threads 32 --bind ${PYTORRENT_HOST}:${PYTORRENT_PORT} --access-logfile - --error-logfile - wsgi:app
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
KillSignal=SIGINT
|
||||
TimeoutStopSec=20
|
||||
User=www-data
|
||||
Group=www-data
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Reference in New Issue
Block a user