DB-유

Synology Web Station: Hosting a Python Application

· synology, python, web-station

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.log and netstat -tlnp | grep 8000.
  • python3: command not found in 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.conf landed in the wrong <uuid> directory. Match it against the include line in sites-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.