support for gunicorn
This commit is contained in:
@@ -1,33 +1,400 @@
|
||||
# lapp
|
||||
# L@pp
|
||||
|
||||
list app
|
||||
L@pp is a small, mobile-friendly shared-list application built with Flask. It is
|
||||
designed primarily for grocery lists, but works just as well for chores, packing
|
||||
lists, or any other lightweight checklist.
|
||||
|
||||
install
|
||||
1. clone repo
|
||||
2. create subfolder "instance" to store database file
|
||||
3. for an initial instance, run `.venv/bin/python initialize_data.py`
|
||||
4. add the below config to your apache2 enabled site
|
||||
Users can create and share lists, add items with quantities and categories, and
|
||||
check items off from a phone-friendly interface. The included web app can also
|
||||
be installed as a Progressive Web App (PWA).
|
||||
|
||||
## Secrets and configuration
|
||||
## Features
|
||||
|
||||
LAPP stores its private configuration in `instance/secrets.yaml`. On first
|
||||
startup, missing values are generated automatically and the file permissions are
|
||||
set to `0600`.
|
||||
- Multiple lists per user
|
||||
- List sharing between registered users
|
||||
- Item quantities and units, including input such as `Milk 2 cartons`
|
||||
- Grocery categories with icons
|
||||
- Reusable suggestions for previously completed items
|
||||
- Bulk item entry
|
||||
- Responsive, installable PWA interface
|
||||
- User registration with group invitation keys
|
||||
- Administrator approval for new accounts
|
||||
- Session-based JSON API for companion clients
|
||||
- SQLite by default, with other SQLAlchemy database URLs supported
|
||||
|
||||
The file contains the Flask session key, API application key, database URI,
|
||||
initial account credentials, initial group secrets, and the Fernet key used by
|
||||
`test.py`. To change the database, edit `runtime.database_uri` in this file.
|
||||
## Requirements
|
||||
|
||||
Keep `runtime.secret_key` stable: changing it logs users out and invalidates
|
||||
existing auto-login cookies. `runtime.application_key` is used by
|
||||
`/api/validate_key`.
|
||||
- Python 3.9 or newer
|
||||
- `pip` and Python virtual-environment support
|
||||
|
||||
The entire `instance/` directory is ignored by Git. Back up `secrets.yaml`
|
||||
securely and never commit or share it.
|
||||
## Quick start
|
||||
|
||||
## Initial data secrets
|
||||
Clone the repository and enter its directory:
|
||||
|
||||
`initialize_data.py` creates initial users and groups only when the database has
|
||||
no users yet. Set the values under `initial_data` in `instance/secrets.yaml`
|
||||
before running it, or use the securely generated defaults. The script prints the
|
||||
location of the credentials when it creates the initial data.
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd lapp
|
||||
```
|
||||
|
||||
Create a virtual environment and install the dependencies:
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirement.txt
|
||||
```
|
||||
|
||||
Initialize the database with an administrator, a regular user, two groups, and
|
||||
some example lists:
|
||||
|
||||
```bash
|
||||
python initialize_data.py
|
||||
```
|
||||
|
||||
The command creates `instance/secrets.yaml` and `instance/app.db`. It does not
|
||||
print the generated passwords or registration keys; open
|
||||
`instance/secrets.yaml` locally to retrieve them. The seeded usernames are
|
||||
`admin` and `ignace`.
|
||||
|
||||
Start the development server:
|
||||
|
||||
```bash
|
||||
python lapp.py
|
||||
```
|
||||
|
||||
Then open <http://127.0.0.1:5001>. The built-in server runs in debug mode and is
|
||||
intended for local development only.
|
||||
|
||||
Running the application without `initialize_data.py` creates an empty database
|
||||
schema, but no users or registration groups.
|
||||
|
||||
## Configuration and secrets
|
||||
|
||||
Private configuration lives in `instance/secrets.yaml`. On first use, missing
|
||||
values are generated automatically and the file is restricted to mode `0600`.
|
||||
A generated file has this structure:
|
||||
|
||||
```yaml
|
||||
runtime:
|
||||
secret_key: <Flask session secret>
|
||||
application_key: <key used by /api/validate_key>
|
||||
database_uri: sqlite:///app.db
|
||||
initial_data:
|
||||
admin_password: <initial admin password>
|
||||
admin_group: <initial admin registration key>
|
||||
user_password: <initial user password>
|
||||
user_group: <initial user registration key>
|
||||
fernet:
|
||||
key: <Fernet key used by test.py>
|
||||
```
|
||||
|
||||
For SQLite, relative database paths are resolved inside Flask's `instance/`
|
||||
directory. To use another database, set `runtime.database_uri` to a compatible
|
||||
SQLAlchemy URL and install the corresponding database driver.
|
||||
|
||||
Keep `runtime.secret_key` stable. Replacing it invalidates active sessions and
|
||||
the seven-day auto-login cookie. The `runtime.application_key` is checked by
|
||||
`POST /api/validate_key`.
|
||||
|
||||
The complete `instance/` directory is ignored by Git. Back it up securely and
|
||||
never commit or share its contents. In particular, back up both `app.db` and
|
||||
`secrets.yaml` together when using the default SQLite setup.
|
||||
|
||||
### Customizing the initial accounts
|
||||
|
||||
To choose credentials instead of using generated values:
|
||||
|
||||
1. Generate the configuration and empty schema without seeding accounts:
|
||||
|
||||
```bash
|
||||
python -c 'from lapp import create_app; create_app()'
|
||||
```
|
||||
|
||||
2. Edit the values under `initial_data` in `instance/secrets.yaml`.
|
||||
3. Run `python initialize_data.py`.
|
||||
|
||||
Initialization is deliberately idempotent: if the database already contains a
|
||||
user, no seed data is added. It also refuses to initialize a partially populated
|
||||
database.
|
||||
|
||||
## How accounts and sharing work
|
||||
|
||||
Registration requires a group key. The two keys created during initialization
|
||||
are stored as `initial_data.admin_group` and `initial_data.user_group`. A newly
|
||||
registered account remains pending until an administrator approves it at
|
||||
`/admin/`.
|
||||
|
||||
After signing in, a user can create, activate, rename, share, or delete their own
|
||||
lists. A shared list is visible and editable by every selected user. Only the
|
||||
owner can change the list itself or its sharing settings.
|
||||
|
||||
Administrators can create additional registration groups from `/groups`.
|
||||
|
||||
## Item input
|
||||
|
||||
L@pp extracts simple quantities and units from item text. For example:
|
||||
|
||||
| Input | Label | Quantity | Unit |
|
||||
| --- | --- | ---: | --- |
|
||||
| `Apples 4` | Apples | 4 | `x` |
|
||||
| `Milk 2 cartons` | Milk | 2 | `cartons` |
|
||||
| `3 kg potatoes` | Potatoes | 3 | `kg` |
|
||||
|
||||
Cleaning a list moves checked items into its suggestion history. Those items can
|
||||
then be added again quickly from the add-items screen.
|
||||
|
||||
## API overview
|
||||
|
||||
API requests use form-encoded fields and return JSON. Authentication is stored
|
||||
in the Flask session cookie, so API clients must retain cookies after login.
|
||||
|
||||
| Method | Endpoint | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `POST` | `/api/validate_key` | Validate the configured application key (`key`) |
|
||||
| `POST` | `/api/login` | Sign in with `user` and `password` |
|
||||
| `POST` | `/api/register` | Register with `user` and a group key in `password` |
|
||||
| `POST` | `/api/load_lists` | Load lists available to the signed-in user |
|
||||
| `POST` | `/api/load_items` | Load active items for `listid` |
|
||||
|
||||
Responses use a numeric `status` field: `1` means success, `0` means failure,
|
||||
and registration uses `2` to indicate that approval is pending.
|
||||
|
||||
Example login with a cookie jar:
|
||||
|
||||
```bash
|
||||
curl -c cookies.txt \
|
||||
-X POST \
|
||||
-d 'user=ignace' \
|
||||
-d 'password=YOUR_PASSWORD' \
|
||||
http://127.0.0.1:5001/api/login
|
||||
|
||||
curl -b cookies.txt \
|
||||
-X POST \
|
||||
http://127.0.0.1:5001/api/load_lists
|
||||
```
|
||||
|
||||
## Production deployment with Nginx and Gunicorn
|
||||
|
||||
The following example targets Debian or Ubuntu, installs L@pp in `/srv/lapp`,
|
||||
and serves it from `lists.example.com`. Substitute your own installation path
|
||||
and domain where necessary.
|
||||
|
||||
Gunicorn loads the existing Flask application factory as
|
||||
`lapp:create_app()`. The repository's `lapp.wsgi` file is only needed for a
|
||||
mod_wsgi deployment and is not used here.
|
||||
|
||||
### 1. Install the system packages
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install nginx python3 python3-venv
|
||||
```
|
||||
|
||||
Create a dedicated, unprivileged service account:
|
||||
|
||||
```bash
|
||||
sudo useradd --system \
|
||||
--home /srv/lapp \
|
||||
--shell /usr/sbin/nologin \
|
||||
lapp
|
||||
```
|
||||
|
||||
Place or clone the repository at `/srv/lapp`, then install the application and
|
||||
Gunicorn:
|
||||
|
||||
```bash
|
||||
cd /srv/lapp
|
||||
|
||||
sudo python3 -m venv .venv
|
||||
sudo .venv/bin/pip install --upgrade pip
|
||||
sudo .venv/bin/pip install -r requirement.txt
|
||||
sudo .venv/bin/pip install 'gunicorn>=23,<24'
|
||||
```
|
||||
|
||||
Gunicorn should run as the service account, not as `root`, and should only be
|
||||
reachable through Nginx.
|
||||
|
||||
### 2. Initialize the application
|
||||
|
||||
Create the private instance directory with permissions that allow the service
|
||||
account to maintain the SQLite database:
|
||||
|
||||
```bash
|
||||
sudo install -d \
|
||||
-o lapp \
|
||||
-g www-data \
|
||||
-m 0750 \
|
||||
/srv/lapp/instance
|
||||
|
||||
sudo -u lapp /srv/lapp/.venv/bin/python \
|
||||
/srv/lapp/initialize_data.py
|
||||
```
|
||||
|
||||
View the generated passwords and registration keys locally:
|
||||
|
||||
```bash
|
||||
sudo -u lapp sed -n '1,120p' \
|
||||
/srv/lapp/instance/secrets.yaml
|
||||
```
|
||||
|
||||
Back up `instance/app.db` and `instance/secrets.yaml` together. Do not expose
|
||||
the `instance/` directory through Nginx.
|
||||
|
||||
### 3. Run Gunicorn with systemd
|
||||
|
||||
Create `/etc/systemd/system/lapp.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=L@pp service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=lapp
|
||||
Group=www-data
|
||||
WorkingDirectory=/srv/lapp
|
||||
|
||||
RuntimeDirectory=lapp
|
||||
RuntimeDirectoryMode=0750
|
||||
UMask=0007
|
||||
|
||||
ExecStart=/srv/lapp/.venv/bin/gunicorn \
|
||||
--workers 2 \
|
||||
--bind unix:/run/lapp/lapp.sock \
|
||||
--access-logfile - \
|
||||
--error-logfile - \
|
||||
lapp:create_app()
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
PrivateTmp=true
|
||||
NoNewPrivileges=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
One or two workers is a sensible starting point for this small SQLite
|
||||
application. Adding many workers can increase SQLite write contention.
|
||||
|
||||
Enable and start the service:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now lapp
|
||||
sudo systemctl status lapp
|
||||
```
|
||||
|
||||
Follow its logs or verify that its socket exists:
|
||||
|
||||
```bash
|
||||
sudo journalctl -u lapp -f
|
||||
sudo ls -l /run/lapp/lapp.sock
|
||||
```
|
||||
|
||||
### 4. Configure Nginx
|
||||
|
||||
Create `/etc/nginx/sites-available/lapp`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name lapp.suy.nl;
|
||||
|
||||
client_max_body_size 2m;
|
||||
|
||||
location /static/ {
|
||||
alias /srv/lapp/static/;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://unix:/run/lapp/lapp.sock;
|
||||
|
||||
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_read_timeout 60s;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Enable the site and validate the configuration before reloading Nginx:
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/lapp \
|
||||
/etc/nginx/sites-enabled/lapp
|
||||
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
The application should now be available at `http://lists.example.com`.
|
||||
|
||||
### 5. Enable HTTPS
|
||||
|
||||
Once the domain's DNS records point to the server, obtain a TLS certificate
|
||||
with your preferred ACME client. For example, using Certbot:
|
||||
|
||||
```bash
|
||||
sudo apt install certbot python3-certbot-nginx
|
||||
sudo certbot --nginx -d lists.example.com
|
||||
```
|
||||
|
||||
Do not expose L@pp publicly over plain HTTP: it handles passwords and persistent
|
||||
login cookies.
|
||||
|
||||
### Updating a deployment
|
||||
|
||||
Back up the database and secrets first. Then update the source and dependencies
|
||||
and restart Gunicorn:
|
||||
|
||||
```bash
|
||||
cd /srv/lapp
|
||||
sudo .venv/bin/pip install -r requirement.txt
|
||||
sudo systemctl restart lapp
|
||||
sudo systemctl status lapp
|
||||
```
|
||||
|
||||
See the official [Flask Gunicorn deployment guide](https://flask.palletsprojects.com/en/stable/deploying/gunicorn/),
|
||||
[Gunicorn documentation](https://docs.gunicorn.org/en/stable/run.html), and
|
||||
[Nginx proxy module documentation](https://nginx.org/en/docs/http/ngx_http_proxy_module.html)
|
||||
for additional configuration options.
|
||||
|
||||
## Utility scripts
|
||||
|
||||
- `initialize_data.py` safely seeds a new database.
|
||||
- `show_db.py` prints all database records for local diagnostics. Its output
|
||||
includes password hashes and group secrets, so treat it as sensitive.
|
||||
- `update_db.py` contains historical, manual migration snippets. Review and
|
||||
adapt it before running it against any database.
|
||||
- `test.py` is a Fernet encryption example, not an automated test suite.
|
||||
|
||||
## Project structure
|
||||
|
||||
```text
|
||||
lapp.py Application factory and blueprint registration
|
||||
auth.py Login, registration, logout, and password changes
|
||||
admin.py Account approval
|
||||
groups.py Registration-group management
|
||||
lists.py List creation, ownership, and sharing
|
||||
items.py Item entry, categories, quantities, and completion
|
||||
api.py Session-based JSON API
|
||||
models.py SQLAlchemy models
|
||||
templates/ Jinja templates
|
||||
static/ CSS, JavaScript, icons, and PWA manifest
|
||||
instance/ Local secrets and database (generated, ignored by Git)
|
||||
```
|
||||
|
||||
## Development notes
|
||||
|
||||
There is currently no automated test suite or migration framework. Before
|
||||
upgrading an existing installation, back up the database and secrets, then test
|
||||
the change on a copy of the data.
|
||||
|
||||
@@ -3,3 +3,4 @@ Flask-Login>=0.6,<1.0
|
||||
Flask-SQLAlchemy>=3.1,<4.0
|
||||
cryptography>=42,<46
|
||||
PyYAML>=6.0,<7.0
|
||||
gunicorn
|
||||
|
||||
Reference in New Issue
Block a user