Nine documents carried a hand-typed contract version and every one was stale. One was load-bearing: PLUGIN-EXTERNAL-REPO.md told an external author to pin ">=0.13.0,<0.14.0" while the contract is at 0.19.0, so a plugin built by following that guide is refused by the loader at startup. The plugin count was wrong in six more. They now point at docs/PROJECT-MAP.md, which is generated. A test enforces it: no document may declare a version literal, a stated current version must match the code, and a stated plugin count must match the tree. ADRs are exempt from the current-version rule, because an ADR states the version a decision was taken AT - that is a record of the past, and rewriting it would falsify the record ADRs exist to keep. CONTRACT-STABILITY.md was missing 0.17.0, 0.18.0 and 0.19.0 - including the only BREAKING change in the series - in the one document a site reads to choose its pin. All three are recorded, with 0.19.0 called out: it took something away, and it shipped before it was written down, which is the argument for pinning tight rather than trusting that a minor bump is safe.
223 lines
9.6 KiB
Markdown
223 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.json # install + enable the chosen set, in dependency order
|
|
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). |
|