The X-Forwarded-For rewrite rule alone is not enough: waitress 2+ strips forwarded headers from untrusted proxies by default, so the app still saw 127.0.0.1 with the rule active. Trust the loopback proxy and consume x-forwarded-for on the waitress command line; waitress then rewrites remote_addr to the real client. Runbook gains the allowedServerVariables unlock (500.52) and both troubleshooting rows.
10 KiB
ShopDB - Windows + IIS install runbook
A step-by-step, tested install for a new site on Windows Server / Windows 11 with IIS in front of the Flask app (HttpPlatformHandler -> waitress), backed by MySQL. This runbook was validated end to end on a win11 + IIS + MySQL 5.6 box.
APP_ROOT below = the deploy folder, e.g. C:\shopdb-flask (where wsgi.py
lives). Run PowerShell as Administrator.
0. Prerequisites
| Need | Notes |
|---|---|
| Python 3.12 (64-bit) | python --version |
| IIS with HttpPlatformHandler | https://www.iis.net/downloads/microsoft/httpplatformhandler (direct MSI: download.microsoft.com/download/8/1/3/813AC4E6-9203-4F7A-8DD5-F3D54D10C5CD/httpPlatformHandler_amd64.msi) |
| MySQL 5.7+/8.0 (or 5.6 with the flags in step 1) | reachable from the app host |
| URL Rewrite (optional) | only for the real-client-IP rule; skip it and the app still runs |
The app itself pulls in waitress and tzdata from requirements.txt (step 4).
1. MySQL: flags (5.6 only) + database + user
On MySQL 5.6 only, add to my.ini/my.cnf under [mysqld] and restart MySQL
(5.7+/8.0 need none of this):
innodb_file_per_table = 1
innodb_file_format = Barracuda
innodb_large_prefix = 1
Without them, flask db upgrade fails with error 1071 ("key too long") - the
migrations use ROW_FORMAT=DYNAMIC, which needs the 3072-byte prefix these unlock.
Then create the database (utf8mb4) and an app user:
CREATE DATABASE shopdb_flask CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'shopdb'@'%' IDENTIFIED BY 'CHANGE_ME';
GRANT ALL PRIVILEGES ON shopdb_flask.* TO 'shopdb'@'%';
FLUSH PRIVILEGES;
2. Deploy the app files
Copy the release (the repo minus venv/, .git/, node_modules/,
frontend/src/) to APP_ROOT. It must contain wsgi.py, shopdb/, plugins/,
migrations/, requirements.txt, and the pre-built frontend/dist/.
3. Virtual env + dependencies
cd APP_ROOT
python -m venv venv
venv\Scripts\python -m pip install -r requirements.txt
This installs Flask, SQLAlchemy, PyMySQL, waitress (the WSGI server IIS launches) and tzdata (Windows has no IANA tz database; without it the notifications plugin fails with "No time zone found with key America/New_York").
4. Secrets + connection (.env)
Create APP_ROOT\.env (read by wsgi.py via load_dotenv()). Lock its ACLs to
the app-pool identity + admins.
FLASK_ENV=production
SECRET_KEY=<64+ random chars>
JWT_SECRET_KEY=<another 64+ random chars>
DATABASE_URL=mysql+pymysql://shopdb:CHANGE_ME@<mysql-host>:3306/shopdb_flask?charset=utf8mb4
CORS_ORIGINS=http://<the site's own hostname-or-ip:port>
Generate a key: venv\Scripts\python -c "import secrets;print(secrets.token_urlsafe(64))".
Production refuses to boot if any of SECRET_KEY, JWT_SECRET_KEY,
DATABASE_URL, CORS_ORIGINS is missing or a dev default.
5. Preflight (catch problems before installing)
$env:FLASK_APP="shopdb"
venv\Scripts\flask db-utils preflight
Checks Python, required env, DB connectivity, and the MySQL 5.6 index flags, and prints exactly what to fix. Fix any FAIL before continuing.
6. Schema + data + plugins + admin
$env:FLASK_APP="shopdb"
venv\Scripts\flask db upgrade # creates every table (to head)
venv\Scripts\flask seed reference-data # statuses, machine/location/rel types
venv\Scripts\flask seed permissions
venv\Scripts\flask seed settings
# enable the plugins this site tracks (registry is empty on a fresh box).
# usb + employees install DISABLED by default - enable them later in the wizard
# if the site wants those (they create extra tables).
foreach ($p in "computers","equipment","network","notifications","printers","knowledgebase","slides","warranty") {
venv\Scripts\flask plugin install $p
}
# first admin (password generated + printed once - store it):
venv\Scripts\flask seed admin --username admin --email admin@yourfacility.example.com
Prefer no CLI? Skip
seed admin(and even the seed steps): start the site, and the login page offers to create the first admin on a fresh instance, then the setup wizard can seed reference data. Either path works.
7. IIS site
Two supported deployment methods:
- Method A - own site (recommended, default): the app gets its own IIS site, port (or hostname), app pool, and venv. Steps 1-5 below.
- Method B - subpath under an existing site: the app runs as an IIS
Application (e.g.
/ops) under a site you already have (such as the classic ASP site or Default Web Site), so it shares that site's binding and TLS cert:https://<host>/ops/. Do steps 1-4 below, then follow 7b instead of step 5.
- Copy
deploy\windows\web.configtoAPP_ROOT\web.config. IfAPP_ROOTis notC:\shopdb-flask, fix the paths inside it. CreateAPP_ROOT\logs. - Create an app pool with No Managed Code:
Import-Module WebAdministration New-WebAppPool -Name shopdbflask Set-ItemProperty IIS:\AppPools\shopdbflask -Name managedRuntimeVersion -Value "" - Grant the app-pool identity access:
icacls APP_ROOT /grant "IIS AppPool\shopdbflask:(OI)(CI)RX" /T icacls APP_ROOT\logs /grant "IIS AppPool\shopdbflask:(OI)(CI)M" /T - Unlock the handler sections (locked server-wide by default; without this
IIS returns HTTP 500.19):
%windir%\system32\inetsrv\appcmd unlock config /section:system.webServer/handlers %windir%\system32\inetsrv\appcmd unlock config /section:system.webServer/httpPlatform - Create the site (own port; the classic ASP site can keep 8080):
New-Website -Name shopdb-flask -Port 8090 -PhysicalPath APP_ROOT -ApplicationPool shopdbflask New-NetFirewallRule -DisplayName "shopdb-flask 8090" -Direction Inbound -Protocol TCP -LocalPort 8090 -Action Allow Start-Website shopdb-flask
IIS launches waitress-serve --port=%HTTP_PLATFORM_PORT% wsgi:app per the
web.config and reverse-proxies the site port to it. First request takes ~15s
(the app boots + connects to MySQL).
7b. Method B: subpath under an existing site
The mount path must match in three places: the IIS Application alias, the
MOUNT_PATH the backend sees, and the VITE_BASE_PATH the frontend was built
with. /ops is the example throughout; any alias works.
- Rebuild the frontend for the subpath (on the dev box, then copy
dist):cd frontend && VITE_BASE_PATH=/ops/ npm run build # note the trailing slash - Create the Application under the existing site (instead of
New-Website):New-WebApplication -Site "Default Web Site" -Name ops -PhysicalPath APP_ROOT -ApplicationPool shopdbflask - Tell the backend its mount path: in
APP_ROOT\web.config, uncomment theMOUNT_PATHenvironment variable (value/ops), or setMOUNT_PATH=/opsinAPP_ROOT\.env.wsgi.pythen serves everything under the prefix (requests outside it get a plain 404 naming the mount). - Recycle the app pool. The app is at
http(s)://<host>/ops/and the API at/ops/api/....
The handler mappings in the app's web.config apply only inside the
Application, so the parent site's own handlers (classic ASP, static files)
are untouched. CORS_ORIGINS in .env is origin-only (scheme + host + port,
no path), so it is the same for both methods.
The
X-Forwarded-ForURL Rewrite rule in web.config is commented out by default. It needs the URL Rewrite module; with it active but the module absent, IIS returns 500.19. Install URL Rewrite, then uncomment the<rewrite>block, to record real client IPs in audit logs.Two companion requirements, or the app keeps seeing 127.0.0.1:
allowedServerVariablesis locked at server level by default (500.52 when the block activates) - unlock once withappcmd unlock config -section:system.webServer/rewrite/allowedServerVariables. And waitress 2+ strips X-Forwarded-For from untrusted proxies, so the waitressargumentsline must carry--trusted-proxy=127.0.0.1 --trusted-proxy-headers=x-forwarded-for(the shipped web.config already does).
8. Smoke test + first run
(Invoke-WebRequest http://localhost:8090/ -UseBasicParsing).StatusCode # 200 (SPA)
Invoke-WebRequest http://localhost:8090/api/auth/login -Method POST `
-Body '{"username":"admin","password":"<the printed password>"}' `
-ContentType application/json -UseBasicParsing # 200 + token
Browse to http://<host>:8090, sign in as the admin, and the setup wizard
walks through site name, features (per-plugin: create tables here vs connect a DB),
floor-map upload, and starter data. Multiple Flask apps can share one IIS box -
each gets its own site, app pool, port, and venv.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
flask db upgrade -> error 1071 |
MySQL 5.6 without the step-1 flags (or server not restarted). |
| IIS 500.19 | handler sections not unlocked (step 7.4), or the <rewrite> block active without URL Rewrite. |
| IIS 500.52 after enabling the rewrite block | allowedServerVariables locked at server level - appcmd unlock config -section:system.webServer/rewrite/allowedServerVariables. |
| Audit log shows only 127.0.0.1 with the rewrite block active | waitress strips untrusted proxy headers - --trusted-proxy=127.0.0.1 --trusted-proxy-headers=x-forwarded-for missing from the waitress arguments. |
| 500 with an empty HttpPlatform log | app-pool identity can't read APP_ROOT / run the venv (step 7.3), or .env missing/invalid. |
| "No time zone found with key America/New_York" | tzdata not installed (pip install tzdata). |
| Nav missing Equipment/PCs/... | plugins not installed (step 6 flask plugin install), or site not recycled. |
Method B: blank page / assets 404 under /ops |
frontend dist built without VITE_BASE_PATH=/ops/ (step 7b.1). |
| Method B: SPA loads but every API call 404s | MOUNT_PATH unset or not matching the Application alias (step 7b.3). |
| ConfigError on boot | a required .env var missing or left at a dev default. |