Deployment and operations¶
Operator reference for running SWAG-DSS. Every statement here was checked against the repository. Where a figure depends on the target environment and cannot be established from the source, it is called out as unverified rather than estimated.
What the application is¶
| Property | Value |
|---|---|
| Runtime | Python 3.11 (Dockerfile: FROM python:3.11-slim) |
| Web framework | Flask 3.0.0 |
| WSGI server | gunicorn 21.2.0 |
| WSGI entrypoint | app:app (module-level app in app/app.py) |
| Container port | 8000 (ENV PORT=8000, EXPOSE 8000) |
| Database | none |
| Persistent storage | none required |
| Background workers | none |
| Scheduled jobs | none |
Container start command, from the Dockerfile CMD:
Shell form, so a platform-injected $PORT overrides the default of 8000. The
container runs as the unprivileged user appuser.
Network requirements¶
Every front-end library is loaded from a public CDN at request time. None are
vendored into app/static/ and the container carries no copies. The browser
makes these requests, not the server, so what matters is what the user's
browser can reach and what the page's Content-Security-Policy allows.
| Host | Serves | If unreachable |
|---|---|---|
unpkg.com |
Leaflet core and CSS | the map does not render |
cdn.jsdelivr.net |
Chart.js, Leaflet plugins, Bootstrap Icons | charts and icons do not render |
cdn.tailwindcss.com |
Tailwind CSS | the page loads unstyled |
fonts.googleapis.com |
web fonts | fallback system fonts are used |
www.gstatic.com |
Firebase JS SDK | Google sign-in is unavailable |
Behind a strict Content-Security-Policy these hosts must be allow-listed, or
the dashboard renders as an unstyled page with no map and no charts. The
application still returns HTTP 200 in that state, so a health check on /
passes while the interface is unusable. Do not rely on the health check to
catch this.
If Firebase sign-in is used, the deployed hostname must also be added in the Firebase console under Authentication, Settings, Authorized domains, or sign-in fails on the deployed host while working locally.
The application server itself needs no outbound internet access at runtime: all data it serves is on disk in the image. Outbound access is required only during the Docker build, to install system and Python packages.
System dependencies¶
The geospatial stack (GeoPandas, Fiona, Shapely, pyproj) links against GDAL, GEOS and PROJ. The image installs these Debian packages:
and sets CPLUS_INCLUDE_PATH and C_INCLUDE_PATH to /usr/include/gdal so the
Python packages build against them. Any deployment target that does not provide
these libraries will fail at import time, not at request time. This is the
reason to prefer a Docker-based platform over a generic Python platform.
Environment variables¶
| Variable | Required | Purpose | Behaviour when unset |
|---|---|---|---|
SECRET_KEY |
yes for production | Signs the Flask session cookie | A random key is generated per process and a warning is written to stderr |
ADMIN_USERNAME |
yes for production (unless using Firebase) | Admin login username | Admin login is disabled |
ADMIN_PASSWORD |
yes for production (unless using Firebase) | Admin login password | Admin login is disabled |
PORT |
no | Listen port | 8000 in the container |
BYPASS_AUTH |
no | Set to 1 to skip the login gate; local development only |
Auth is enforced |
FIREBASE_API_KEY and other FIREBASE_* |
no | Optional Firebase authentication | Firebase login is not offered |
No credential has a committed fallback value. The two failure modes this produces are both silent at boot and visible only in use:
SECRET_KEYunset. The app starts and serves traffic. Because the container runs 2 gunicorn workers, each with its own random key, a session cookie signed by one worker is rejected by the other. Users appear to be logged out at random, and every restart or redeploy invalidates all sessions. The stderr warning at boot is the only direct signal.ADMIN_USERNAMEorADMIN_PASSWORDunset.admin_login_enabled()inapp/auth.pyreturns false and the login form rejects every submission, including one that omits the fields entirely. There is no way to log in. This is the intended safe state, not a bug.
Set all three for any deployment that real users will log into. Supply them through the platform's secret store rather than the image or a committed file.
What happens at boot¶
config.pyis imported. It callsload_dotenv(), reads the environment once, and emits theSECRET_KEYwarning if that variable is missing.create_app()inapp/app.pybuilds the Flask app, enables CORS, and sets a 50 MB request-body cap (MAX_CONTENT_LENGTH).init_firebase()runs. It is a no-op when Firebase is not configured.init_db()runs. This is the significant step:data.pyreads every CSV and GeoJSON underapp/data/into a module-level in-memory dictionary. It is idempotent, and it runs once per worker process.- The auth, pages and API blueprints are registered.
- A line beginning
Data loaded:is printed, reporting WRUA, reallocation, band and simulation row counts. Its absence from the logs means the data load did not complete.
Missing data files are not fatal. load_data() substitutes an empty DataFrame
or GeoDataFrame for any file it cannot find, so an incomplete app/data/
produces a running app that serves empty payloads and 200 responses. If the
dashboard renders but shows no data, check that app/data/ reached the image.
Statelessness¶
The application writes nothing to disk at request time and holds no cross-request state beyond the in-memory data cache.
- CSV export builds the file in an in-memory buffer (
app/api/exports.py). - PDF export writes into an in-memory buffer (
app/api/report.py). - Sessions are client-side Flask cookies signed with
SECRET_KEY. - The GeoJSON response cache is per-process and rebuilt on demand.
Consequences for deployment: instances are interchangeable, can be scaled
horizontally, and can be replaced at any time. No volume, no shared filesystem
and no database are required. The only cross-instance requirement is that every
instance share the same SECRET_KEY, so that a session issued by one instance
is accepted by the others.
Resource expectations¶
| Resource | What is known |
|---|---|
| Data on disk | app/data/ is approximately 47 MB |
| Largest single files | simulation_combined_114.csv and simulation_sat_114.csv, roughly 11 MB each |
| Memory | The full dataset is parsed into pandas and GeoPandas objects and held for the process lifetime, once per worker. The container runs 2 workers, so the data is held twice. In-memory size is larger than the on-disk size, by a factor that depends on pandas dtype inference and has not been measured. |
| Startup time | Dominated by the data load, since every file is parsed before the first request is served. Not measured on target hardware. |
| CPU | Requests slice and aggregate tables already in memory. No hydrological computation happens at request time. |
The gunicorn --timeout 120 is generous relative to normal request work and is
sized for the heavier export and report endpoints.
Provision against measurement on the target instance type rather than against the figures above. Memory is the resource to watch, because it scales with the worker count.
Health checks¶
GET / returns 200. It is handled by pages.landing in app/pages.py and has
no @login_required decorator, so it does not need a session and is suitable
for an unauthenticated load-balancer health check.
Routes that do require a session are /dashboard and /comparison. Both
redirect to /login when there is no session, so neither is usable as a health
check.
Note that the health check succeeding does not imply the data loaded, since a
missing dataset is non-fatal. Pair it with a check of the Data loaded: boot
log line.
Documentation site¶
The /documentation/ routes serve a MkDocs site built into app/site/. The
Docker build runs mkdocs build and writes the output there. If app/site/ is
absent the route returns a 404 JSON hint and the rest of the application is
unaffected, so the documentation build is optional and can be removed from the
Dockerfile without breaking the application.
Tests¶
From the repository root:
79 tests. The suite sets BYPASS_AUTH=1 for itself and needs no environment
configuration. app/tests/ is excluded from the container image.