Introduce Gunicorn production runner

Switch the web UI from Werkzeug's built-in server to Gunicorn for a more stable production deployment path.

Keep a separate dev config so local runs still reload quickly, while the production path uses a dedicated WSGI entrypoint and cleaner startup behavior.

The main motivation is to reduce the websocket teardown noise and make the server behavior more predictable under the app's mostly background-driven workload.
This commit is contained in:
Antti Kettunen 2026-04-18 10:39:33 +03:00
parent d688e7fa15
commit 14bc9b6fad
7 changed files with 144 additions and 88 deletions

View file

@ -90,4 +90,4 @@ ENV UMASK=022
# Set entrypoint and default command # Set entrypoint and default command
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]
CMD ["python", "web_server.py"] CMD ["gunicorn", "-c", "gunicorn.conf.py", "wsgi:application"]

View file

@ -220,7 +220,7 @@ PUID/PGID are exposed in the template — set them to match your Unraid permissi
git clone https://github.com/Nezreka/SoulSync git clone https://github.com/Nezreka/SoulSync
cd SoulSync cd SoulSync
pip install -r requirements.txt pip install -r requirements.txt
python web_server.py gunicorn -c gunicorn.conf.py wsgi:application
# Open http://localhost:8008 # Open http://localhost:8008
``` ```
@ -229,6 +229,7 @@ For local development and tests:
```bash ```bash
pip install -r requirements-dev.txt pip install -r requirements-dev.txt
pytest pytest
gunicorn -c gunicorn.dev.conf.py wsgi:application
``` ```
--- ---

14
gunicorn.conf.py Normal file
View file

@ -0,0 +1,14 @@
"""Gunicorn configuration for production deployments."""
bind = "0.0.0.0:8008"
worker_class = "gthread"
workers = 1
threads = 8
# Keep requests from hanging forever on slow external services.
timeout = 120
# Logging goes to stdout/stderr so Docker can collect it.
accesslog = "-"
errorlog = "-"
loglevel = "info"

15
gunicorn.dev.conf.py Normal file
View file

@ -0,0 +1,15 @@
"""Gunicorn configuration for local development."""
bind = "127.0.0.1:8008"
worker_class = "gthread"
workers = 1
threads = 4
reload = True
# Keep requests from hanging forever on slow external services.
timeout = 120
# Logging goes to stdout/stderr so the shell launcher can collect it.
accesslog = "-"
errorlog = "-"
loglevel = "info"

View file

@ -44,3 +44,5 @@ tidalapi>=0.7.6
# WebSocket server for real-time UI updates # WebSocket server for real-time UI updates
flask-socketio>=5.3.0 flask-socketio>=5.3.0
gunicorn>=25.3.0
simple-websocket>=1.1.0

View file

@ -199,6 +199,7 @@ def _set_profile_context():
g.profile_id = pid g.profile_id = pid
def get_current_profile_id() -> int: def get_current_profile_id() -> int:
"""Get the current profile ID from Flask g context or default to 1""" """Get the current profile ID from Flask g context or default to 1"""
try: try:
@ -54174,10 +54175,19 @@ def _emit_repair_progress_loop():
# END WEBSOCKET HANDLERS # END WEBSOCKET HANDLERS
# ================================================================================================ # ================================================================================================
_runtime_start_lock = threading.Lock()
_runtime_started = False
if __name__ == '__main__':
print("Starting SoulSync Web UI Server...") def start_runtime_services():
print("Open your browser and navigate to http://127.0.0.1:8008") """Start one-time server background services for direct and WSGI launches."""
global _runtime_started
with _runtime_start_lock:
if _runtime_started:
return
print("Starting SoulSync runtime services...")
# Dump SOULSYNC_* env vars for diagnostics (helps debug Docker/Unraid env issues) # Dump SOULSYNC_* env vars for diagnostics (helps debug Docker/Unraid env issues)
_soulsync_env = {k: v for k, v in os.environ.items() if k.startswith('SOULSYNC_')} _soulsync_env = {k: v for k, v in os.environ.items() if k.startswith('SOULSYNC_')}
@ -54210,7 +54220,6 @@ if __name__ == '__main__':
start_import_suggestions_cache() start_import_suggestions_cache()
# Initialize app start time for uptime tracking # Initialize app start time for uptime tracking
import time
app.start_time = time.time() app.start_time = time.time()
# Register action handlers and start automation engine # Register action handlers and start automation engine
@ -54264,4 +54273,11 @@ if __name__ == '__main__':
socketio.start_background_task(_emit_rate_monitor_loop) socketio.start_background_task(_emit_rate_monitor_loop)
print("WebSocket emitters started (Phase 1-7: global/dashboard/enrichment/tools/sync/automations/repair + rate monitor)") print("WebSocket emitters started (Phase 1-7: global/dashboard/enrichment/tools/sync/automations/repair + rate monitor)")
_runtime_started = True
if __name__ == '__main__':
print("Starting SoulSync Web UI Server...")
print("Open your browser and navigate to http://127.0.0.1:8008")
start_runtime_services()
socketio.run(app, host='0.0.0.0', port=8008, debug=False, allow_unsafe_werkzeug=True) socketio.run(app, host='0.0.0.0', port=8008, debug=False, allow_unsafe_werkzeug=True)

8
wsgi.py Normal file
View file

@ -0,0 +1,8 @@
"""WSGI entrypoint for SoulSync production deployments."""
from web_server import app, start_runtime_services
start_runtime_services()
application = app