feat: add InvenTree Cloudron package (Business-Apps) [#658]

Official-image wrapper of inventree/inventree 1.5.2 (digest-pinned).
allauth OIDC provider JSON built from the platform env; invoke update
on boot; RQ worker behind the web-port gate; admin seeded with a
generated password. grind-stack: lowercase image names. 19th package.
Verified: health 200, SSO route 302s to issuer, migrations clean.

Detail: https://projects.knownelement.com/issues/658#note-5086
This commit is contained in:
2026-09-06 19:12:02 -05:00
parent b9bc032d2b
commit 69d0d3e896
11 changed files with 285 additions and 9 deletions
@@ -0,0 +1 @@
repo
@@ -0,0 +1,12 @@
# Changelog — InvenTree Cloudron Package
## 1.0.0 — 2026-09-06
- Initial package: InvenTree 1.5.2 (official-image wrapper, digest
pinned sha256:2df695b3…)
- postgresql + redis + localstorage addons; gunicorn on :8000
- INVENTREE_* env mapped from the platform; data dir pinned to
/app/data (media/static/config + persisted secret key)
- OIDC provider JSON built from CLOUDRON_OIDC_* (allauth
openid_connect, PKCE); local admin seeded with generated password
- `invoke update` on boot; RQ worker backgrounded behind web-port gate
@@ -0,0 +1,23 @@
{
"manifestVersion": 2,
"type": "app",
"id": "io.cloudron.inventree",
"title": "InvenTree",
"description": "Open-source inventory management system: parts and stock tracking, bills of materials, suppliers, purchase and build orders, with a plugin ecosystem and REST API. PostgreSQL storage, Redis-backed background workers. SSO via the Cloudron OIDC provider; a local admin account is seeded on first boot (generated password in /app/data/.admin_password).",
"author": "InvenTree contributors",
"website": "https://inventree.org/",
"contactEmail": "cloudron@tsys.dev",
"tagline": "Inventory management: parts, stock, BOMs and orders",
"version": "1.5.2",
"healthCheckPath": "/api/system/health/",
"httpPort": 8000,
"memoryLimit": 2048,
"addons": {
"localstorage": {},
"postgresql": {},
"redis": {}
},
"mediaLinks": [],
"changelog": "Initial Cloudron package for InvenTree 1.5.2 (official-image wrapper of inventree/inventree, digest pinned). start.sh maps the postgresql/redis addons onto INVENTREE_* env, persists secret key + generated admin password under /app/data, builds the django-allauth openid_connect provider JSON from the Cloudron OIDC env, drives `invoke update` (migrations + static) before serving, and backgrounds the RQ worker gated on the web port. Health endpoint /api/system/health/ per upstream convention.",
"icon": "file://logo.png"
}
@@ -0,0 +1,39 @@
# InvenTree Cloudron Package
#
# InvenTree is an open-source inventory management system (Django):
# parts/stock tracking, BOMs, suppliers, purchase orders, build orders,
# plugin ecosystem. PostgreSQL storage + Redis for background-task
# queues (RQ) and optional caching.
#
# Upstream: https://github.com/inventree/InvenTree (MIT), v1.5.2.
# - Official image inventree/inventree (single image; upstream compose
# runs inventree-server [gunicorn :8000] and inventree-worker
# [invoke worker] as separate services - Cloudron is one container,
# so start.sh backgrounds the worker gated on the web port, the
# same pattern as NetBox).
# - Everything is env-configurable via INVENTREE_* (config.md): DB,
# cache, site URL, secret key, admin seed, SOCIAL_PROVIDERS JSON.
# - The image ENTRYPOINT (init.sh) prepares dirs + config template
# and execs the CMD; migrations/static collection run via
# `invoke update`, which start.sh drives explicitly.
#
# Authentication: NATIVE OIDC (preferred) via django-allauth
# (settings.py:1002 reads INVENTREE_SOCIAL_PROVIDERS;
# SOCIALACCOUNT_OPENID_CONNECT_URL_PREFIX='' so SSO routes live at
# /accounts/oidc/...). start.sh builds the provider JSON from the
# Cloudron platform OIDC env (CLOUDRON_OIDC_*). LDAP also exists in
# core (INVENTREE_LDAP_ENABLED) as an alternative. Local login remains;
# the first admin is seeded (generated password, see README).
#
# Pattern: official-image wrapper. Image pinned by tag AND digest
# (amd64 manifest digest of 1.5.2, from docker manifest inspect).
FROM inventree/inventree:1.5.2@sha256:2df695b338b9fbcb4d86a6a3c27c5f53e690d1b58af8a216e69fec49fc178a14
# no packages needed: the image ships bash, python, invoke; start.sh
# only orchestrates env + processes
COPY start.sh /app/start.sh
# gunicorn web server; API/SSO all on this port (worker has no listener)
EXPOSE 8000
ENTRYPOINT ["/bin/bash", "/app/start.sh"]
@@ -0,0 +1,51 @@
# InvenTree — Cloudron Package
[InvenTree](https://github.com/inventree/InvenTree) v1.5.2 — open-source
inventory management: parts and stock, BOMs, suppliers, purchase/build
orders, plugins, REST API. Django + PostgreSQL + Redis(RQ). Packaged as
an **official-image wrapper** around `inventree/inventree:1.5.2`
(digest-pinned). Upstream runs server and worker as separate compose
services; Cloudron is one container, so start.sh backgrounds
`invoke worker` gated on the web port (same pattern as NetBox).
## Authentication (auth gate: ✅ OIDC preferred)
django-allauth is built into core; `INVENTREE_SOCIAL_PROVIDERS`
(settings.py:1002) carries provider config. start.sh builds the
`openid_connect` provider from the Cloudron platform env:
```json
{"openid_connect": {"OAUTH_PKCE_ENABLED": true, "APPS": [{
"provider_id": "cloudron", "name": "Cloudron SSO",
"server_url": "<CLOUDRON_OIDC_ISSUER>", "client_id": "...", "secret": "..."
}]}}
```
- SSO login route: `https://<app-domain>/accounts/oidc/cloudron/login/`
- LDAP is also available in core (`INVENTREE_LDAP_*`) as an alternative.
- Local login remains for the admin.
**First-boot admin**: a local `admin` account is seeded with a
generated password stored at `/app/data/.admin_password` (read it via
the Cloudron file manager). Change it after first login, or set
`INVENTREE_ADMIN_EMAIL`/`INVENTREE_ADMIN_PASSWORD` app env vars to
control the seed yourself.
## Runtime layout
| Concern | How |
|---------|-----|
| Database | Cloudron `postgresql` addon |
| Cache/queues | Cloudron `redis` addon (cache DB 1; RQ default DB 0) |
| Schema + static | `invoke update` on every boot (migrations + collectstatic) |
| Worker | `invoke worker` backgrounded, gated on web port (post-migration) |
| Persistence | `/app/data` (localstorage): media/, static/, config.yaml, secret key, admin password |
| Health | `/api/system/health/` (upstream's own unauthenticated endpoint) |
## Notes
- Version upgrades: bump tag+digest, rebuild — `invoke update` migrates
on first boot.
- The image ENTRYPOINT's venv logic is bypassed (deps are baked in the
image); start.sh controls ordering instead.
- Package pattern + verification: repo `JOURNAL.md`, InvenTree section.
Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
set -euo pipefail
# InvenTree runtime setup for Cloudron:
# 1. map the Cloudron postgresql + redis addons onto INVENTREE_* env
# and pin the data dir into localstorage (/app/data)
# 2. wait for both addons
# 3. drive `invoke update` (migrations + static collection) before
# serving; the image ENTRYPOINT is bypassed so we control ordering
# 4. background the RQ worker (gated on the web port = migrations
# done), then exec gunicorn on :8000
#
# Authentication: OIDC via the Cloudron platform provider, mapped into
# INVENTREE_SOCIAL_PROVIDERS (django-allauth openid_connect). The first
# admin is seeded via INVENTREE_ADMIN_* with a generated password,
# persisted at /app/data/.admin_password (see README).
DATA_DIR="/app/data"
ADMIN_PASS_FILE="${DATA_DIR}/.admin_password"
mkdir -p "${DATA_DIR}"
# --- 1. Cloudron -> INVENTREE_* environment ---------------------------------------
export INVENTREE_DATA_DIR="${DATA_DIR}"
export INVENTREE_CONFIG_FILE="${DATA_DIR}/config.yaml"
export INVENTREE_DB_ENGINE=postgresql
export INVENTREE_DB_HOST="${CLOUDRON_POSTGRESQL_HOST:-127.0.0.1}"
export INVENTREE_DB_PORT="${CLOUDRON_POSTGRESQL_PORT:-5432}"
export INVENTREE_DB_NAME="${CLOUDRON_POSTGRESQL_DATABASE:-inventree}"
export INVENTREE_DB_USER="${CLOUDRON_POSTGRESQL_USERNAME:-inventree}"
export INVENTREE_DB_PASSWORD="${CLOUDRON_POSTGRESQL_PASSWORD:-}"
export INVENTREE_CACHE_ENABLED=true
export INVENTREE_CACHE_HOST="${CLOUDRON_REDIS_HOST:-127.0.0.1}"
export INVENTREE_CACHE_PORT="${CLOUDRON_REDIS_PORT:-6379}"
export INVENTREE_CACHE_PASSWORD="${CLOUDRON_REDIS_PASSWORD:-}"
export INVENTREE_CACHE_DB=1
export INVENTREE_SITE_URL="${CLOUDRON_APP_ORIGIN:-http://localhost}"
export INVENTREE_SECRET_KEY="$(cat "${DATA_DIR}/.secret_key" 2>/dev/null || true)"
if [[ -z "${INVENTREE_SECRET_KEY}" ]]; then
( umask 077; head -c 48 /dev/urandom | od -An -tx1 | tr -d ' \n' > "${DATA_DIR}/.secret_key" )
export INVENTREE_SECRET_KEY="$(cat "${DATA_DIR}/.secret_key")"
echo "Generated new InvenTree secret key"
fi
# first admin: generated password, stored under /app/data
if [[ ! -s "${ADMIN_PASS_FILE}" ]]; then
( umask 077; head -c 12 /dev/urandom | od -An -tx1 | tr -d ' \n' > "${ADMIN_PASS_FILE}" )
echo "Generated admin password (stored at ${ADMIN_PASS_FILE} - see README)"
fi
export INVENTREE_ADMIN_USER="${INVENTREE_ADMIN_USER:-admin}"
export INVENTREE_ADMIN_PASSWORD="${INVENTREE_ADMIN_PASSWORD:-$(cat "${ADMIN_PASS_FILE}")}"
export INVENTREE_ADMIN_EMAIL="${INVENTREE_ADMIN_EMAIL:-admin@${CLOUDRON_APP_DOMAIN:-localhost}}"
# platform OIDC -> django-allauth openid_connect (server_url drives
# .well-known discovery; PKCE on)
if [[ -n "${CLOUDRON_OIDC_CLIENT_ID:-}" ]]; then
export INVENTREE_SOCIAL_PROVIDERS="$(jq -nc \
--arg cid "${CLOUDRON_OIDC_CLIENT_ID}" \
--arg csec "${CLOUDRON_OIDC_CLIENT_SECRET:-}" \
--arg issuer "${CLOUDRON_OIDC_ISSUER:-}" \
'{openid_connect: {OAUTH_PKCE_ENABLED: true, APPS: [{provider_id: "cloudron", name: "Cloudron SSO", server_url: $issuer, client_id: $cid, secret: $csec}]}}')"
fi
# --- 2. wait for the addons ---------------------------------------------------------
wait_tcp() {
local host="$1" port="$2" name="$3"
echo "Waiting for ${name} at ${host}:${port} ..."
until (exec 3<>"/dev/tcp/${host}/${port}") 2>/dev/null; do
echo "${name} is unavailable - sleeping"
sleep 2
done
echo "${name} is up"
}
wait_tcp "${INVENTREE_DB_HOST}" "${INVENTREE_DB_PORT}" "PostgreSQL"
wait_tcp "${INVENTREE_CACHE_HOST}" "${INVENTREE_CACHE_PORT}" "Redis"
# --- 3. schema + static + plugin setup ----------------------------------------------
# image layout: INVENTREE_HOME=/home/inventree (tasks.py + gunicorn.conf.py
# + init.sh live here), INVENTREE_BACKEND_DIR=/home/inventree/src/backend
cd "${INVENTREE_HOME:-/home/inventree}"
echo "Running invoke update (migrations + static files) ..."
invoke update
# --- 4. worker + web server ----------------------------------------------------------
(
until (exec 3<>/dev/tcp/127.0.0.1/8000) 2>/dev/null; do sleep 2; done
echo "web port is up - starting InvenTree worker"
exec invoke worker
) &
echo "Starting InvenTree web server on :8000 ..."
exec gunicorn -c ./gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:8000 \
--chdir "${INVENTREE_BACKEND_DIR:-/home/inventree/src/backend}/InvenTree"