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

5.4 KiB

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

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

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_todosteady.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.