Synology Web Station: Hosting a Python Application
I had a small Flask script that needed a home, and Web Station didn't want it. It's built around PHP: you pick Apache or nginx, a PHP version, and it serves the web share. No WSGI module, no Python option in the virtual host dialog. Still, it's easy enough to run the app under the Python3 package on a local port and let Web Station's nginx proxy a virtual host to it.
I use this for internal dashboards, small APIs and webhook receivers that don't deserve a Docker container. You need Web Station (Nginx back end), the Python3 package (3.5 right now), and SSH with a sudo-capable account.
1. Virtual environment
The system Python from the package lives in /usr/local/bin/python3. Do not install libraries into it globally; create a venv inside the web share where you will keep the application:
sudo mkdir -p /volume1/web/myapp
sudo chown $(whoami) /volume1/web/myapp
cd /volume1/web/myapp
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install flask gunicorn
ensurepip is included in the package, so venv produces a working pip.
2. A minimal app
cat > /volume1/web/myapp/app.py <<'EOF'
from flask import Flask, jsonify
import socket
app = Flask(__name__)
@app.route('/')
def index():
return jsonify(host=socket.gethostname(), status='ok')
EOF
Test it once in the foreground, listening only on localhost:
/volume1/web/myapp/venv/bin/gunicorn --bind 127.0.0.1:8000 --workers 2 app:app
curl http://127.0.0.1:8000/ from a second SSH session should return the JSON. Stop it with Ctrl+C.
3. Start at boot with Task Scheduler
DSM does not let you drop unit files into systemd, but Task Scheduler runs scripts at boot. Control Panel → Task Scheduler → Create → Triggered Task → User-defined script:
Task: myapp-gunicorn
User: root
Event: Boot-up
Script:
#!/bin/sh
cd /volume1/web/myapp
exec /volume1/web/myapp/venv/bin/gunicorn \
--bind 127.0.0.1:8000 \
--workers 2 \
--user http --group http \
--access-logfile /volume1/web/myapp/access.log \
--error-logfile /volume1/web/myapp/error.log \
--daemon app:app
Running as http (the account Web Station uses) keeps the file permissions simple. Click Run once from Task Scheduler to start it without rebooting.
4. Virtual host in Web Station
Web Station → Virtual Host → Create:
Hostname: myapp.example.com (or port-based: 8080)
Document root: web/myapp
HTTP back-end: Nginx
PHP: Disabled
Web Station generates its own nginx server block but only serves static files from the document root. To forward to gunicorn, add a user config file, which Web Station's nginx includes for each virtual host. Find the generated config name first:
ls /usr/local/etc/nginx/sites-enabled/
You will see a file such as myapp.example.com.conf containing an include /usr/local/etc/nginx/conf.d/<uuid>/user.conf*; line. Create the user config in that directory:
# /usr/local/etc/nginx/conf.d/<uuid>/user.conf
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
Then reload:
sudo nginx -t
sudo synoservicecfg --reload nginx
This file survives Web Station changes and DSM updates, unlike edits to the generated server block. I lost a hand-edited block once, so I don't touch those anymore.
5. HTTPS
Control Panel → Security → Certificate → Add a Let's Encrypt certificate for myapp.example.com, then Configure and assign it to the virtual host. Web Station virtual hosts appear in that list once created.
Checking it
curl -s https://myapp.example.com/
tail -n 5 /volume1/web/myapp/access.log
The JSON response should come back with the NAS hostname, and the gunicorn access log should show the request with the real client IP in the X-Forwarded-For header position. After a reboot, ps aux | grep gunicorn should list the master and two workers.
Common failures
- Browser shows "502 Bad Gateway": gunicorn is not running or is bound to a different port. Check
error.logandnetstat -tlnp | grep 8000. python3: command not foundin the Task Scheduler script: the boot-time PATH is minimal. Use full paths as in the script above.ModuleNotFoundError: No module named 'flask': you called the system interpreter instead of the venv's gunicorn; the venv is not activated inside scheduled tasks.- The virtual host serves a directory listing or "403 Forbidden" instead of proxying: the
user.conflanded in the wrong<uuid>directory. Match it against theincludeline insites-enabled.
It's a bit of a hack. But for one or two small Python services it has been stable for me, and I like that it reuses DSM's nginx and certificates.