diff --git a/.DS_Store b/.DS_Store index efe8a73..0191257 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/README.md b/README.md index 01131c7..422decb 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,224 @@ flask: Set `URL_PREFIX` in the environment to override this value at deployment time. Use an empty value (`url_prefix: ""` or `URL_PREFIX=`) to serve from `/` instead. -The reverse proxy must preserve the prefix when forwarding requests to Waitress. +The reverse proxy must preserve the prefix when forwarding requests to the WSGI +server (Waitress or Gunicorn). + +## Production deployment: Nginx → Gunicorn → Fatima + +The following example targets a Debian/Ubuntu server using: + +- `/srv/fatima` for the application and virtual environment +- `/etc/fatima/config.yaml` for secrets and application configuration +- `/var/lib/fatima/calendar.sqlite` for persistent data +- `/run/fatima/gunicorn.sock` for the private Gunicorn socket +- `fatima.service` to run the application as an unprivileged `fatima` user + +Replace `calendar.example.com` and the sample credentials before exposing the +site. The examples assume the application remains mounted at `/fatima`. + +### 1. Install the application + +Install the operating-system packages and create a service account: + +```sh +sudo apt update +sudo apt install nginx python3 python3-venv +sudo useradd --system --user-group --home-dir /srv/fatima --shell /usr/sbin/nologin fatima +``` + +Copy or clone the repository into `/srv/fatima`, then create the virtual +environment and install the dependencies. Gunicorn is an additional production +dependency; Waitress remains suitable for the local command documented above. + +```sh +sudo chown -R fatima:fatima /srv/fatima +sudo -u fatima python3 -m venv /srv/fatima/.venv +sudo -u fatima /srv/fatima/.venv/bin/pip install -r /srv/fatima/requirements.txt gunicorn +``` + +For repeatable deployments, pin the Gunicorn version used by the server in your +deployment tooling or requirements file. + +### 2. Create the production configuration + +Generate a signing key: + +```sh +python3 -c "import secrets; print(secrets.token_hex(32))" +``` + +Create `/etc/fatima/config.yaml` using `config.example.yaml` as a guide: + +```sh +sudo install -d -o root -g fatima -m 750 /etc/fatima +``` + +```yaml +flask: + secret_key: "paste-the-generated-random-value-here" + url_prefix: "/fatima" + log_file: "/var/log/fatima/startup.log" + database: "/var/lib/fatima/calendar.sqlite" + +auth: + username: "replace-with-the-login-name" + password: "replace-with-a-strong-password" + +amounts: + presets: + - "55" + - "27.50" +``` + +Protect the file because it contains the login password and Flask signing key: + +```sh +sudo chown root:fatima /etc/fatima/config.yaml +sudo chmod 640 /etc/fatima/config.yaml +``` + +The systemd unit below creates `/var/lib/fatima`, `/var/log/fatima`, and +`/run/fatima` with ownership suitable for the service. If an existing database +is copied into `/var/lib/fatima`, make sure it and its parent directory are +writable by `fatima`; SQLite may create journal files beside the database. + +### 3. Run Gunicorn with systemd + +Create `/etc/systemd/system/fatima.service`: + +```ini +[Unit] +Description=Fatima calendar +After=network.target + +[Service] +Type=simple +User=fatima +Group=www-data +SupplementaryGroups=fatima +WorkingDirectory=/srv/fatima +Environment=APP_CONFIG=/etc/fatima/config.yaml +ExecStart=/srv/fatima/.venv/bin/gunicorn \ + --workers 2 \ + --bind unix:/run/fatima/gunicorn.sock \ + --umask 007 \ + --access-logfile - \ + --error-logfile - \ + flask_fatima:app +Restart=on-failure +RestartSec=5 +RuntimeDirectory=fatima +RuntimeDirectoryMode=0750 +StateDirectory=fatima +StateDirectoryMode=0750 +LogsDirectory=fatima +LogsDirectoryMode=0750 +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true + +[Install] +WantedBy=multi-user.target +``` + +`Group=www-data` and `--umask 007` allow Nginx to connect to the Unix socket +without exposing it to other users. `APP_CONFIG` makes the application load the +production YAML file rather than `/srv/fatima/config.yaml`. + +Load and start the service: + +```sh +sudo systemctl daemon-reload +sudo systemctl enable --now fatima +sudo systemctl status fatima +sudo journalctl -u fatima -n 50 --no-pager +``` + +The startup log should show the resolved production database path. Before +configuring Nginx, verify Gunicorn directly: + +```sh +sudo -u www-data curl --unix-socket /run/fatima/gunicorn.sock \ + --head http://localhost/fatima/ +``` + +A redirect to `/fatima/login` is the expected response when no authenticated +session exists. + +### 4. Proxy `/fatima` through Nginx + +Create `/etc/nginx/sites-available/fatima`: + +```nginx +upstream fatima_gunicorn { + server unix:/run/fatima/gunicorn.sock; +} + +server { + listen 80; + listen [::]:80; + server_name calendar.example.com; + + location = /fatima { + return 301 /fatima/; + } + + location /fatima/ { + proxy_pass http://fatima_gunicorn; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +There is deliberately no URI after `proxy_pass`: Nginx must forward +`/fatima/...` unchanged because Fatima's prefix middleware removes that prefix +before Flask routes the request. Do not rewrite it to `/`. + +Enable and validate the site: + +```sh +sudo ln -s /etc/nginx/sites-available/fatima /etc/nginx/sites-enabled/fatima +sudo nginx -t +sudo systemctl reload nginx +curl --head http://calendar.example.com/fatima/ +``` + +Add TLS before exposing the login publicly. Once the certificate is configured, +redirect HTTP to HTTPS and keep the same `/fatima/` proxy block in the TLS +server. The application stores authentication in a signed browser session, so +keep `secret_key` stable across restarts and deployments; changing it logs out +all existing sessions. + +### Updating and troubleshooting + +After deploying new source or dependencies: + +```sh +sudo -u fatima /srv/fatima/.venv/bin/pip install -r /srv/fatima/requirements.txt +sudo systemctl restart fatima +sudo systemctl reload nginx +``` + +Useful checks: + +```sh +sudo journalctl -u fatima -f +sudo tail -f /var/log/nginx/error.log +sudo nginx -t +sudo systemctl status fatima nginx +``` + +If Nginx returns `502 Bad Gateway`, confirm that `fatima.service` is running and +that `www-data` can access `/run/fatima/gunicorn.sock`. If the app starts but +cannot save calendar entries, check ownership of `/var/lib/fatima` and confirm +the startup summary names `/var/lib/fatima/calendar.sqlite`. ## Import the Excel calendar diff --git a/requirements.txt b/requirements.txt index 4a04aaf..4f4e2d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ Flask==3.1.1 PyYAML==6.0.2 openpyxl==3.1.5 -waitress==3.0.2 +gunicorn + diff --git a/static/wwwsuynl.png b/static/wwwsuynl.png deleted file mode 100644 index d4a2d46..0000000 Binary files a/static/wwwsuynl.png and /dev/null differ