deployment ready
This commit is contained in:
parent
87d087b913
commit
11820b9f4e
10 changed files with 348 additions and 43 deletions
13
.dockerignore
Normal file
13
.dockerignore
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
.git
|
||||
.gitignore
|
||||
.env
|
||||
.venv
|
||||
__pycache__
|
||||
*.py[cod]
|
||||
.pytest_cache
|
||||
.coverage
|
||||
htmlcov
|
||||
instance/*.db
|
||||
instance/*.sqlite*
|
||||
tests
|
||||
*.md
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
# Local development values. Docker Compose sets STEADY_ENV and DATABASE_URL.
|
||||
STEADY_ENV=development
|
||||
SECRET_KEY=replace-with-a-long-random-value
|
||||
# DATABASE_URL may override the default absolute instance/steady.db location.
|
||||
FEATURE_ADMIN=false
|
||||
# TRUSTED_HOSTS=steady.example.com,www.steady.example.com
|
||||
TRUSTED_HOSTS=steady.bujour.de
|
||||
TRUST_PROXY_HEADERS=false
|
||||
GUNICORN_BIND=127.0.0.1:8000
|
||||
GUNICORN_WORKERS=2
|
||||
GUNICORN_THREADS=2
|
||||
|
|
|
|||
169
DEPLOYMENT.md
169
DEPLOYMENT.md
|
|
@ -1,43 +1,158 @@
|
|||
# Deployment Guide
|
||||
# Deploy Steady with Docker Compose and Caddy
|
||||
|
||||
## Production Requirements
|
||||
This repository is ready for the established VPS layout:
|
||||
|
||||
Use a supported Python version, Gunicorn, TLS termination, and a reverse proxy or managed platform. Never use `flask run` in production. Flask's official guidance recommends a dedicated WSGI server and commonly a reverse proxy: [Flask deployment documentation](https://flask.palletsprojects.com/en/stable/deploying/).
|
||||
- repository directory: `~/stacks/steady`
|
||||
- container/service name: `steady`
|
||||
- public address: `https://steady.bujour.de`
|
||||
- private Docker port: `5000` (it is not published on the VPS)
|
||||
- persistent SQLite volume: `steady_data`, mounted at `/app/instance`
|
||||
- reverse proxy: Caddy on the external Docker network `proxy`
|
||||
|
||||
Set environment variables outside the repository:
|
||||
The container runs database migrations before Gunicorn starts. Caddy terminates
|
||||
TLS and forwards requests over the private Docker network.
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
Install Git, Docker Engine with the Compose plugin, and Caddy in its existing
|
||||
Compose stack. DNS for `steady.bujour.de` must resolve to the VPS, and inbound
|
||||
ports 80 and 443 must be allowed by both the VPS firewall and provider firewall.
|
||||
Do not open port 5000.
|
||||
|
||||
Confirm the shared proxy network exists:
|
||||
|
||||
```bash
|
||||
STEADY_ENV=production
|
||||
SECRET_KEY=<long-random-secret>
|
||||
DATABASE_URL=sqlite:////srv/steady/instance/steady.db
|
||||
TRUSTED_HOSTS=steady.example.com
|
||||
FEATURE_ADMIN=false
|
||||
docker network inspect proxy >/dev/null 2>&1 || docker network create proxy
|
||||
```
|
||||
|
||||
Generate a secret with `python -c "import secrets; print(secrets.token_urlsafe(48))"`. Restrict the instance directory and environment to the service account. SQLite is suitable for a single-host deployment; use platform-managed storage and a database designed for concurrency before horizontal scaling.
|
||||
## 2. Clone the application
|
||||
|
||||
## Release Procedure
|
||||
Replace the example repository URL with the actual Git remote:
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python -m pip install -r requirements.txt
|
||||
flask --app app db upgrade
|
||||
python -m pytest
|
||||
gunicorn --config gunicorn.conf.py wsgi:app
|
||||
mkdir -p ~/stacks
|
||||
git clone <repository-url> ~/stacks/steady
|
||||
cd ~/stacks/steady
|
||||
```
|
||||
|
||||
Gunicorn binds to `127.0.0.1:8000` by default. Put TLS and request buffering in a trusted reverse proxy; do not expose the internal bind when the proxy is intended to be mandatory. Configure forwarded headers only for known proxy addresses rather than applying `ProxyFix` generically.
|
||||
For an existing checkout, use `git pull --ff-only` instead of cloning it again.
|
||||
|
||||
## Operations
|
||||
## 3. Create the production environment
|
||||
|
||||
- Probe `/health` for process availability; it intentionally does not test every dependency.
|
||||
- Back up the SQLite database using a consistent database-aware snapshot. Per-user JSON exports are portability backups, not a replacement for server backups.
|
||||
- Back up before migrations and test restore procedures regularly.
|
||||
- Rotate `SECRET_KEY` only with a plan to invalidate existing sessions.
|
||||
- Review logs without recording passwords, secrets, backup contents, or task descriptions.
|
||||
- Confirm HTTPS responses include HSTS and all responses include the configured security headers.
|
||||
- Keep `FEATURE_ADMIN=false` unless the guarded placeholder is explicitly required.
|
||||
Compose automatically reads `~/stacks/steady/.env`. Create it on the VPS; do
|
||||
not commit it:
|
||||
|
||||
Rollback should restore both the prior application release and its compatible database backup. Do not run destructive schema downgrades without reviewing the generated migration and recovery plan.
|
||||
```bash
|
||||
cd ~/stacks/steady
|
||||
umask 077
|
||||
python -c "import secrets; print('SECRET_KEY=' + secrets.token_urlsafe(48))" > .env
|
||||
printf '%s\n' 'TRUSTED_HOSTS=steady.bujour.de' >> .env
|
||||
```
|
||||
|
||||
`compose.yaml` supplies the remaining production settings, including the exact
|
||||
SQLite URL `sqlite:////app/instance/steady.db`. The named volume therefore
|
||||
persists the database across container replacement.
|
||||
|
||||
Validate the rendered configuration without printing the secret in shared logs:
|
||||
|
||||
```bash
|
||||
docker compose config --quiet
|
||||
```
|
||||
|
||||
## 4. Build and start
|
||||
|
||||
```bash
|
||||
docker compose build --pull
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
docker compose logs --tail=100 steady
|
||||
```
|
||||
|
||||
Startup fails if the migration fails, so do not ignore an unhealthy or restarting
|
||||
container. Check the private endpoint from another container on the proxy network:
|
||||
|
||||
```bash
|
||||
docker run --rm --network proxy curlimages/curl:8.14.1 -fsS http://steady:5000/health
|
||||
```
|
||||
|
||||
The expected body is `{"status":"ok"}`. `curl localhost:5000` intentionally
|
||||
does not work because the application port is not published to the host.
|
||||
|
||||
## 5. Configure Caddy
|
||||
|
||||
Add this site block to the existing Caddyfile in `~/stacks/caddy/`:
|
||||
|
||||
```caddyfile
|
||||
steady.bujour.de {
|
||||
reverse_proxy steady:5000
|
||||
}
|
||||
```
|
||||
|
||||
Ensure the Caddy service also joins the external `proxy` network. Validate and
|
||||
reload Caddy using its actual Compose service name (the examples assume
|
||||
`caddy`):
|
||||
|
||||
```bash
|
||||
cd ~/stacks/caddy
|
||||
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
|
||||
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
|
||||
```
|
||||
|
||||
Caddy obtains and renews the TLS certificate automatically after DNS and ports
|
||||
80/443 are correct.
|
||||
|
||||
## 6. Verify the public deployment
|
||||
|
||||
```bash
|
||||
curl -fsS https://steady.bujour.de/health
|
||||
curl -I https://steady.bujour.de/
|
||||
```
|
||||
|
||||
Confirm the health JSON, a valid HTTPS certificate, and security headers such as
|
||||
`Strict-Transport-Security`, `Content-Security-Policy`, and
|
||||
`X-Content-Type-Options`. Then register an account and exercise a normal task
|
||||
workflow in the browser.
|
||||
|
||||
## Updates
|
||||
|
||||
Back up first, then pull, rebuild, and replace the service:
|
||||
|
||||
```bash
|
||||
cd ~/stacks/steady
|
||||
docker compose exec -T steady python -c "import sqlite3; source=sqlite3.connect('/app/instance/steady.db'); backup=sqlite3.connect('/app/instance/steady-backup.db'); source.backup(backup); backup.close(); source.close()"
|
||||
git pull --ff-only
|
||||
docker compose build --pull
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
docker compose logs --tail=100 steady
|
||||
```
|
||||
|
||||
Copy the backup off the named volume to protected storage as part of the VPS
|
||||
backup process. The in-volume backup is only an immediate pre-update snapshot.
|
||||
Test restore procedures regularly.
|
||||
|
||||
## Operations and rollback
|
||||
|
||||
- Probe `https://steady.bujour.de/health` for process availability.
|
||||
- View logs with `docker compose logs --tail=200 steady` or add `-f` to follow.
|
||||
- Restart with `docker compose restart steady`.
|
||||
- Keep `FEATURE_ADMIN=false` unless the guarded, data-free placeholder is needed.
|
||||
- Never expose Flask/Gunicorn directly or use `flask run` in production.
|
||||
- Rotate `SECRET_KEY` only when intentionally invalidating all sessions.
|
||||
|
||||
Application rollback requires both the prior Git revision/image and a compatible
|
||||
database backup. Do not run destructive migration downgrades without reviewing
|
||||
the migration and recovery plan.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Compose says `SECRET_KEY` is missing:** create `.env` as shown above.
|
||||
- **Container restarts:** inspect `docker compose logs steady`; migration or
|
||||
configuration errors appear before Gunicorn starts.
|
||||
- **Caddy returns 502:** verify both containers join `proxy`, the service is
|
||||
healthy, and the upstream is exactly `steady:5000`.
|
||||
- **Host rejected with HTTP 400:** make `TRUSTED_HOSTS` match the public hostname.
|
||||
- **No certificate:** verify public DNS and inbound ports 80/443; inspect Caddy's
|
||||
logs, not the application logs.
|
||||
- **Permission error for SQLite:** verify `/app/instance` is the `steady_data`
|
||||
volume. The image runs as the unprivileged user `steady` (UID 10001).
|
||||
|
|
|
|||
17
Dockerfile
17
Dockerfile
|
|
@ -1,7 +1,4 @@
|
|||
# Base image: matches Python 3.12. Change this if you tested locally on a
|
||||
# different version — a mismatch can cause subtle bugs that don't show up
|
||||
# until production (e.g. dependency wheels built for the wrong version).
|
||||
FROM python:3.12-slim
|
||||
FROM python:3.14-slim
|
||||
|
||||
# All subsequent commands run from here inside the container.
|
||||
WORKDIR /app
|
||||
|
|
@ -12,10 +9,16 @@ WORKDIR /app
|
|||
# edited your Python files. Copying everything first would bust that cache
|
||||
# on every single code change.
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN python -m pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Now copy the rest of the application code.
|
||||
COPY . .
|
||||
RUN useradd --create-home --uid 10001 steady \
|
||||
&& mkdir -p /app/instance \
|
||||
&& chown -R steady:steady /app
|
||||
|
||||
COPY --chown=steady:steady . .
|
||||
|
||||
USER steady
|
||||
|
||||
# Documents which port the app listens on. This does NOT actually publish
|
||||
# the port anywhere — that happens in compose.yaml via `expose`.
|
||||
|
|
@ -25,4 +28,4 @@ EXPOSE 5000
|
|||
# that's Flask's single-threaded development server, not meant to handle
|
||||
# real traffic or run unattended. gunicorn is a proper WSGI server.
|
||||
# "wsgi:app" means: import the `app` object from wsgi.py.
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "wsgi:app"]
|
||||
CMD ["gunicorn", "--config", "gunicorn.conf.py", "wsgi:app"]
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -10,6 +10,8 @@ theme, reading style, completion sound, and future reminder frequency.
|
|||
|
||||
## Local setup
|
||||
|
||||
Python 3.14 is the development and container runtime used by this repository.
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
|
@ -81,8 +83,15 @@ status, and due-date query path. Dynamic responses are not cached; static assets
|
|||
use a bounded cache; and responses include CSP, framing, MIME, referrer,
|
||||
permissions, and cross-origin policies.
|
||||
|
||||
Use [DEPLOYMENT.md](DEPLOYMENT.md) for Gunicorn, environment, TLS/reverse-proxy,
|
||||
migration, backup, health-check, and rollback guidance. Use
|
||||
The production stack uses Docker Compose behind Caddy at
|
||||
`https://steady.bujour.de`. In summary: clone the repository to
|
||||
`~/stacks/steady`, create a private `.env` containing `SECRET_KEY`, start with
|
||||
`docker compose up -d --build`, add `reverse_proxy steady:5000` to Caddy, and
|
||||
verify `/health`. The application port is private and must not be published.
|
||||
|
||||
Follow [DEPLOYMENT.md](DEPLOYMENT.md) for the complete copy-paste deployment,
|
||||
update, TLS/reverse-proxy, migration, backup, verification, troubleshooting, and
|
||||
rollback procedure. Use
|
||||
[ACCESSIBILITY.md](ACCESSIBILITY.md) for automated evidence and the mandatory
|
||||
manual browser, keyboard, zoom, forced-colors, and screen-reader release matrix.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from pathlib import Path
|
|||
from typing import Any
|
||||
|
||||
from flask import Flask
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
from config import CONFIGS
|
||||
|
||||
|
|
@ -33,6 +34,7 @@ def create_app(
|
|||
|
||||
Path(app.instance_path).mkdir(parents=True, exist_ok=True)
|
||||
_validate_config(app)
|
||||
_configure_proxy_headers(app)
|
||||
_initialize_extensions(app)
|
||||
_register_blueprints(app)
|
||||
configure_security_headers(app)
|
||||
|
|
@ -45,6 +47,21 @@ def _validate_config(app: Flask) -> None:
|
|||
raise RuntimeError("SECRET_KEY must be set outside development and testing.")
|
||||
|
||||
|
||||
def _configure_proxy_headers(app: Flask) -> None:
|
||||
"""Trust one reverse proxy only when explicitly enabled.
|
||||
|
||||
The production container is not published on a host port, so its only HTTP
|
||||
peer on the Docker network is the Caddy reverse proxy.
|
||||
"""
|
||||
if app.config.get("TRUST_PROXY_HEADERS"):
|
||||
app.wsgi_app = ProxyFix(
|
||||
app.wsgi_app,
|
||||
x_for=1,
|
||||
x_proto=1,
|
||||
x_host=1,
|
||||
)
|
||||
|
||||
|
||||
def _initialize_extensions(app: Flask) -> None:
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
|
|
|
|||
29
compose.yaml
29
compose.yaml
|
|
@ -1,22 +1,39 @@
|
|||
services:
|
||||
steady:
|
||||
build: /home/patsy/apps/steady
|
||||
build: .
|
||||
container_name: steady
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- STEADY_ENV=production
|
||||
- SECRET_KEY=${SECRET_KEY}
|
||||
STEADY_ENV: production
|
||||
SECRET_KEY: ${SECRET_KEY:?Set SECRET_KEY in the VPS .env file}
|
||||
DATABASE_URL: sqlite:////app/instance/steady.db
|
||||
TRUSTED_HOSTS: ${TRUSTED_HOSTS:-steady.bujour.de}
|
||||
TRUST_PROXY_HEADERS: "true"
|
||||
GUNICORN_BIND: 0.0.0.0:5000
|
||||
volumes:
|
||||
- steady_data:/app/instance
|
||||
networks:
|
||||
- proxy
|
||||
expose:
|
||||
- "8020"
|
||||
- "5000"
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- flask --app app db upgrade && exec gunicorn --config gunicorn.conf.py wsgi:app
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- python
|
||||
- -c
|
||||
- "import urllib.request; urllib.request.urlopen('http://127.0.0.1:5000/health', timeout=3)"
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
|
||||
volumes:
|
||||
steady_data:
|
||||
a
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ class Config:
|
|||
WTF_CSRF_TIME_LIMIT = 3600
|
||||
MAX_CONTENT_LENGTH = 2 * 1024 * 1024
|
||||
TRUSTED_HOSTS = environment_list("TRUSTED_HOSTS")
|
||||
TRUST_PROXY_HEADERS = environment_flag("TRUST_PROXY_HEADERS")
|
||||
ENABLE_HSTS = False
|
||||
|
||||
FEATURE_ADMIN = environment_flag("FEATURE_ADMIN")
|
||||
|
|
|
|||
108
flask_app_deploy_prompt.md
Normal file
108
flask_app_deploy_prompt.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# Flask App VPS Deployment — LLM Prompt
|
||||
|
||||
> **Purpose:** Paste this into a chat with a local model (e.g. `architect` or
|
||||
> `debug` via Cline) when preparing to deploy a new Flask app to the VPS.
|
||||
> It describes the established pattern, confirmed working end-to-end with
|
||||
> the `better_todo` app (deployed as `steady.bujour.de`).
|
||||
|
||||
---
|
||||
|
||||
## Context: the VPS deployment pattern
|
||||
|
||||
- **Reverse proxy:** Caddy, handles HTTP/HTTPS only, automatic TLS via Let's Encrypt (HTTP-01 challenge). No manual certificate steps — Caddy requests, stores, and renews certs itself, as long as DNS points at the VPS and ports 80/443 are reachable.
|
||||
- **Isolation:** Docker Compose, one app per subdirectory under `~/stacks/` on the VPS.
|
||||
- **Shared network:** an external Docker network named `proxy` — every app container and Caddy join it, so Caddy can reach containers by name without publishing their ports to the internet.
|
||||
- **Own-app pattern (not a pre-built image):** the app's own git repo contains both `Dockerfile` and `compose.yaml`, committed and pushed like any other file. The VPS deploys by cloning that repo directly into `~/stacks/<appname>/` — both files arrive together, already in the right relative location. No copying config files around separately.
|
||||
- **Port exposure:** `expose:` in compose.yaml (visible only to other containers on `proxy`), never `ports:` — Caddy is the only thing that should reach the app directly. `ports:` is only needed for non-HTTP protocols (e.g. Forgejo's SSH on 2222), which requires opening the port in *both* UFW and the IONOS cloud firewall.
|
||||
|
||||
## Standard inputs needed before generating files
|
||||
|
||||
Ask for these if not already known:
|
||||
1. App/repo name (used as container name and `~/stacks/` subfolder name)
|
||||
2. Public subdomain (e.g. `steady.bujour.de`) — separate from the repo/container name, no need to match
|
||||
3. Python version used during local development
|
||||
4. Entry point — confirm app-factory pattern (`wsgi.py` importing `app = create_app()`)
|
||||
5. Contents of `requirements.txt` — specifically, is a production WSGI server (`gunicorn`) already listed? Never add a redundant `pip install gunicorn` line if it's already in requirements.txt.
|
||||
6. Does the app hold persistent data (SQLite file, uploads, etc.)?
|
||||
7. If SQLite: the **exact** resolved path of the DB file inside the container. Don't assume Flask's default `instance_path` — trace it from the actual config. Find `BASE_DIR` (or equivalent) in the config file, confirm how many `.parent` calls it uses and where that config file sits relative to `wsgi.py`. If `wsgi.py` and the config are both at repo root, `BASE_DIR` = `/app` inside the container (since `Dockerfile` does `COPY . .` from repo root into `/app`), so a `sqlite:///{BASE_DIR}/instance/x.db` pattern resolves to `/app/instance/x.db`. If either file lives in a subfolder, adjust accordingly.
|
||||
8. Port the app listens on (gunicorn's `--bind` target)
|
||||
|
||||
## Dockerfile template
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.12-slim # match confirmed local Python version
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 5000 # match confirmed app port
|
||||
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "wsgi:app"]
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `requirements.txt` copied and installed *before* the rest of the app — preserves Docker's layer cache so code-only changes don't re-trigger a full pip install.
|
||||
- Never use `flask run` / `app.run()` as the production `CMD` — single-threaded dev server, not meant for real traffic.
|
||||
|
||||
## compose.yaml template
|
||||
|
||||
```yaml
|
||||
services:
|
||||
<appname>:
|
||||
build: .
|
||||
container_name: <appname>
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- <appname>_data:/app/<resolved instance path>
|
||||
networks:
|
||||
- proxy
|
||||
expose:
|
||||
- "<port>"
|
||||
|
||||
volumes:
|
||||
<appname>_data:
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
```
|
||||
|
||||
## Caddy site block template
|
||||
|
||||
```
|
||||
<subdomain>.bujour.de {
|
||||
reverse_proxy <appname>:<port>
|
||||
}
|
||||
```
|
||||
|
||||
Added to the existing Caddyfile under `~/stacks/caddy/`, then Caddy reloaded.
|
||||
|
||||
## Deployment steps (VPS side)
|
||||
|
||||
1. Confirm DNS: subdomain covered by wildcard, or new A record added in IONOS.
|
||||
2. `git clone <repo-url> ~/stacks/<appname>`
|
||||
3. Add the Caddy site block, reload Caddy.
|
||||
4. `cd ~/stacks/<appname> && docker compose up -d`
|
||||
5. Check logs: `docker compose logs -f`
|
||||
6. Test internally: `curl localhost:<port>` (bypasses Caddy/DNS — isolates app vs. proxy problems)
|
||||
7. Test externally: `https://<subdomain>.bujour.de` — confirm valid HTTPS, not just that it loads
|
||||
8. Log the new app in `VPS_Setup_Guide.md`: add row to "Current Stack on VPS" table, add bullet under "What Is Confirmed Working"
|
||||
|
||||
## Worked example on file: `better_todo` → `steady.bujour.de`
|
||||
|
||||
- Repo: `better_todo`, container name `better_todo`, subdomain `steady.bujour.de`
|
||||
- `wsgi.py` at repo root, `app = create_app()`
|
||||
- `gunicorn` already present in `requirements.txt`
|
||||
- Config: `SQLALCHEMY_DATABASE_URI = f"sqlite:///{BASE_DIR / 'instance' / 'steady.db'}"`, with `BASE_DIR = Path(__file__).resolve().parent` — config file also at repo root
|
||||
- Resolved container path: `/app/instance/steady.db` → volume `better_todo_data:/app/instance`
|
||||
- Port: 5000
|
||||
- Confirmed working end-to-end (internal curl + external HTTPS test both passed)
|
||||
|
||||
---
|
||||
|
||||
**Getan:** Reusable LLM deployment prompt written, generalizing the `better_todo`/`steady` deployment into a template for future Flask apps.
|
||||
**Gelernt:** SQLite path resolution must be traced from the actual `BASE_DIR` definition and its file's location relative to `wsgi.py` — never assumed from Flask's default `instance_path`.
|
||||
|
|
@ -38,6 +38,26 @@ def test_production_hsts_is_https_only():
|
|||
assert "Strict-Transport-Security" not in insecure.headers
|
||||
|
||||
|
||||
def test_trusted_proxy_marks_forwarded_request_secure():
|
||||
app = create_app(
|
||||
"production",
|
||||
{
|
||||
"SECRET_KEY": "production-test-only",
|
||||
"SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:",
|
||||
"TRUST_PROXY_HEADERS": True,
|
||||
},
|
||||
)
|
||||
client = app.test_client()
|
||||
|
||||
response = client.get(
|
||||
"/health",
|
||||
headers={"X-Forwarded-Proto": "https", "X-Forwarded-Host": "steady.example"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Strict-Transport-Security" in response.headers
|
||||
|
||||
|
||||
def test_trusted_hosts_reject_unknown_host():
|
||||
app = create_app(
|
||||
"testing",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue