Sign inSign up

certyiknofetch/rpinotes

By certyiknofetch

Updated 3 months ago

Image
0

4.0K

certyiknofetch/rpinotes repository overview

Version Size Pulls Stars


Version Size Pulls Stars


PiNotes

A self-hosted, privacy-focused note-taking web application built with Flask. Designed for personal use on Raspberry Pi, home servers, or any Docker-capable host.

Features

  • Rich Text Editor — CKEditor 5 with code highlighting (Prism.js), tables, lists, media embeds
  • Password-Protected Notes — Lock individual notes with scrypt-hashed passwords
  • File Attachments — Upload images, documents, and archives per note
  • Tags & Colors — Organize notes with tags and color-coded cards
  • Pin Notes — Pin important notes to the top
  • Dark / Light Theme — Toggle with one click, persisted in cookies
  • Grid / List View — Switch between card grid and compact list layouts
  • Search — Full-text search across titles, content, and tags
  • Media Embeds — Embed YouTube, Vimeo, Dailymotion, Spotify, and Google Maps
  • Fully Offline — All CSS, JS, fonts, and vendor libraries served locally (no CDN calls)
  • Responsive — Works on desktop, tablet, and mobile
  • Pagination & API — Paginated notes list with a JSON API endpoint

Architecture

ComponentTechnology
BackendPython 3.13, Flask, Gunicorn
DatabaseSQLite (WAL mode)
FrontendBootstrap 5, CKEditor 5, Font Awesome, Prism.js
ContainerAlpine Linux, non-root user
SecurityCSRF protection, CSP, XSS sanitization, rate limiting, scrypt password hashing

Quick Start

docker run -d \
  --name rpinotes \
  -p 9000:9000 \
  -v rpinotes_data:/data \
  certyiknofetch/rpinotes:latest

Open http://localhost:9000 in your browser.

Docker Compose — Minimal
services:
  rpinotes:
    image: certyiknofetch/rpinotes:5.0
    container_name: rpinotes
    ports:
      - "9000:9000"
    restart: always
    volumes:
      - rpinotes_data:/data

volumes:
  rpinotes_data:
Docker Compose — Production (with reverse proxy)
services:
  rpinotes:
    image: certyiknofetch/rpinotes:latest
    container_name: rpinotes
    restart: unless-stopped
    ports:
      - "9000:9000"
    command: >-
      sh -c "gunicorn -b 0.0.0.0:9000 app:app
      -w ${GUNICORN_WORKERS:-2}
      --threads ${GUNICORN_THREADS:-4}
      --timeout ${GUNICORN_TIMEOUT:-60}
      --worker-class gthread
      --keep-alive ${GUNICORN_KEEPALIVE:-5}
      --log-level ${GUNICORN_LOGLEVEL:-info}
      --access-logfile ${GUNICORN_ACCESSLOG:--}
      --error-logfile ${GUNICORN_ERRORLOG:--}"
    environment:
      - SECRET_KEY=${SECRET_KEY:-change-me-in-production}
      - ALLOWED_ORIGIN=${ALLOWED_ORIGIN:-https://notes.example.com}
      - PROXY_TRUST=${PROXY_TRUST:-1}
      - UPLOAD_DIR=${UPLOAD_DIR:-/data/uploads}
      - DB_PATH=${DB_PATH:-/data/db/posts.db}
    volumes:
      - ./data/uploads:/data/uploads
      - ./data/db:/data/db
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0) if urllib.request.urlopen('http://localhost:9000/health').getcode()==200 else sys.exit(1)"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 10s

Environment Variables

