Skip to content

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:

gunicorn --bind 0.0.0.0:${PORT:-8000} --workers 2 --threads 4 --timeout 120 app:app

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:

libgdal-dev libgeos-dev libproj-dev gdal-bin

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_KEY unset. 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_USERNAME or ADMIN_PASSWORD unset. admin_login_enabled() in app/auth.py returns 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

  1. config.py is imported. It calls load_dotenv(), reads the environment once, and emits the SECRET_KEY warning if that variable is missing.
  2. create_app() in app/app.py builds the Flask app, enables CORS, and sets a 50 MB request-body cap (MAX_CONTENT_LENGTH).
  3. init_firebase() runs. It is a no-op when Firebase is not configured.
  4. init_db() runs. This is the significant step: data.py reads every CSV and GeoJSON under app/data/ into a module-level in-memory dictionary. It is idempotent, and it runs once per worker process.
  5. The auth, pages and API blueprints are registered.
  6. 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:

pip install pytest
python -m pytest app/tests -q

79 tests. The suite sets BYPASS_AUTH=1 for itself and needs no environment configuration. app/tests/ is excluded from the container image.