Six procedures that could not be followed as written.
Eighty-nine curl examples single-quoted `Authorization: Bearer $TOKEN`, so the
shell never expanded it and the server answered 422 "Not enough segments". Nine
more did the same with X-API-Key. The other 129 examples in the same file
already used double quotes, so this was drift rather than a convention, and the
spec regenerated from it carried the fault onward.
The GE-Enforce report example put a `//` comment inside a JSON body. The server
parses with silent=True, so it saw `{}` and answered "hostname is required"
about a body that plainly has one - the worst kind of error message, one that
sends the reader to the wrong field entirely.
The IIS install ran `flask db upgrade` and a per-plugin install loop but never
`flask plugin upgrade-all`, leaving every plugin's own chain unapplied. That is
precisely the 1054 "Unknown column" a deploy then hits somewhere else, days
later, on the page that uses the new column.
The pilot runbook looped `flask plugin enable` over plugins that were not yet
installed; enable refuses those, so on a fresh database it exited 1 on every
iteration and enabled nothing. ADR-013 had already recorded that defect.
`apply-profile` installs and enables in dependency order, which is what the step
was reaching for.
DEPLOY-WINDOWS-IIS named a profile file that does not exist; the shipped ones do.
And PLUGIN-EXTERNAL-REPO never mentioned PLUGIN_TABLE_OWNERS, while the
migration engine raises for any plugin missing from it - so the guide's own
step 5 fails for any external plugin that owns a table. That the registry lives
in the framework repo is deliberate, so the guide now says so, and says what it
costs: a table-owning external plugin is a two-repository change, and a plugin
that owns no tables avoids it entirely.
224 lines
9.6 KiB
Markdown
224 lines
9.6 KiB
Markdown
# Deploy shopdb-flask to Windows IIS (MySQL 5.6)
|
|
|
|
> **Not the route for a new site.** Sister sites install from the Windows
|
|
> installer - one `.exe`, no manual IIS work: **[INSTALL-WINDOWS.md](INSTALL-WINDOWS.md)**.
|
|
>
|
|
> This is the **manual** procedure for the the reference site server, which was built
|
|
> by hand against its existing MySQL 5.6 and predates the installer. Keep it for
|
|
> that box.
|
|
|
|
|
|
Runbook for standing up a single-site instance on the production Windows Server
|
|
that already runs the classic ASP shopdb, using IIS + HttpPlatformHandler +
|
|
waitress, against the existing MySQL 5.6. This is the test-instance path; keep
|
|
developing on the Linux dev box and redeploy as needed.
|
|
|
|
The Docker path in `DEPLOY.md` does NOT apply on Windows (gunicorn is Linux
|
|
only, and there is no MySQL container here). This file replaces it for IIS.
|
|
|
|
Notation: `APP_ROOT` = the deploy folder, e.g. `C:\shopdb-flask`. The IIS site
|
|
physical path must be `APP_ROOT` (where `wsgi.py` lives).
|
|
|
|
## 0. Prerequisites on the box
|
|
|
|
- Python 3.14 (same minor as dev and CI). `py -3.14 --version` to confirm.
|
|
- IIS with the **HttpPlatformHandler** module:
|
|
https://www.iis.net/downloads/microsoft/httpplatformhandler
|
|
- **URL Rewrite** module (only for the optional real-client-IP rule).
|
|
- Network access to the MySQL 5.6 server.
|
|
- If the box is air-gapped, you cannot `pip install` live. On the dev box run
|
|
`pip download -r requirements.txt -d wheels\` (on a matching
|
|
Windows/Python target, or use `--platform` wheels), copy `wheels\` over, and
|
|
install with `pip install --no-index --find-links wheels\ ...`.
|
|
|
|
## 1. Copy the code
|
|
|
|
Copy the repo to `APP_ROOT`, INCLUDING `frontend/dist` (the built SPA the API
|
|
serves). Build it on dev first if stale:
|
|
|
|
```bash
|
|
# on the dev box
|
|
cd frontend && npm run build # produces frontend/dist
|
|
```
|
|
|
|
Ship `frontend/dist` with the code (Node is not needed on the prod box).
|
|
|
|
## 2. Python venv + dependencies
|
|
|
|
```powershell
|
|
cd C:\shopdb-flask
|
|
py -3.14 -m venv venv
|
|
venv\Scripts\python -m pip install --upgrade pip
|
|
venv\Scripts\pip install -r requirements.txt
|
|
```
|
|
|
|
The DB driver is `pymysql` (pure Python) so no C compiler / MySQL client libs
|
|
are needed. `waitress` is the WSGI server and ships in `requirements.txt`
|
|
(unlike gunicorn, which the Docker image installs separately).
|
|
|
|
## 3. Prepare MySQL 5.6 (the utf8mb4 gotcha)
|
|
|
|
MySQL 5.6 defaults cannot index utf8mb4 VARCHAR(255) columns (767-byte prefix
|
|
limit) and often defaults the server charset to latin1. The schema is utf8mb4,
|
|
so the server needs Barracuda + large-prefix, made durable in `my.ini` under
|
|
`[mysqld]`, then restart the MySQL service:
|
|
|
|
```ini
|
|
[mysqld]
|
|
innodb_file_per_table = 1
|
|
innodb_file_format = Barracuda
|
|
innodb_large_prefix = 1
|
|
```
|
|
|
|
Then create the database as utf8mb4 and a least-privilege app user:
|
|
|
|
```sql
|
|
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;
|
|
```
|
|
|
|
Without the `[mysqld]` flags, `flask db upgrade` fails with error 1071
|
|
("Specified key was too long"). The migration chain emits `ROW_FORMAT=DYNAMIC`
|
|
per table (see `migrations/env.py`), which fits the 3072-byte prefix those
|
|
flags unlock.
|
|
|
|
## 4. Configure secrets and connection (.env)
|
|
|
|
Create `APP_ROOT\.env` (loaded by `wsgi.py` via `load_dotenv()`). Keep secrets
|
|
here, not in `web.config`. Lock the file's ACLs to the IIS app-pool identity +
|
|
administrators.
|
|
|
|
```
|
|
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=https://<the site's own hostname>
|
|
```
|
|
|
|
`ProductionConfig.validate()` refuses to boot if any of `SECRET_KEY`,
|
|
`JWT_SECRET_KEY`, `DATABASE_URL`, `CORS_ORIGINS` is missing or left at a dev
|
|
default. `CORS_ORIGINS` is the browser origin users hit (the IIS binding).
|
|
|
|
Generate a key: `venv\Scripts\python -c "import secrets;print(secrets.token_urlsafe(64))"`.
|
|
|
|
## 5. Initialize schema, data, plugins, admin
|
|
|
|
Run from `APP_ROOT` with the venv active and `.env` present:
|
|
|
|
```powershell
|
|
$env:FLASK_APP="shopdb"
|
|
venv\Scripts\flask db upgrade
|
|
venv\Scripts\flask seed reference-data
|
|
|
|
# Install the plugins this site tracks (registry lives in the gitignored
|
|
# instance/plugins.json, so a fresh box starts with none installed). Run
|
|
# `flask plugin list` to see the current bundled set; the the bundled plugins are
|
|
# computers, employees, geenforce, knowledgebase, machines, measuringtools,
|
|
# network, notifications, printedparts, printers, slides, usb, warranty. Install
|
|
# only the ones this site wants:
|
|
venv\Scripts\flask plugin list
|
|
venv\Scripts\flask plugin install machines
|
|
venv\Scripts\flask plugin install printers
|
|
venv\Scripts\flask plugin install computers
|
|
venv\Scripts\flask plugin install network
|
|
venv\Scripts\flask plugin install notifications
|
|
venv\Scripts\flask plugin install usb
|
|
venv\Scripts\flask plugin install knowledgebase
|
|
venv\Scripts\flask plugin install slides
|
|
venv\Scripts\flask plugin install employees
|
|
venv\Scripts\flask plugin upgrade-all
|
|
|
|
# First admin (password is generated and printed once):
|
|
venv\Scripts\flask seed admin --username admin --email admin@yourfacility.example.com
|
|
```
|
|
|
|
Cleaner than a hand list: declare the set once in a site profile and apply it:
|
|
|
|
```powershell
|
|
venv\Scripts\flask plugin apply-profile deploy\site-profile-universal.json # install + enable the chosen set, in dependency order
|
|
# deploy\site-profile.example.json is the annotated starting point to copy and trim.
|
|
venv\Scripts\flask plugin upgrade-all
|
|
venv\Scripts\flask plugin prune-schema --yes --force # FIRST PROVISIONING ONLY - see the warning below
|
|
```
|
|
|
|
(Alternatively copy the dev box's `instance/plugins.json` to `APP_ROOT\instance\`
|
|
to reproduce the exact set, then just run `flask plugin upgrade-all`.)
|
|
|
|
> **`prune-schema --force` is for first provisioning only.** It drops the tables
|
|
> of plugins this site did not install *even when they hold rows*. On a site that
|
|
> already has data, run `flask plugin prune-schema` with no flags first and read
|
|
> what it says it would drop. Re-running with `--force` after a feature has been
|
|
> used deletes that feature's records with no prompt and no backup.
|
|
|
|
## 6. Create the IIS site + web.config
|
|
|
|
This describes the own-site method (the app gets its own IIS site + port). To
|
|
mount the app at a subpath under an existing site instead (e.g.
|
|
`https://<host>/ops/` sharing the classic site's binding and cert), see
|
|
**docs/INSTALL-WINDOWS-IIS.md section 7b**: same web.config, but the site is a
|
|
`New-WebApplication` under the parent, `MOUNT_PATH=/ops` is set (web.config or
|
|
`.env`), and the frontend is built with `VITE_BASE_PATH=/ops/`.
|
|
|
|
1. In IIS Manager, add a new **Site** (separate from the classic ASP site):
|
|
- Physical path: `APP_ROOT`
|
|
- Binding: a free port or a dedicated hostname (e.g. `https` 443 with the
|
|
facility cert, or `http` on a test port like 8081 to start).
|
|
- App pool: No Managed Code, and an identity that can read `APP_ROOT`.
|
|
2. Copy `deploy\windows\web.config` to `APP_ROOT\web.config` and edit the paths
|
|
(`C:\shopdb-flask` -> your `APP_ROOT`). It launches
|
|
`waitress-serve --port=%HTTP_PLATFORM_PORT% wsgi:app` and sets
|
|
`FLASK_ENV=production` + `PYTHONPATH`.
|
|
3. Create `APP_ROOT\logs` for the HttpPlatform stdout log.
|
|
4. **Unlock the handler sections** (locked server-wide by default; without this
|
|
IIS returns **HTTP 500.19** "section cannot be used at this path"):
|
|
```powershell
|
|
%windir%\system32\inetsrv\appcmd unlock config /section:system.webServer/handlers
|
|
%windir%\system32\inetsrv\appcmd unlock config /section:system.webServer/httpPlatform
|
|
```
|
|
5. Grant the app-pool identity read/execute on `APP_ROOT` and modify on
|
|
`APP_ROOT\logs` (e.g. `icacls APP_ROOT /grant "IIS AppPool\<pool>:(OI)(CI)RX" /T`).
|
|
6. Recycle the app pool / restart the site.
|
|
|
|
TLS terminates at the IIS binding. The `X-Forwarded-For` URL Rewrite rule in the
|
|
web.config (real client IP for audit logs / kiosk visitor-location) is
|
|
**commented out by default** because it needs the URL Rewrite module - with it
|
|
active but URL Rewrite absent, IIS returns HTTP 500.19. Install URL Rewrite and
|
|
uncomment the `<rewrite>` block to enable it.
|
|
|
|
## 7. Smoke test
|
|
|
|
```powershell
|
|
# SPA loads:
|
|
curl.exe -k https://<host>/ # returns index.html
|
|
# API rejects an empty login with a validation error (health signal):
|
|
curl.exe -k -X POST https://<host>/api/auth/login -H "Content-Type: application/json" -d "{}"
|
|
# expect JSON containing VALIDATION_ERROR
|
|
```
|
|
|
|
Then log in through the browser as the admin from step 5 and confirm the
|
|
dashboard renders.
|
|
|
|
## 8. Redeploying as dev advances
|
|
|
|
Because this is a test instance you keep iterating on:
|
|
|
|
1. Pull/copy new code to `APP_ROOT` (rebuild `frontend/dist` on dev if the UI
|
|
changed).
|
|
2. `venv\Scripts\pip install -r requirements.txt` (if deps changed).
|
|
3. `venv\Scripts\flask db upgrade` (if new migrations).
|
|
4. Recycle the app pool.
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Cause / fix |
|
|
|---|---|
|
|
| Site 502 / process won't start | Check `APP_ROOT\logs\httpplatform*`. Usually a bad `processPath`, missing waitress, or `wsgi:app` not importable (set `PYTHONPATH`). |
|
|
| Boots but SQL echoes / debug on | `FLASK_ENV` not `production` (web.config env var or `.env`). |
|
|
| `flask db upgrade` error 1071 | MySQL 5.6 `[mysqld]` flags in step 3 not applied / server not restarted. |
|
|
| ConfigError on boot | A required var (SECRET_KEY / JWT_SECRET_KEY / DATABASE_URL / CORS_ORIGINS) missing or left at a dev default in `.env`. |
|
|
| Login works, CORS errors in browser | `CORS_ORIGINS` does not match the exact origin (scheme + host + port) the browser used. |
|
|
| Audit logs show 127.0.0.1 | Expected without the URL Rewrite X-Forwarded-For rule (step 6). |
|