Application
VariableDefaultDescription
SECRET_KEYRandom (regenerated on startup)Flask session secret. Set a fixed value in production. If missing and you use multiple workers, CSRF/session validation can fail across workers.
DATABASE_PATH/data/posts.dbPath to the SQLite database file.
DB_PATH/data/posts.dbAlias for DATABASE_PATH (either works).
UPLOAD_FOLDER/data/uploadsDirectory for file attachments.
UPLOAD_DIR/data/uploadsAlias for UPLOAD_FOLDER (either works).
PORT9000Port for the built-in dev server (ignored when using Gunicorn).
ALLOWED_ORIGIN(empty)Set to your public app URL (for example https://notes.example.com) when behind a reverse proxy.
PROXY_TRUST0Set to 1 when behind a reverse proxy (Nginx Proxy Manager, Traefik, etc.) so Flask trusts forwarded headers via ProxyFix.
SECURE_COOKIES0Set to 1 only if cookie Secure behavior is validated end-to-end in your proxy/CDN setup.
FLASK_DEBUG0Set to 1 to enable debug mode (dev server only, never in production).
Gunicorn (override via environment or .env file)
VariableDefaultDescription
GUNICORN_WORKERS2Number of worker processes. Rule of thumb: (2 × CPU cores) + 1.
GUNICORN_THREADS4Threads per worker (gthread worker class).
GUNICORN_TIMEOUT60Worker timeout in seconds.
GUNICORN_KEEPALIVE5Keep-alive timeout for connections.
GUNICORN_LOGLEVELinfoLog level: debug, info, warning, error, critical.
GUNICORN_ACCESSLOG- (stdout)Access log file path. - for stdout.
GUNICORN_ERRORLOG- (stderr)Error log file path. - for stderr.

Data & Persistence

All persistent data lives under /data inside the container:

/data/
├── posts.db          # SQLite database (notes, tags, passwords)
└── uploads/
    └── <uuid>/       # Per-note attachment folders
        ├── image.png
        └── doc.pdf

Mount /data as a Docker volume or bind mount to persist across container restarts.

If using DB_PATH=/data/db/posts.db, the directory structure becomes:

/data/
├── db/
│   └── posts.db
└── uploads/
    └── <uuid>/

Security

FeatureDetail
CSRFFlask-WTF CSRF tokens on all forms
XSSServer-side HTML sanitization (bleach), CKEditor whitelist, escapeHtml() in JS
Content-Security-PolicyStrict CSP — default-src 'self', whitelisted media embed domains
Password Hashingscrypt via Werkzeug
SessionHttpOnly, SameSite=Lax, 30-minute expiry, optional Secure flag
Rate LimitingUnlock attempts: 5/min, Write operations: 20/min (per IP)
File UploadsExtension whitelist, MIME-type validation, magic-byte checking, 100MB limit
DoS ProtectionMax 500 notes, 1GB total storage cap
HeadersX-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, HSTS
Note IDsUUIDs (not sequential integers) — prevents enumeration
Non-root ContainerRuns as appuser inside Docker

API Endpoints

MethodPathDescription
GET/Main page (notes list)
POST/createCreate a new note
GET/POST/edit/<uid>View/edit a note
POST/delete/<uid>Delete a note
GET/POST/unlock/<uid>Unlock a password-protected note
POST/lock/<uid>Set a password on a note
POST/relock/<uid>Re-lock an unlocked note (clears session)
POST/remove_lock/<uid>Remove password from a note
POST/toggle_pin/<uid>Pin/unpin a note
POST/update_color/<uid>Change note color
POST/uploadUpload file attachment
GET/uploads/<uid>/<filename>Serve an attachment
GET/toggle-themeSwitch dark/light theme
GET/api/postsJSON API — paginated notes list
GET/healthHealth check (returns {"status": "ok"})

Supported Platforms

Multi-architecture Docker images are published for:

  • linux/amd64 (x86_64 — desktops, servers, VMs)
  • linux/arm64 (aarch64 — Raspberry Pi 4/5, Apple Silicon, ARM servers)

Image Tags

TagDescription
certyiknofetch/rpinotes:latestLatest stable release
certyiknofetch/rpinotes:5.0Version 5.0

Development (without Docker)

cd app
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

# Run with Flask dev server
export DATABASE_PATH=./posts.db
export UPLOAD_FOLDER=./uploads
python3 app.py

The app will be available at http://localhost:9000.

License

Private / self-hosted use.

Tag summary

Content type

Image

Digest

sha256:02e2bd4bf

Size

32.6 MB

Last updated

3 months ago

docker pull certyiknofetch/rpinotes