Support subpath IIS deployment as a second install method
Some checks failed
CI / backend (push) Successful in 1m41s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 8s

The app can run as an IIS Application under an existing site
(e.g. https://host/ops/) instead of its own site + port:

- frontend: vite base via VITE_BASE_PATH; router history, axios
  baseURL, and root-absolute asset/route paths resolve through
  utils/basePath.js withBase()
- backend: MOUNT_PATH (env or .env) wraps the app in a WSGI
  middleware that shifts the prefix into SCRIPT_NAME, so one knob
  serves API + SPA under the mount
- docs: INSTALL-WINDOWS-IIS.md section 7b runbook + troubleshooting
  rows; DEPLOY-WINDOWS-IIS.md pointer; commented examples in
  deploy/windows/web.config and .env.example

Root deployment unchanged (MOUNT_PATH unset, base '/'). Also folds
two stray root-absolute callers into the shared plumbing
(MachineForm relationship-types fetch, reports CSV window.open).
This commit is contained in:
cproudlock
2026-07-13 16:11:12 -04:00
parent 69dd6d0abe
commit 6010f01de1
19 changed files with 143 additions and 25 deletions

View File

@@ -79,3 +79,10 @@ ZABBIX_TOKEN=
# CMMC_USB_DB_USER= # CMMC_USB_DB_USER=
# CMMC_USB_DB_PASSWORD= # CMMC_USB_DB_PASSWORD=
# CMMC_USB_DB_NAME=cmmc_usb # CMMC_USB_DB_NAME=cmmc_usb
# ---- Subpath deployment (optional) ----
# Serve the app under a URL prefix instead of the server root, e.g. as an IIS
# Application at /ops under an existing site. The frontend must be rebuilt with
# the matching base: VITE_BASE_PATH=/ops/ npm run build. Leave unset when the
# app owns its own site/port (the default). See docs/INSTALL-WINDOWS-IIS.md.
# MOUNT_PATH=/ops

View File

@@ -38,6 +38,13 @@
config (SQL echo, debug, wrong DB URL). Real secrets go in .env. --> config (SQL echo, debug, wrong DB URL). Real secrets go in .env. -->
<environmentVariable name="FLASK_ENV" value="production" /> <environmentVariable name="FLASK_ENV" value="production" />
<environmentVariable name="PYTHONPATH" value="C:\shopdb-flask" /> <environmentVariable name="PYTHONPATH" value="C:\shopdb-flask" />
<!-- Subpath method only: when this web.config sits in an IIS
Application (e.g. /ops) under an existing site instead of its own
site, tell the app its mount path. Must match the alias the
Application was created with AND the VITE_BASE_PATH the frontend
was built with ('/ops/'). Omit for the own-site method.
<environmentVariable name="MOUNT_PATH" value="/ops" />
-->
</environmentVariables> </environmentVariables>
</httpPlatform> </httpPlatform>

View File

@@ -128,6 +128,13 @@ to reproduce the exact enabled set, then just run `flask plugin upgrade-all`.)
## 6. Create the IIS site + web.config ## 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): 1. In IIS Manager, add a new **Site** (separate from the classic ASP site):
- Physical path: `APP_ROOT` - Physical path: `APP_ROOT`
- Binding: a free port or a dedicated hostname (e.g. `https` 443 with the - Binding: a free port or a dedicated hostname (e.g. `https` 443 with the

View File

@@ -129,6 +129,16 @@ venv\Scripts\flask seed admin --username admin --email admin@yourfacility.exampl
## 7. IIS site ## 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.
1. Copy `deploy\windows\web.config` to `APP_ROOT\web.config`. If `APP_ROOT` is not 1. Copy `deploy\windows\web.config` to `APP_ROOT\web.config`. If `APP_ROOT` is not
`C:\shopdb-flask`, fix the paths inside it. Create `APP_ROOT\logs`. `C:\shopdb-flask`, fix the paths inside it. Create `APP_ROOT\logs`.
2. Create an app pool with **No Managed Code**: 2. Create an app pool with **No Managed Code**:
@@ -159,6 +169,32 @@ 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 web.config and reverse-proxies the site port to it. First request takes ~15s
(the app boots + connects to MySQL). (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.
1. Rebuild the frontend for the subpath (on the dev box, then copy `dist`):
```bash
cd frontend && VITE_BASE_PATH=/ops/ npm run build # note the trailing slash
```
2. Create the Application under the existing site (instead of `New-Website`):
```powershell
New-WebApplication -Site "Default Web Site" -Name ops -PhysicalPath APP_ROOT -ApplicationPool shopdbflask
```
3. Tell the backend its mount path: in `APP_ROOT\web.config`, uncomment the
`MOUNT_PATH` environment variable (value `/ops`), or set `MOUNT_PATH=/ops`
in `APP_ROOT\.env`. `wsgi.py` then serves everything under the prefix
(requests outside it get a plain 404 naming the mount).
4. 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-For` URL Rewrite rule in web.config is **commented out by > The `X-Forwarded-For` URL Rewrite rule in web.config is **commented out by
> default**. It needs the URL Rewrite module; with it active but the module > default**. It needs the URL Rewrite module; with it active but the module
> absent, IIS returns 500.19. Install URL Rewrite, then uncomment the > absent, IIS returns 500.19. Install URL Rewrite, then uncomment the
@@ -191,4 +227,6 @@ each gets its own site, app pool, port, and venv.
| **500** with an empty HttpPlatform log | app-pool identity can't read `APP_ROOT` / run the venv (step 7.3), or `.env` missing/invalid. | | **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`). | | "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. | | 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. | | ConfigError on boot | a required `.env` var missing or left at a dev default. |

View File

@@ -1,7 +1,10 @@
import axios from 'axios' import axios from 'axios'
import { withBase } from './../utils/basePath'
// BASE_URL ends in '/', so this is '/api' at root or '/ops/api' under a subpath
// mount. Keeps the SPA, its API, and IIS all on the same mount path.
const api = axios.create({ const api = axios.create({
baseURL: '/api', baseURL: import.meta.env.BASE_URL + 'api',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
@@ -31,10 +34,11 @@ api.interceptors.response.use(
// Only redirect if user was previously logged in (session expired). // Only redirect if user was previously logged in (session expired).
// Preserve the destination so login returns the user to this page. // Preserve the destination so login returns the user to this page.
if (hadToken) { if (hadToken) {
const loginPath = withBase('/login')
const here = window.location.pathname + window.location.search const here = window.location.pathname + window.location.search
const target = here && here !== '/login' const target = here && here !== loginPath
? '/login?redirect=' + encodeURIComponent(here) ? loginPath + '?redirect=' + encodeURIComponent(here)
: '/login' : loginPath
window.location.href = target window.location.href = target
} }
} }

View File

@@ -5,6 +5,7 @@
// install still renders; each site uploads its own blueprint in Settings. // install still renders; each site uploads its own blueprint in Settings.
import { reactive } from 'vue' import { reactive } from 'vue'
import { settingsApi } from '../api' import { settingsApi } from '../api'
import { withBase } from '../utils/basePath'
// Fallback defaults - match the seeded map_blueprint_* setting defaults. // Fallback defaults - match the seeded map_blueprint_* setting defaults.
const DEFAULTS = { const DEFAULTS = {
@@ -59,7 +60,7 @@ export function reloadMapConfig() {
// Blueprint image URL for the given theme ('light' | 'dark'). // Blueprint image URL for the given theme ('light' | 'dark').
export function blueprintUrlFor(theme) { export function blueprintUrlFor(theme) {
return theme === 'light' ? state.blueprintLight : state.blueprintDark return withBase(theme === 'light' ? state.blueprintLight : state.blueprintDark)
} }
export function useMapConfig() { export function useMapConfig() {

View File

@@ -117,7 +117,7 @@ const routes = [
] ]
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(import.meta.env.BASE_URL),
routes routes
}) })

View File

@@ -0,0 +1,13 @@
// Single source for the app's mount path. Vite injects import.meta.env.BASE_URL
// from the build-time `base` (default '/', or e.g. '/ops/' for a subpath IIS
// mount). Every root-absolute URL to a Flask-served asset or route must go
// through withBase() so it resolves under the mount instead of the server root.
export const BASE_URL = import.meta.env.BASE_URL
// Prefix a root-absolute path (e.g. '/ge-aerospace-logo.svg', '/api', '/tv')
// with the mount base. Leaves full URLs (http, data:) untouched.
export function withBase(path) {
if (!path) return path
if (/^([a-z]+:)?\/\//i.test(path) || path.startsWith('data:')) return path
return BASE_URL + String(path).replace(/^\//, '')
}

View File

@@ -2,6 +2,7 @@
// The settings GET is public (jwt optional), so the kiosk dashboard and the // The settings GET is public (jwt optional), so the kiosk dashboard and the
// print views can read these without auth. Fetched once and cached per page. // print views can read these without auth. Fetched once and cached per page.
import { settingsApi } from '@/api' import { settingsApi } from '@/api'
import { withBase } from '@/utils/basePath'
let settingsCache = null let settingsCache = null
@@ -38,7 +39,7 @@ export async function getFacilityName() {
// Main site logo (sidebar, login, dashboard header). Fallback = shipped GE mark. // Main site logo (sidebar, login, dashboard header). Fallback = shipped GE mark.
export async function getSiteLogo() { export async function getSiteLogo() {
return getSetting('site_logo', '/ge-aerospace-logo.svg') return withBase(await getSetting('site_logo', '/ge-aerospace-logo.svg'))
} }
// Logo composited into the center of printer QR codes. Empty = no overlay. // Logo composited into the center of printer QR codes. Empty = no overlay.
@@ -47,17 +48,18 @@ export async function getSiteLogo() {
export async function getQrLogo() { export async function getQrLogo() {
const settings = await loadSettings() const settings = await loadSettings()
const value = settings['qr_logo'] const value = settings['qr_logo']
return (value === undefined || value === null) ? '/ge-monogram.svg' : value return withBase((value === undefined || value === null) ? '/ge-monogram.svg' : value)
} }
// Logo printed on machine inspection badges. // Logo printed on machine inspection badges.
export async function getBadgeLogo() { export async function getBadgeLogo() {
return getSetting('badge_logo', '/ge-aerospace-logo.svg') return withBase(await getSetting('badge_logo', '/ge-aerospace-logo.svg'))
} }
// Browser-tab favicon. Empty = keep the shipped /favicon.svg. // Browser-tab favicon. Empty = keep the shipped /favicon.svg.
export async function getFavicon() { export async function getFavicon() {
return getSetting('site_favicon', '') const value = await getSetting('site_favicon', '')
return value ? withBase(value) : value
} }
// Brand primary color override. Empty = built-in palette from style.css. // Brand primary color override. Empty = built-in palette from style.css.

View File

@@ -25,8 +25,8 @@
</template> </template>
<div class="nav-section">Displays</div> <div class="nav-section">Displays</div>
<a href="/shopfloor" target="_blank" class="external-link">Shopfloor Dashboard</a> <a :href="withBase('/shopfloor')" target="_blank" class="external-link">Shopfloor Dashboard</a>
<a href="/tv" target="_blank" class="external-link">TV Slideshow</a> <a :href="withBase('/tv')" target="_blank" class="external-link">TV Slideshow</a>
<router-link v-if="authStore.isAdmin" to="/settings">Settings</router-link> <router-link v-if="authStore.isAdmin" to="/settings">Settings</router-link>
</nav> </nav>
@@ -107,6 +107,7 @@ import { useAuthStore } from '../stores/auth'
import { currentTheme, toggleTheme } from '../stores/theme' import { currentTheme, toggleTheme } from '../stores/theme'
import { dashboardApi, notificationsApi } from '../api' import { dashboardApi, notificationsApi } from '../api'
import { getFacilityName, getSiteLogo, getServicenowUrls } from '../utils/siteSettings' import { getFacilityName, getSiteLogo, getServicenowUrls } from '../utils/siteSettings'
import { withBase } from '../utils/basePath'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@@ -118,7 +119,7 @@ const searchQuery = ref('')
const navItems = ref([]) const navItems = ref([])
const activeNotifications = ref([]) const activeNotifications = ref([])
const facilityName = ref('ShopDB') const facilityName = ref('ShopDB')
const siteLogo = ref('/ge-aerospace-logo.svg') const siteLogo = ref(withBase('/ge-aerospace-logo.svg'))
const servicenowConfig = ref({ enabled: true, searchUrl: '' }) const servicenowConfig = ref({ enabled: true, searchUrl: '' })
function getTicketSearchUrl(ticketnumber) { function getTicketSearchUrl(ticketnumber) {

View File

@@ -41,6 +41,7 @@ import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth' import { useAuthStore } from '../stores/auth'
import { authApi } from '../api' import { authApi } from '../api'
import { getSiteLogo } from '../utils/siteSettings' import { getSiteLogo } from '../utils/siteSettings'
import { withBase } from '../utils/basePath'
import { apiError } from '../utils/apiError' import { apiError } from '../utils/apiError'
const router = useRouter() const router = useRouter()
@@ -48,7 +49,7 @@ const authStore = useAuthStore()
const forced = computed(() => authStore.mustChangePassword) const forced = computed(() => authStore.mustChangePassword)
const siteLogo = ref('/ge-aerospace-logo.svg') const siteLogo = ref(withBase('/ge-aerospace-logo.svg'))
const currentPassword = ref('') const currentPassword = ref('')
const newPassword = ref('') const newPassword = ref('')
const confirmPassword = ref('') const confirmPassword = ref('')

View File

@@ -52,6 +52,7 @@ import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '../stores/auth' import { useAuthStore } from '../stores/auth'
import { setupApi } from '../api' import { setupApi } from '../api'
import { getSiteLogo } from '../utils/siteSettings' import { getSiteLogo } from '../utils/siteSettings'
import { withBase } from '../utils/basePath'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@@ -67,7 +68,7 @@ function postLoginTarget() {
return '/' return '/'
} }
const siteLogo = ref('/ge-aerospace-logo.svg') const siteLogo = ref(withBase('/ge-aerospace-logo.svg'))
const mode = ref('login') const mode = ref('login')
const username = ref('') const username = ref('')
const email = ref('') const email = ref('')

View File

@@ -186,10 +186,11 @@
import { ref, computed, onMounted, onUnmounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
import { notificationsApi, businessUnitsApi, dashboardDefaultsApi } from '@/api' import { notificationsApi, businessUnitsApi, dashboardDefaultsApi } from '@/api'
import { getFacilityName, getSiteLogo, getServicenowUrls } from '@/utils/siteSettings' import { getFacilityName, getSiteLogo, getServicenowUrls } from '@/utils/siteSettings'
import { withBase } from '@/utils/basePath'
const loading = ref(true) const loading = ref(true)
const facilityName = ref('ShopDB') const facilityName = ref('ShopDB')
const siteLogo = ref('/ge-aerospace-logo.svg') const siteLogo = ref(withBase('/ge-aerospace-logo.svg'))
// ServiceNow ticket-link config; loaded on mount. Empty/disabled = plain text. // ServiceNow ticket-link config; loaded on mount. Empty/disabled = plain text.
const servicenowConfig = ref({ enabled: true, incidentUrl: '', changeUrl: '' }) const servicenowConfig = ref({ enabled: true, incidentUrl: '', changeUrl: '' })
const businessUnit = ref('') const businessUnit = ref('')

View File

@@ -7,7 +7,7 @@
class="slide" class="slide"
:class="{ active: idx === currentSlide }" :class="{ active: idx === currentSlide }"
> >
<img :src="basePath + slide.filename" :alt="slide.filename" /> <img :src="withBase(basePath + slide.filename)" :alt="slide.filename" />
</div> </div>
<div v-if="error" class="error-message"> <div v-if="error" class="error-message">
@@ -28,6 +28,7 @@
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
import api from '@/api' import api from '@/api'
import { withBase } from '@/utils/basePath'
const INTERVAL = 10 // seconds between slides const INTERVAL = 10 // seconds between slides

View File

@@ -347,7 +347,7 @@
<script setup> <script setup>
import { ref, onMounted, computed, watch } from 'vue' import { ref, onMounted, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { machinesApi, vendorsApi, locationsApi, modelsApi, businessunitsApi, computersApi, assetsApi } from '../../api' import { machinesApi, vendorsApi, locationsApi, modelsApi, businessunitsApi, computersApi, assetsApi, relationshipTypesApi } from '../../api'
import ShopFloorMap from '../../components/ShopFloorMap.vue' import ShopFloorMap from '../../components/ShopFloorMap.vue'
import Modal from '../../components/Modal.vue' import Modal from '../../components/Modal.vue'
import CustomFieldsInputs from '../../components/CustomFieldsInputs.vue' import CustomFieldsInputs from '../../components/CustomFieldsInputs.vue'
@@ -463,11 +463,8 @@ onMounted(async () => {
// Load relationship types separately // Load relationship types separately
try { try {
const relRes = await fetch('/api/assets/relationshiptypes') const relRes = await relationshipTypesApi.list()
if (relRes.ok) { relationshipTypes.value = relRes.data.data || []
const relData = await relRes.json()
relationshipTypes.value = relData.data || []
}
} catch (e) { } catch (e) {
// Fallback - use hardcoded Controls type // Fallback - use hardcoded Controls type
relationshipTypes.value = [{ relationshiptypeid: 1, relationshiptype: 'Controls' }] relationshipTypes.value = [{ relationshiptypeid: 1, relationshiptype: 'Controls' }]

View File

@@ -30,6 +30,7 @@ import { ref, computed, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { machinesApi } from '../../api' import { machinesApi } from '../../api'
import { getBadgeLogo } from '@/utils/siteSettings' import { getBadgeLogo } from '@/utils/siteSettings'
import { withBase } from '@/utils/basePath'
import JsBarcode from 'jsbarcode' import JsBarcode from 'jsbarcode'
const route = useRoute() const route = useRoute()
@@ -37,7 +38,7 @@ const loading = ref(true)
const machine = ref(null) const machine = ref(null)
const barcodeEl = ref(null) const barcodeEl = ref(null)
const geLogo = ref('/ge-aerospace-logo.svg') const geLogo = ref(withBase('/ge-aerospace-logo.svg'))
const isInspection = computed(() => { const isInspection = computed(() => {
if (!machine.value) return false if (!machine.value) return false

View File

@@ -220,6 +220,7 @@
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { reportsApi, businessunitsApi, assetsApi, locationsApi, applicationsApi } from '@/api' import { reportsApi, businessunitsApi, assetsApi, locationsApi, applicationsApi } from '@/api'
import { withBase } from '@/utils/basePath'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@@ -418,7 +419,7 @@ async function runReport(report) {
function exportCSV() { function exportCSV() {
if (!currentReport.value) return if (!currentReport.value) return
const params = new URLSearchParams({ format: 'csv', ...filterParams() }) const params = new URLSearchParams({ format: 'csv', ...filterParams() })
window.open(`/api/reports/${currentReport.value.id}?${params}`, '_blank') window.open(withBase(`/api/reports/${currentReport.value.id}?${params}`), '_blank')
} }
function clearReport() { function clearReport() {

View File

@@ -3,6 +3,10 @@ import vue from '@vitejs/plugin-vue'
import path from 'path' import path from 'path'
export default defineConfig({ export default defineConfig({
// Mount path of the built app. Default '/' (own IIS site / dev). Set
// VITE_BASE_PATH='/ops/' (trailing slash) to build for a subpath mount under
// Default Web Site, e.g. `VITE_BASE_PATH=/ops/ npm run build`.
base: process.env.VITE_BASE_PATH || '/',
plugins: [vue()], plugins: [vue()],
resolve: { resolve: {
alias: { alias: {

31
wsgi.py
View File

@@ -10,5 +10,36 @@ from shopdb import create_app
app = create_app(os.environ.get('FLASK_ENV', 'development')) app = create_app(os.environ.get('FLASK_ENV', 'development'))
class MountPathMiddleware:
"""Serve the whole app (API + SPA) under a URL prefix, e.g. '/ops'.
Used when the app is deployed as an IIS Application under an existing
site instead of its own site: IIS forwards the full request path
('/ops/api/...'), so the prefix is moved from PATH_INFO to SCRIPT_NAME
before Flask routes it. Flask then also generates URLs under the prefix.
The frontend must be built with the matching VITE_BASE_PATH ('/ops/').
"""
def __init__(self, wsgi_app, mountpath):
self.wsgi_app = wsgi_app
self.mountpath = '/' + mountpath.strip('/')
def __call__(self, environ, start_response):
path = environ.get('PATH_INFO', '')
if path == self.mountpath or path.startswith(self.mountpath + '/'):
environ['SCRIPT_NAME'] = environ.get('SCRIPT_NAME', '') + self.mountpath
environ['PATH_INFO'] = path[len(self.mountpath):] or '/'
return self.wsgi_app(environ, start_response)
start_response('404 Not Found', [('Content-Type', 'text/plain')])
return [b'Not Found: the app is mounted at ' + self.mountpath.encode() + b'/']
# MOUNT_PATH (.env or web.config) activates the subpath deployment method.
# Unset/empty = the app owns the server root (its own IIS site; the default).
_mountpath = os.environ.get('MOUNT_PATH', '').strip()
if _mountpath and _mountpath != '/':
app.wsgi_app = MountPathMiddleware(app.wsgi_app, _mountpath)
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=5001, debug=True) app.run(host='0.0.0.0', port=5001, debug=True)