flask_template_codex/flask_app_deploy_prompt.md
2026-08-08 06:08:24 +02:00

108 lines
5.4 KiB
Markdown

# 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`.