another deployment update

This commit is contained in:
2026-07-26 12:44:50 +02:00
parent d642e0e2b8
commit 73380b0c07
2 changed files with 200 additions and 12 deletions
+198 -11
View File
@@ -74,12 +74,6 @@ The other settings remain environment-based:
| `FLOWERS_ACCESS_FILE` | `/tmp/wsgi_flower_accessfile` |
| `FLOWERS_LOG_FILE` | `/tmp/wsgi_flowers.log` |
For example:
```bash
waitress-serve --threads=6 --host=127.0.0.1 --port=5012 --call flowers:create_app
```
The provisioning account is needed only by the web-based `/create` flow. Existing vault reads and writes connect with their existing per-user database accounts.
## Database bootstrap for a new installation
@@ -97,7 +91,7 @@ FLUSH PRIVILEGES;
Put the same value in `database.admin_password` in `secrets.yaml` before starting Flowers.
## Run
## Run locally
Development:
@@ -105,13 +99,204 @@ Development:
python flowers.py
```
Production application server:
To exercise the production WSGI server locally:
```bash
waitress-serve --threads=6 --host=127.0.0.1 --port=5012 --call flowers:create_app
gunicorn --workers 1 --threads 6 --bind 127.0.0.1:5012 'flowers:create_app()'
```
Keep the service behind HTTPS. If it is mounted under `/flowers`, set `FLOWERS_URL_PREFIX=/flowers` before starting it.
Flowers keeps authenticated database credentials in process memory. Always use exactly
one Gunicorn worker: users would otherwise appear to be logged out whenever a request
reached a different worker. Threads provide concurrency within that worker. Restarting
Gunicorn intentionally expires all active Flowers sessions.
## Deploy with Nginx, Gunicorn, and systemd
The following example installs Flowers in `/opt/flowers`, runs it as an unprivileged
`flowers` user, binds Gunicorn only to loopback, and exposes it through Nginx over
HTTPS. Adapt the paths, hostname, user, and certificate locations for your server.
### 1. Install the application
Install Python, its virtual-environment support, MariaDB/MySQL client libraries, Nginx,
and your distribution's certificate tooling. Then copy or clone the repository and
create the service account and virtual environment:
```bash
sudo useradd --system --user-group --home /opt/flowers \
--shell /usr/sbin/nologin flowers
sudo install -d -o flowers -g flowers /opt/flowers
sudo -u flowers git clone https://example.invalid/flowers.git /opt/flowers
sudo -u flowers python3 -m venv /opt/flowers/.venv
sudo -u flowers /opt/flowers/.venv/bin/pip install \
-r /opt/flowers/requirements.txt
```
Replace the example clone URL with this repository's URL. For an artifact-based
deployment, copy the release into `/opt/flowers` instead and make it owned by
`flowers:flowers`.
### 2. Configure secrets and the environment
Keep deployment secrets outside the repository:
```bash
sudo install -d -m 0750 -o root -g flowers /etc/flowers
sudo install -m 0640 -o root -g flowers \
/opt/flowers/secrets.example.yaml /etc/flowers/secrets.yaml
sudoedit /etc/flowers/secrets.yaml
sudoedit /etc/flowers/flowers.env
sudo chown root:flowers /etc/flowers/flowers.env
sudo chmod 0640 /etc/flowers/flowers.env
```
Use this as `/etc/flowers/flowers.env`:
```dotenv
FLOWERS_SECRETS_FILE=/etc/flowers/secrets.yaml
FLOWERS_DB_HOST=127.0.0.1
FLOWERS_DB_PORT=3306
FLOWERS_DB_NAME=Flowers
FLOWERS_DB_ADMIN_USER=flower
FLOWERS_COOKIE_SECURE=1
FLOWERS_ACCESS_FILE=/var/lib/flowers/access
FLOWERS_LOG_FILE=/var/log/flowers/flowers.log
```
Create the writable locations referenced above:
```bash
sudo install -d -m 0750 -o flowers -g flowers /var/lib/flowers
sudo install -d -m 0750 -o flowers -g flowers /var/log/flowers
```
Set a stable, random `flask.secret_key` and the database provisioning password in
`/etc/flowers/secrets.yaml`. Do not generate a new Flask secret on each deployment,
because changing it invalidates every browser session. If the database is on another
host, adjust `FLOWERS_DB_HOST` and ensure its grants and firewall allow the Flowers
server.
### 3. Create the systemd service
Create `/etc/systemd/system/flowers.service`:
```ini
[Unit]
Description=Flowers credential vault
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=flowers
Group=flowers
WorkingDirectory=/opt/flowers
EnvironmentFile=/etc/flowers/flowers.env
ExecStart=/opt/flowers/.venv/bin/gunicorn --workers 1 --threads 6 --bind 127.0.0.1:5012 --access-logfile - --error-logfile - "flowers:create_app()"
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
PrivateTmp=true
NoNewPrivileges=true
UMask=0077
[Install]
WantedBy=multi-user.target
```
Enable the service and confirm that Gunicorn answers locally:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now flowers
sudo systemctl status flowers
curl --fail --head http://127.0.0.1:5012/
```
Service and access logs are available through `journalctl -u flowers`. The application
also writes its own log to the configured `FLOWERS_LOG_FILE`.
### 4. Configure Nginx
Create `/etc/nginx/sites-available/flowers` (or the equivalent include path on your
distribution):
```nginx
server {
listen 80;
listen [::]:80;
server_name flowers.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name flowers.example.com;
ssl_certificate /etc/letsencrypt/live/flowers.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/flowers.example.com/privkey.pem;
client_max_body_size 64k;
location / {
proxy_pass http://127.0.0.1:5012;
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;
proxy_set_header Connection "";
proxy_read_timeout 30s;
}
}
```
Obtain the certificate before enabling the TLS server, or first use your ACME client's
HTTP-only bootstrap configuration. Then enable and validate the site:
```bash
sudo ln -s /etc/nginx/sites-available/flowers /etc/nginx/sites-enabled/flowers
sudo nginx -t
sudo systemctl reload nginx
curl --fail --head https://flowers.example.com/
```
Keep port `5012` closed in the host firewall; only ports 80 and 443 need to be public.
If another reverse proxy sits in front of Nginx, configure Nginx to accept client IP
headers only from that proxy rather than from arbitrary clients.
### Deploy under a URL prefix
To publish Flowers at `https://example.com/flowers/`, add this setting to
`/etc/flowers/flowers.env`:
```dotenv
FLOWERS_URL_PREFIX=/flowers
```
Use these Nginx locations without a trailing path on `proxy_pass`, so the `/flowers`
prefix reaches Flask unchanged:
```nginx
location = /flowers {
return 301 /flowers/;
}
location /flowers/ {
proxy_pass http://127.0.0.1:5012;
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;
proxy_set_header Connection "";
}
```
After changing the environment or application code, restart Gunicorn with
`sudo systemctl restart flowers`. After changing Nginx, run `sudo nginx -t` before
reloading it.
## Tests
@@ -137,4 +322,6 @@ The rewrite removes the immediately exploitable SQL interpolation and browser-co
- the in-memory credential store is suitable for one application process, not a multi-process cluster; and
- rate-limit state is local to one host.
Use a strong unique password made only of Base64-compatible characters, bind Waitress to localhost, and expose it only through an authenticated HTTPS reverse proxy. Set stable provisioning and Flask secrets before deployment.
Use a strong unique password made only of Base64-compatible characters, bind Gunicorn
to localhost, and expose it only through an authenticated HTTPS reverse proxy. Set
stable provisioning and Flask secrets before deployment.