From 3684d984563d91723e4ce9401bf8df9ee59ac0ea Mon Sep 17 00:00:00 2001 From: bramval Date: Fri, 23 Jan 2026 22:23:31 +0100 Subject: [PATCH] prodv1 --- .dockerignore | 22 +++++++++++ Dockerfile | 30 ++++++++++++++ README.md | 98 ++++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 16 ++++++++ wsgi.py | 34 ++++++++++++++++ 5 files changed, 200 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 wsgi.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9dba41d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd + +.git/ +.gitignore + +.venv/ +venv/ + +instance/ +app/static/uploads/ + +.env +.flaskenv + +*.sqlite + +scripts/ + +README.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3f64def --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +# System deps (kept minimal). Pillow may need some libs; for most cases this is fine on slim. +# If you hit Pillow build/runtime issues, consider adding: libjpeg62-turbo, zlib1g, etc. + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt \ + && pip install --no-cache-dir gunicorn + +COPY . . + +# Create runtime dirs (also mountable as volumes) +RUN mkdir -p instance app/static/uploads + +EXPOSE 8000 + +# Default config (override at runtime) +ENV FLASK_ENV=production \ + GUNICORN_WORKERS=2 \ + GUNICORN_BIND=0.0.0.0:8000 + +# Run via WSGI entrypoint +CMD ["sh", "-c", "gunicorn -w ${GUNICORN_WORKERS} -b ${GUNICORN_BIND} wsgi:app"] diff --git a/README.md b/README.md index 1e43d49..08c314c 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,100 @@ flask run --debug Open http://127.0.0.1:5000 +## Production (WSGI) + +This repo includes a `wsgi.py` entrypoint for production WSGI servers. + +### Important (Windows) + +If you try to run Gunicorn directly on Windows you will see an error like: + +``` +ModuleNotFoundError: No module named 'fcntl' +``` + +That’s expected: **Gunicorn is Unix-only**. On Windows, run the app via: + +- **Docker** (recommended) so Gunicorn runs inside a Linux container, or +- **WSL2/Linux** (Gunicorn works), or +- use a Windows-native WSGI server (e.g. Waitress) instead of Gunicorn. + +Examples: + +```bash +# gunicorn (Linux) +gunicorn -w 4 -b 0.0.0.0:8000 wsgi:app + +# uWSGI +uwsgi --http :8000 --wsgi-file wsgi.py --callable app +``` + +Note: unlike `flask run`, WSGI servers typically don't auto-load `.env` / `.flaskenv`. +`wsgi.py` attempts to load `.env` (best-effort), but for real production you should set +environment variables via your process manager / secrets. + +## Docker + +### Docker Compose (recommended) + +This repo includes a `docker-compose.yml` for a one-command startup. + +```powershell +docker compose up --build +``` + +Run in the background: + +```powershell +docker compose up -d --build +``` + +Stop: + +```powershell +docker compose down +``` + +Data persistence: + +- SQLite DB is mounted to `./instance` on your host +- uploads are mounted to `./app/static/uploads` on your host + +Build: + +```bash +docker build -t signage:latest . +``` + +Run (with persistent SQLite DB + uploads): + +```bash +docker run --rm -p 8000:8000 \ + -e SECRET_KEY="change-me" \ + -v %cd%/instance:/app/instance \ + -v %cd%/app/static/uploads:/app/app/static/uploads \ + signage:latest +``` + +PowerShell variant (sometimes volume path quoting is easier): + +```powershell +docker run --rm -p 8000:8000 ` + -e SECRET_KEY="change-me" ` + -v "${PWD}/instance:/app/instance" ` + -v "${PWD}/app/static/uploads:/app/app/static/uploads" ` + signage:latest +``` + +Then open: http://127.0.0.1:8000 + +Notes: + +- The container starts with Gunicorn using `wsgi:app`. +- You can override Gunicorn settings via env vars: + - `GUNICORN_WORKERS` (default: 2) + - `GUNICORN_BIND` (default: `0.0.0.0:8000`) + ## Notes - SQLite DB is stored at `instance/signage.sqlite`. @@ -121,3 +215,7 @@ If the reset email is not received: + + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4a057c8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + web: + build: . + image: signage:latest + ports: + - "8000:8000" + environment: + # Override in a .env file or your shell; this default is only for convenience. + SECRET_KEY: "change-me" + # Optional overrides (the Dockerfile already defaults these) + GUNICORN_WORKERS: "2" + GUNICORN_BIND: "0.0.0.0:8000" + volumes: + # Persist SQLite DB and uploads on the host + - ./instance:/app/instance + - ./app/static/uploads:/app/app/static/uploads diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..97bca3a --- /dev/null +++ b/wsgi.py @@ -0,0 +1,34 @@ +"""WSGI entrypoint for production servers (gunicorn/uWSGI/etc.). + +This file exposes a module-level WSGI callable named `app` (and `application`) +so common servers can run the project without relying on Flask's dev server. + +Examples: + gunicorn -w 4 -b 0.0.0.0:8000 wsgi:app + uwsgi --http :8000 --wsgi-file wsgi.py --callable app +""" + +from __future__ import annotations + +import os + + +# `flask run` loads .env/.flaskenv automatically via python-dotenv. +# Production WSGI servers typically *don't*, so we best-effort load `.env` here. +# In real production, prefer setting environment variables via your process manager. +try: + from dotenv import load_dotenv + + load_dotenv(os.environ.get("DOTENV_PATH", ".env"), override=False) +except Exception: + # If python-dotenv isn't installed (or any other issue occurs), continue. + pass + + +from app import create_app + + +app = create_app() + +# Some servers (and hosting platforms) look specifically for `application`. +application = app