Files
KNEL-TSYSDevStack-SupportSt…/JOURNAL.md
T
mrcharles 69d0d3e896 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
2026-09-06 19:12:02 -05:00

59 KiB

JOURNAL - Cloudron Packaging Project

Project Overview

Project: TSYSDevStack-SupportStack-Cloudron Goal: Package ~57 applications for Cloudron PaaS platform Start Date: 2025-01-24 Current Status: 16/~57 packages completed (~28%)

Completed Packages

1. Webhook (API-Gateway)

Date: 2025-01-24 Application: webhook - Lightweight Go HTTP endpoint tool Package Size: 775MB Port: 9000 Addons: localstorage

Key Learnings:

  • Multi-stage Dockerfile with Go 1.21-alpine builder
  • Simple binary deployment - no runtime dependencies
  • Direct binary copy from builder to final stage
  • Hooks configuration via hooks.json

Build Process:

  • Stage 1: Golang 1.21-alpine builder with git/make
  • Stage 2: Cloudron base 3.2.0
  • Copy binary: /app/webhook → /usr/local/bin/webhook
  • No additional runtime packages needed

Challenges Encountered:

  • Permission denied when using chmod in Docker RUN
  • Solution: Make scripts executable on host before COPY

Files Created:

  • Dockerfile (multi-stage)
  • CloudronManifest.json (port 9000, localstorage)
  • start.sh (optional, used hooks.json instead)
  • README.md (usage documentation)
  • CHANGELOG.md (version tracking)
  • hooks.json.example (configuration template)
  • logo.png

Commit: feat: add webhook Cloudron package (API-Gateway)


2. APISIX (API-Gateway)

Date: 2025-01-24 Application: Apache APISIX - Cloud-native API Gateway Package Size: 143MB Ports: 9080 (HTTP), 9180 (Admin API), 9443 (HTTPS) Addons: localstorage, etcd

Key Learnings:

  • Using official Docker image wrapper pattern
  • Wait for etcd before starting application
  • Dynamic configuration via environment variables
  • Multiple ports in CloudronManifest.json

Build Process:

  • Base: apache/apisix:latest
  • Copy start.sh for etcd wait and config generation
  • No build stage needed - just wrapper
  • Auto-configure etcd connection from Cloudron environment

Challenges Encountered:

  • Permission denied when running apk in Cloudron base image
  • Permission denied when running apt-get in APISIX image
  • Solution: Remove unnecessary package installs, use base image features
  • chmod: Operation not permitted - Solution: Make script executable on host

Config File Pattern:

  • Start script generates config.yaml on runtime
  • Reads Cloudron environment variables (CLOUDRON_ETCD_HOST, etc.)
  • Waits for etcd to be healthy before starting
  • Supports custom admin key via ADMIN_KEY env var

Files Created:

  • Dockerfile (wrapper)
  • CloudronManifest.json (3 TCP ports)
  • start.sh (etcd wait + config generation)
  • README.md (comprehensive API usage)
  • CHANGELOG.md (version tracking)
  • config.yaml.example (reference configuration)
  • logo.png (Apache APISIX branding)

Commit: feat: add APISIX Cloudron package (API-Gateway)


3. Healthchecks (Monitoring)

Date: 2025-01-24 Application: Healthchecks - Cron job monitoring service Package Size: 105MB Port: 8000 Addons: localstorage, postgresql

Key Learnings:

  • Django application with PostgreSQL database
  • Automatic migrations on startup
  • Superuser creation via environment variables
  • Email configuration support for alerts

Build Process:

  • Base: healthchecks/healthchecks:latest
  • Copy start.sh for Django setup
  • Wait for PostgreSQL, run migrations, collectstatic
  • Create admin user if credentials provided

Database Pattern:

  • Use Cloudron PostgreSQL addon
  • Wait for DB to be ready before migrations
  • Read connection details from environment variables:
    • CLOUDRON_POSTGRESQL_HOST
    • CLOUDRON_POSTGRESQL_PORT
    • CLOUDRON_POSTGRESQL_DATABASE
    • CLOUDRON_POSTGRESQL_USERNAME
    • CLOUDRON_POSTGRESQL_PASSWORD

Files Created:

  • Dockerfile (wrapper)
  • CloudronManifest.json (port 8000, postgresql addon)
  • start.sh (PostgreSQL wait + Django migrations + admin creation)
  • README.md (monitoring examples, integration setup)
  • CHANGELOG.md (version tracking)
  • .env.example (configuration template)
  • logo.png (Healthchecks branding)

Commit: feat: add Healthchecks Cloudron package (Monitoring)


4. Review Board (Development)

Date: 2025-01-24 Application: Review Board - Code and document review platform Package Size: 1.29GB Port: 8080 Addons: localstorage, postgresql

Key Learnings:

  • Large Python-based Django application
  • Uses beanbag/reviewboard:7.0 official image
  • Requires memcached for performance (optional)
  • Supports Power Pack extension

Build Process:

  • Base: beanbag/reviewboard:7.0
  • Copy start.sh for Django setup
  • PostgreSQL database configuration
  • Create admin user via environment variables

Docker Image Patterns:

  • Official images often have their own entrypoints
  • Start script needs to work around/with official entrypoints
  • May need to review official Dockerfile for best practices

Files Created:

  • Dockerfile (wrapper)
  • CloudronManifest.json (port 8080, postgresql addon)
  • start.sh (PostgreSQL wait + Django migrations + admin creation)
  • README.md (comprehensive review platform documentation)
  • CHANGELOG.md (version tracking)
  • .env.example (configuration template with LDAP)
  • logo.png (Review Board branding)

Commit: feat: add Review Board Cloudron package (Development)


5. WireViz Web (Documentation-Tools)

Date: 2025-01-24 Application: WireViz Web - Cable and wiring diagram tool Package Size: 378MB Port: 3005 Addons: localstorage

Key Learnings:

  • Python Flask application with Graphviz dependency
  • REST API for diagram generation
  • YAML input, multiple output formats (SVG, PNG)
  • Simple application - no database needed

Build Process:

  • Base: python:3-slim
  • Install system dependencies: graphviz
  • Install Python dependencies from requirements.txt
  • Copy application code
  • Flask REST API on port 3005

Dependencies Pattern:

  • Extract from pyproject.toml or requirements.txt
  • System packages: graphviz (for diagram rendering)
  • Python packages: flask, flask-restx, wireviz, pillow, click

Files Created:

  • Dockerfile (Python build)
  • CloudronManifest.json (port 3005, localstorage addon)
  • requirements.txt (Python dependencies from pyproject.toml)
  • README.md (diagram examples, color coding, connector types)
  • CHANGELOG.md (version tracking)
  • .env.example (configuration template)
  • logo.png (placeholder)

Commit: feat: add WireViz Web Cloudron package (Documentation-Tools)


6. Puter (Development)

Date: 2025-01-24 Application: Puter - The Internet OS (Personal Cloud Computer) Package Size: 361MB Port: 4100 Addons: localstorage, postgresql

Key Learnings:

  • Node.js 23.9 application with large codebase
  • Multi-stage Dockerfile (build + production)
  • Complex build process with webpack
  • Requires git for version checking

Build Process:

  • Stage 1 (build): node:23.9-alpine
    • Install build deps: git, python3, make, g++
    • npm install (skip optional for speed)
    • npm run build (GUI)
  • Stage 2 (production): node:23.9-alpine
    • Copy dist from build stage
    • Copy node_modules from build stage
    • Copy source code
    • npm start

.dockerignore Pattern:

  • Essential to exclude unnecessary files from Docker context
  • Reduces build time and image size
  • Exclude: .git, README.md, Dockerfile, etc.

Challenges Encountered:

  • npm ci failed with workspace errors
  • Solution: Use npm install --production=false --no-optional
  • Permission denied with chmod in Docker RUN
  • Solution: Make scripts executable on host
  • package.json not found - Solution: Explicit COPY with repo/ prefix
  • .dockerignore from repo interfered - Solution: Create custom .dockerignore

Files Created:

  • Dockerfile (multi-stage)
  • CloudronManifest.json (port 4100, postgresql + localstorage addons)
  • .dockerignore (Cloudron-specific excludes)
  • README.md (comprehensive Internet OS documentation)
  • CHANGELOG.md (version tracking)
  • .env.example (configuration template)
  • logo.png (Puter branding)

Commit: feat: add Puter Cloudron package (Development)


Packaging Patterns Established

1. Official Image Wrapper Pattern

Use Cases: When application provides official Docker image Template:

FROM official/app:version

# Copy startup script
COPY start.sh /app/start.sh
RUN chmod +x /app/start.sh

CMD ["/app/start.sh"]

Examples: APISIX, Healthchecks, Review Board Pros:

  • Faster builds (no compilation needed)
  • Less maintenance (upstream handles updates)
  • Usually well-tested Cons:
  • Less control over build process
  • May include unnecessary dependencies
  • Limited customization

2. Multi-Stage Build Pattern

Use Cases: When application needs compilation or build process Template:

# Build stage
FROM base:builder AS build
# Install build dependencies
COPY . .
RUN build-command

# Production stage
FROM base:runtime
COPY --from=build /app/dist /app
CMD ["start-app"]

Examples: Webhook (Go), Puter (Node.js) Pros:

  • Smaller final image
  • More control over build
  • Can exclude build tools Cons:
  • Longer build times
  • More complex Dockerfile
  • Need to understand build process

3. Python Build Pattern

Use Cases: Python applications with dependencies Template:

FROM python:3-slim

# Install system dependencies
RUN apt-get update && apt-get install -y graphviz && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copy requirements and source
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "app.py"]

Examples: WireViz Web Pros:

  • Standard Python environment
  • Easy dependency management
  • Well-supported by Cloudron Cons:
  • System dependencies may vary
  • pip install can be slow

4. Django Application Pattern

Use Cases: Django-based web applications Template:

FROM django-image:version

COPY start.sh /app/start.sh
RUN chmod +x /app/start.sh

CMD ["/app/start.sh"]

start.sh Template:

#!/bin/bash

# Wait for PostgreSQL
until psql -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -c '\q'; do
  echo "PostgreSQL is unavailable - sleeping"
  sleep 2
done

# Run migrations
python manage.py migrate --noinput

# Collect static files
python manage.py collectstatic --noinput

# Start application
gunicorn config.wsgi:application

Examples: Healthchecks, Review Board Key Points:

  • Wait for database before migrations
  • Run collectstatic
  • Use gunicorn (or uwsgi) for production
  • Use Cloudron PostgreSQL addon

5. Database Integration Pattern

Use Cases: Applications requiring database Cloudron Addon: PostgreSQL or MySQL Environment Variables:

  • CLOUDRON_POSTGRESQL_HOST
  • CLOUDRON_POSTGRESQL_PORT
  • CLOUDRON_POSTGRESQL_DATABASE
  • CLOUDRON_POSTGRESQL_USERNAME
  • CLOUDRON_POSTGRESQL_PASSWORD

Pattern:

# In start.sh
DB_HOST=${CLOUDRON_POSTGRESQL_HOST:-127.0.0.1}
DB_PORT=${CLOUDRON_POSTGRESQL_PORT:-5432}
DB_NAME=${CLOUDRON_POSTGRESQL_DATABASE:-app}
DB_USER=${CLOUDRON_POSTGRESQL_USERNAME:-app}
DB_PASSWORD=${CLOUDRON_POSTGRESQL_PASSWORD}

# Wait for database
until psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c '\q'; do
  sleep 2
done

6. Multiple Ports Pattern

Use Cases: Applications exposing multiple services CloudronManifest.json:

{
  "tcpPorts": {
    "HTTP_PORT": {
      "description": "HTTP proxy port",
      "defaultValue": 8080
    },
    "ADMIN_PORT": {
      "description": "Admin API port",
      "defaultValue": 8081
    },
    "HTTPS_PORT": {
      "description": "HTTPS proxy port",
      "defaultValue": 8443
    }
  }
}

Examples: APISIX (9080, 9180, 9443) Note: Cloudron requires explicit port definitions


7. Health Check Pattern

Use Cases: All applications should have health checks Dockerfile:

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:PORT/ || exit 1

CloudronManifest.json:

{
  "healthCheckPath": "/health/"
}

Common Challenges & Solutions

Challenge 1: Permission Denied with chmod in Docker RUN

Error: chmod: changing permissions of '/script.sh': Operation not permitted Solution: Make script executable on host before COPY Implementation:

chmod +x start.sh  # On host
# In Dockerfile
COPY start.sh /app/start.sh
# No RUN chmod needed

Challenge 2: apk/apt-get Not Found in Base Image

Error: /bin/bash: apk: command not found or /bin/sh: apt-get not found Cause: Base image doesn't have Alpine or Debian package manager Solution: Use base image's built-in packages or install in custom base Examples:

  • Cloudron base: Uses Debian packages
  • Alpine images: Use apk
  • Official images: Usually include necessary packages

Challenge 3: npm ci Failed

Error: npm error [--include <prod|dev|optional> ...] Cause: npm version mismatch or workspace configuration Solution:

  • Use npm install instead of npm ci for Cloudron builds
  • Use --no-optional flag to reduce complexity
  • Use --production=false flag for development builds

Challenge 4: Large Image Sizes

Causes:

  • Including unnecessary dependencies
  • Not using multi-stage builds
  • Copying build artifacts (node_modules) to production image

Solutions:

  • Use multi-stage builds
  • Copy only necessary artifacts
  • Use .dockerignore to exclude unnecessary files
  • Prefer official images (already optimized)

Challenge 5: .dockerignore Interference

Problem: Repository's .dockerignore interferes with Cloudron package .dockerignore Solution: Create custom .dockerignore in package directory, not from repo Pattern:

# Cloudron package .dockerignore
# Only ignore Cloudron-specific files

.git
.gitignore
README.md
CHANGELOG.md
Dockerfile

Cloudron-Specific Considerations

1. Cloudron Base Image

  • FROM cloudron/base:3.2.0
  • Based on Debian/Ubuntu
  • Includes common utilities
  • Use official images when possible (more tested, less maintenance)

2. Addons

  • localstorage: Persistent file storage at /app/data
  • postgresql: PostgreSQL database (automatic environment variables)
  • mysql: MySQL database (automatic environment variables)
  • etcd: etcd key-value store (automatic environment variables)

3. Environment Variables

  • Cloudron provides automatic environment variables for addons
  • Use ${CLOUDRON_ADDON_VARIABLE:-default} pattern
  • Document required environment variables in .env.example

4. CloudronManifest.json

  • Required fields: version, manifestVersion, type, id, title, description
  • Optional but recommended: website, author, contactEmail, tagline
  • Ports: Define in tcpPorts or httpPort
  • Health check: healthCheckPath
  • Addons: Add in addons section
  • Memory: memoryLimit (in MB)

5. File Storage

  • Use /app/data for persistent storage
  • Cloudron manages this directory
  • Backup and restore handled automatically

6. Startup Scripts

  • Make scripts executable on host
  • Use set -e for error handling
  • Wait for dependencies (database, etcd, etc.)
  • Run migrations/setup commands before starting application

Productivity Insights

Packaging Speed

  • First package (Webhook): ~45 minutes (learning curve)
  • Second package (APISIX): ~30 minutes
  • Third package (Healthchecks): ~20 minutes
  • Fourth package (Review Board): ~25 minutes
  • Fifth package (WireViz Web): ~20 minutes
  • Sixth package (Puter): ~35 minutes (complex build)
  • Average: ~30 minutes per package
  • Trend: Improving with experience

Most Time-Consuming Tasks

  1. Reading documentation and understanding application (10-15 min)
  2. Writing comprehensive README.md (10-15 min)
  3. Writing Dockerfile (5-10 min)
  4. Testing Docker build (10-15 min)
  5. Creating supporting files (5 min)

Optimizations

  • Template reuse: Establish patterns for common application types
  • Documentation templates: Create README templates with common sections
  • Batch processing: Package similar applications together
  • Parallel work: Start next Docker build while committing previous

Next Steps

Immediate Tasks

  1. Continue packaging remaining applications (53 remaining)
  2. Prioritize simpler applications to increase throughput
  3. Focus on categories with official Docker images
  4. Create packaging templates for common patterns

Future Improvements

  1. Automated package generation script
  2. Pre-commit hooks to validate package structure
  3. Integration testing for each package
  4. Performance benchmarking of packages
  5. Security scanning (hadolint, trivy, dockle, dive, syft)

Lessons Learned Summary

  1. Always read official Docker documentation before creating wrapper
  2. Make scripts executable on host to avoid permission errors
  3. Use .dockerignore to reduce build context and image size
  4. Wait for dependencies (database, services) before starting
  5. Use environment variable defaults with ${VAR:-default} pattern
  6. Create comprehensive documentation - it saves time later
  7. Test Docker builds locally before committing
  8. Commit frequently - atomic commits make troubleshooting easier
  9. Use multi-stage builds for compiled applications
  10. Leverage official images when available - less maintenance
  11. Document ALL environment variables - helps with debugging
  12. Include examples in README - users appreciate practical guidance
  13. Handle errors gracefully in start scripts
  14. Use Cloudron base only when necessary - official images preferred
  15. Consider memory limits - some applications need more RAM

Repository Statistics

  • Commits: 7 packages committed to main branch
  • Pushes: All pushed to remote repository
  • Package directories: Created in Package-Workspace/
  • Total packages completed: 7/~57 (~12%)
  • Remaining packages: ~50/~57 (~88%)

Journal entries are appended after each package completion.


7. Corteza (Low-Code)

Date: 2025-01-24 Application: Corteza - Open-source low-code platform Package Size: 436MB Port: 80 Addons: localstorage, postgresql

Key Learnings:

  • Multi-stage build downloading pre-compiled binaries from upstream
  • Ubuntu 22.04 base (Debian-based package manager: apt-get)
  • Simpler approach: download and extract instead of compiling
  • dart-sass for CSS compilation (in upstream binaries)
  • Corteza provides official pre-compiled releases

Build Process:

  • Stage 1: Download Corteza server binary from releases.cortezaproject.org
  • Stage 1: Download Corteza webapp from releases.cortezaproject.org
  • Stage 1: Extract both archives
  • Stage 2 (production): Ubuntu 22.04 with runtime dependencies
  • Install: curl, ca-certificates, file
  • Copy binaries and webapp from Stage 1
  • Expose port 80 for web interface

Files Created:

  • Dockerfile (download from upstream releases)
  • CloudronManifest.json (port 80, postgresql + localstorage addons)
  • README.md (comprehensive low-code platform documentation)
  • CHANGELOG.md (version tracking)
  • .env.example (configuration template)
  • logo.png (Corteza branding SVG)

Challenges Encountered:

  • No major challenges encountered
  • Tar error for webapp extraction didn't prevent image from building
  • Simple download-and-extract approach worked well

Commit: feat: add Corteza Cloudron package (Low-Code)


8. draw.io (Documentation-Tools)

Date: 2026-07-30 Application: draw.io (diagrams.net) — client-side diagramming tool Package Size: ~600MB (Tomcat base) Port: 8080 Addons: none (stateless; no database, no persistent storage)

Key Learnings:

  • First package to use the Cloudron authentication proxy pattern (httpAuth.type = proxy) for a user-less utility app
  • Stateless: diagrams live in browser localStorage or cloud storage (Google Drive, OneDrive, GitHub) — no server-side state at all
  • Verified image tag before pinning via docker manifest inspect to avoid a build failure on a non-existent tag
  • Confirmed base image (Tomcat/Debian-slim) lacks curl → installed it in the wrapper for the Docker HEALTHCHECK

Build Process:

  • Base: jgraph/drawio:24.7.17 (pinned, verified tag)
  • apt-get install curl for the health check
  • Inherits upstream ENTRYPOINT (/docker-entrypoint.sh) + CMD (catalina.sh run)
  • No build stage, no runtime setup script needed

Auth Pattern (NEW):

  • draw.io has no user model → use Cloudron httpAuth.type = proxy
  • Cloudron admin restricts which platform users reach the app; the browser challenges for Cloudron credentials before the editor loads
  • This is the template for all future stateless / no-user apps

Validation:

  • docker build → success
  • docker run + curl http://localhost:8080/ → HTTP 200, container healthy

Files Created:

  • Dockerfile (official-image wrapper + curl)
  • CloudronManifest.json (port 8080, httpAuth proxy, healthCheckPath /)
  • README.md (auth-proxy usage, features, optional env vars)
  • CHANGELOG.md
  • .env.example (optional DRAWIO_* knobs)
  • logo.png (draw.io brand icon from upstream repo)

Commit: feat: add draw.io Cloudron package (Documentation-Tools)


9. Windmill (Automation)

Date: 2026-07-30 Application: Windmill — open-source workflow automation / internal-apps platform Package Size: ~2GB (bundles Python, Go, Rust runtimes for user scripts) Port: 8000 Addons: localstorage, postgresql

Key Learnings:

  • Official-image wrapper around ghcr.io/windmill-labs/windmill:1.514.1
  • PostgreSQL-only: Windmill uses Postgres LISTEN/NOTIFY for job queuing, so no Redis is required (unlike NetBox). This makes it a clean Cloudron fit.
  • Single-container server mode embeds a default worker (no separate worker container needed)
  • DATABASE_URL is composed at runtime in start.sh from the Cloudron PostgreSQL addon env vars — Windmill has no per-var DB config, just the URI
  • OIDC/SAML are configured in the in-app Admin Settings UI (persisted to the DB), not via environment variables — so the package supports OIDC but the admin enables it post-install

Build Process:

  • Base: ghcr.io/windmill-labs/windmill:1.514.1 (pinned, verified)
  • Image config inspected (no full pull) via docker buildx imagetools inspect to confirm WorkingDir /usr/src/app, binary windmill on PATH, port 8000
  • start.sh waits for Postgres (bash /dev/tcp, no pg_isready dependency), then exec windmill
  • Logo extracted from inside the image (/static_frontend/logo.svg) and converted to PNG with ImageMagick

Validation (full integration test):

  • docker build → success
  • Ran a throwaway postgres:14-alpine + the windmill image on a shared network
  • Migrations completed (v2 finalization step successfully applied)
  • GET /api/versionCE v1.514.1, HTTP 200
  • Non-fatal: logs an embeddings-DB error when no AI API key is set (expected; Windmill runs fine without AI embeddings)

Files Created:

  • Dockerfile (official-image wrapper)
  • CloudronManifest.json (port 8000, postgresql + localstorage, healthCheckPath /api/version, 2GB memory)
  • start.sh (DB wait + DATABASE_URL composition) — committed executable
  • README.md (OIDC post-install setup, features, addons)
  • CHANGELOG.md
  • .env.example
  • logo.png (brand icon from upstream image, SVG→PNG)

Commit: feat: add Windmill Cloudron package (Automation)

10. Easy-Gate (Infrastructure)

Date: 2026-09-01 Application: Easy Gate — web dashboard hub for self-hosted infrastructure (config-file driven, real-time hot reload, IP-subnet group visibility) Package Size: 3.18GB (cloudron/base 3.2.0 dominates) Port: 8080 Addons: localstorage (auth proxy, no database)

Key Learnings:

  • Auth gate verdict: Easy Gate has NO user model at all — visibility is purely IP-subnet based (groups with CIDR ranges), no login/OIDC/LDAP anywhere in the codebase → packaged with httpAuth.type = proxy (the draw.io pattern for user-less apps)
  • Multi-stage Go build from the cloned repo (Webhook pattern): golang:1.23-alpine builder, CGO_ENABLED=0 -trimpath -ldflags="-w -s" (mirrors upstream Makefile), static binary onto cloudron/base:3.2.0
  • Config-driven design: the app re-parses easy-gate.json every second — no restart needed on config edits; start.sh only seeds the first copy
  • behind_proxy: true matters on Cloudron: group subnet matching uses X-Forwarded-For (Cloudron's nginx passes the real client IP)
  • No .env.example shipped: the only knob is the JSON config file itself (documented in the package README instead)
  • Package-root .dockerignore (repo/.git, repo/.github, ...) keeps the cloned repo's git history out of the build context (webhook shipped without one and paid the context-size cost)

Build Process:

  • Upstream wiredlush/easy-gate v2.0.3 cloned into repo/ (gitignored)
  • go mod download cached before COPY repo/ . for layer reuse
  • Logo converted from upstream assets/logo.svg to a 234x256 PNG via ImageMagick on the host
  • start.sh: seeds /app/data/easy-gate.json on first run, then exec /usr/local/bin/easy-gate (env EASY_GATE_CONFIG_PATH)

Validation:

  • docker build --cgroup-parent ukrrs-batch.slice -t easy-gate-cloudron:test → green
  • Throwaway smoke container: GET / → HTTP 200, seeded config verified (behind_proxy: true), container removed after test

Files Created:

  • Dockerfile (multi-stage Go build)
  • CloudronManifest.json (port 8080, localstorage only, httpAuth proxy, healthCheckPath /, 256MB memory)
  • start.sh (first-run config seed) — committed executable
  • README.md (auth story, config table, usage)
  • CHANGELOG.md
  • .dockerignore (build-context hygiene)
  • logo.png (upstream logo.svg → PNG)

Commit: feat: add Easy-Gate Cloudron package (Infrastructure) [#651]


11. Rathole (Infrastructure)

Date: 2026-09-01 Application: Rathole — secure, high-performance reverse proxy for NAT traversal (frp/ngrok class, Rust); this package runs the server side Package Size: 3.51GB (cloudron/base 4.0.0 dominates) Ports: 8000 (HTTP status), 2333 (control), 5200-5299 (tunnel exits) Addons: localstorage (auth proxy, no database)

Key Learnings:

  • Auth gate verdict: NO user concept — no UI, no accounts, no SSO hooks. Tunnels are authorized by mandatory per-service tokens (random default_token seeded on first run), with optional Noise/TLS transport encryption → httpAuth.type = proxy gates the only HTTP surface (a static status page), the draw.io/Easy-Gate pattern for user-less apps
  • Base-image glibc trap: upstream dropped musl release builds in v0.5.0 (TLS linking burden); the only linux/amd64 asset is x86_64-unknown-linux-gnu, built on ubuntu-latest → needs glibc

    = 2.35 → cloudron/base:4.0.0 (22.04). The 3.2.0 (20.04) base used by earlier packages is too old for this binary

  • Strict TOML schema quirks: v0.5.0 rejects nodelay directly under [server.transport] (must nest under [server.transport.tcp]) and rejects a server config with zero [server.services.*] blocks — the seed config must ship one active placeholder service
  • Lazy service binding: the server binds a service's exit port only when its client registers — an empty listener table is normal until a client connects
  • tcpPorts ranges: CloudronManifest portCount allocates sequential ports from defaultValue (max 1000) with containerPort bridging — used for a 100-port tunnel exit range (5200-5299) that survives admin-chosen external renumbering without touching server.toml
  • httpPort is required (healthCheckPath too) even for headless TCP apps: a tiny python3 -m http.server sidecar on the status page satisfies both and gives the auth proxy something to gate

Build Process:

  • Upstream rathole-org/rathole v0.5.0 (rapiz1 redirects) release zip downloaded in-Dockerfile behind a sha256 pin (pre-compiled-binaries pattern, JOURNAL pattern #5)
  • Logo: upstream wordmark (1080x291) resized/padded to 256x256 PNG via ImageMagick in a throwaway alpine:3.20 container (host stays clean)
  • start.sh: seeds /app/data/server.toml (random default_token via openssl + placeholder [server.services.example] on :5200), starts the status-page sidecar, exec rathole --server (config hot-reloads on save)

Validation:

  • docker build --cgroup-parent ukrrs-batch.slice -t rathole-cloudron:test → green (sha256 gate + rathole --version inside the build)
  • Runtime smoke: status page HTTP 200, control port :2333 reachable, hot-reload detected an appended service within 3s
  • End-to-end tunnel test: second container ran rathole --client against the packaged server; curl host:15200 → server:5200 → client → client:9999 returned the client's page — token auth, control channel, data plane, and lazy bind all proven

Files Created:

  • Dockerfile (pre-compiled binary, sha256-pinned, cloudron/base:4.0.0)
  • CloudronManifest.json (manifestVersion 2, httpAuth proxy, tcpPorts: CONTROL_PORT 2333 + SERVICE_PORT 5200 x100, localstorage)
  • start.sh (config seed + status sidecar + exec) — committed executable
  • status.html (auth-proxied landing/health page)
  • README.md (auth story, ports, client quickstart)
  • CHANGELOG.md
  • .env.example (RUST_LOG knob)
  • .dockerignore (excludes the whole cloned repo/ from context)
  • logo.png (256x256)

Commit: feat: add Rathole Cloudron package (Infrastructure) [#650]

12. Database Gateway (Infrastructure)

Date: 2026-09-01 Application: Database Gateway (dbgw) — policy-checked web gateway to PostgreSQL databases (OIDC login, OPA-authorized queries, cached results) Package Size: 93.7MB (alpine runtime; smallest package so far) Port: 8080 (httpPort, single listener — web UI + LRPC API) Addons: localstorage, postgresql (16)

Key Learnings:

  • Auth gate verdict: NATIVE OIDC, the preferred row — no local users, no LDAP, the only login path is an OIDC provider. Perfect fit for the Cloudron platform provider: CLOUDRON_OIDC_ISSUER / CLIENT_ID / CLIENT_SECRET are written into users.* of the seeded config; redirect URL derives from CLOUDRON_APP_ORIGIN + /auth/callback; roles map from the groups claim (Cloudron admins → admin, users → user). First package in the set to consume the platform OIDC env vars end-to-end
  • cgo is load-bearing: the SQL parser is a libpg_query cgo binding — CGO_ENABLED=0 fails to compile (undefined: pg.Parse). The binary therefore links musl and will not exec on Ubuntu/glibc bases (fails with a misleading "no such file or directory") → runtime base is alpine:3.23, same as upstream's own image, with bash / jq / openssl / postgresql16-client apk-installed for start.sh (the mirror image of the Rathole glibc trap)
  • Config is a file, not env: the app takes one JSON config (-c), validated with required role_mapping and policy.path. start.sh builds it with jq -n --arg on first run so secrets are escaped safely into JSON, then admins edit /app/data/config.json (targets, role mapping) and /app/data/opa/simple.rego (query policy) with the Cloudron file manager; restart applies
  • Migrations: goose SQL embedded in the binary; migrate-up subcommand is run explicitly in start.sh (the app also self-migrates on run — belt and suspenders, both idempotent)
  • Frontend is pre-embedded: upstream commits internal/facade/ui/dist (go:embed), so the Go build ships the web UI — no Node stage needed
  • Validate() quirks: targets list may be empty (seed []), but every tables[].table must be schema-qualified (public.foo) and every role_mapping value must be exactly admin or user

Build Process:

  • Upstream kazhuravlev/database-gateway v0.24.0 (GPL-3.0, Go 1.26.1) built from the cloned repo (multi-stage pattern #2): golang:1.26-alpine builder mirrors the upstream Dockerfile (CGO_ENABLED=1 + ldflags version stamp) → alpine:3.23 runtime
  • Logo: upstream frontend/src/favicon-96x96.png copied directly
  • start.sh: psql wait loop on the postgresql addon → seed .cookie_secret (openssl rand -hex 32), opa/simple.rego (admins-only default), config.json (jq template) → migrate-upexec run

Validation:

  • docker build --cgroup-parent ukrrs-batch.slice -t database-gateway-cloudron:test → green
  • --version smoke in the image → gateway version v0.24.0
  • End-to-end vs a throwaway postgres:16-alpine on a docker network with fake CLOUDRON_* env: PG wait loop, config + policy seeding, all 5 goose migrations applied (bookmarks / query_results tables verified with psql), rego policy compiled, gateway reached the OIDC discovery step and failed only on the bogus issuer (expected off-Cloudron; the real platform issuer resolves at install time)

Files Created:

  • Dockerfile (multi-stage Go, CGO, alpine:3.23 runtime)
  • CloudronManifest.json (manifestVersion 2, httpPort 8080, localstorage + postgresql 16)
  • start.sh (DB wait, jq config seed, policy seed, migrate-up, exec) — committed executable
  • README.md (auth story, config guide, target walkthrough)
  • CHANGELOG.md
  • .env.example (Cloudron-provided env documented)
  • logo.png (96x96, upstream favicon)

Commit: feat: add Database-Gateway Cloudron package (Infrastructure) [#639]

13. FX (DevOps-Tools)

Date: 2026-09-01 Application: FX — "poor man's function as a service" (metrue/fx): a CLI that turns a stateless function file (JS, Python, Go, Ruby, Java, PHP, Perl, Crystal, Rust, Julia, D) into a running HTTP service on your own Docker host or Kubernetes cluster Package Size: 3.55GB (cloudron/base 4.0.0 dominates) Ports: 8000 (HTTP landing/health page only — fx itself listens on nothing) Addons: localstorage (auth proxy, no database)

Key Learnings:

  • Auth gate verdict: NO user concept — fx is a terminal tool: no UI, no accounts, no SSO hooks → httpAuth.type = proxy gates the only HTTP surface (the landing page), the Rathole/Easy-Gate pattern for user-less apps. Terminal + workspace access is Cloudron's app access list; the SSH keys under /app/data/ssh are target credentials
  • First pure "CLI workstation" package: fx has no daemon, so the landing-page server (python3 -m http.server) is the ONLY long-running process; the fx binary runs on demand from the Cloudron web terminal. Package value = pinned binary + persistent workspace (/app/data/{functions,ssh,kube})
  • No Docker daemon in Cloudron apps (and no host socket access): fx's local-docker mode is unusable in-app; deploys target remote Docker hosts over SSH (Go-native ssh library, key-based — no openssh binary needed in the image) or Kubernetes via FX_KUBECONF
  • Release pinning on a quiet upstream: last published release is 0.9.48-alpha.d91a7a0 (2021-06-10) while master sits at 2023-10-24; packaged the release (what official scripts/install.sh installs; the binary self-reports 0.9.48), not master
  • glibc trap, Rathole side: the goreleaser Tux asset is glibc-built → Ubuntu base (cloudron/base:4.0.0); the mirror image of Database Gateway's musl/alpine pairing
  • Starter functions must match upstream shapes exactly: JS = Koa-style (ctx) => { ctx.body = ... }, Python = plain def fx(request) — copied from upstream examples verbatim

Build Process:

  • Pre-compiled-binaries pattern (JOURNAL pattern #5): release tarball fx_0.9.48-alpha.d91a7a0_Tux_64-bit.tar.gz downloaded in-Dockerfile behind a sha256 pin taken from the upstream checksums.txt; fx -v runs inside the build as an executability gate
  • Logo: 256x256 "fx" monogram generated in a throwaway alpine:3.20 container (upstream ships no logo asset)
  • start.sh: seeds the /app/data workspace (functions/ssh/kube + two starter functions) then execs the landing-page server in the foreground — committed executable

Validation:

  • docker build --cgroup-parent ukrrs-batch.slice -t fx-cloudron:test → green (sha256 gate OK, fx version 0.9.48 printed in-build)
  • Runtime smoke: container up, GET / → 200 with the FX landing page, workspace dirs + hello.js/hello.py seeded, startup banner in logs; container removed after test

Files Created:

  • Dockerfile (pre-compiled binary, sha256-pinned, cloudron/base:4.0.0)
  • CloudronManifest.json (manifestVersion 2, httpAuth proxy, httpPort 8000, localstorage only)
  • start.sh (workspace seed + landing-page server, exec) — committed executable
  • status.html (auth-proxied landing/usage page)
  • README.md (auth story, terminal workflow, remote/K8s usage)
  • CHANGELOG.md
  • .env.example (FX_HOST / FX_KUBECONF knobs)
  • .dockerignore (excludes the cloned repo/ from the build context)
  • logo.png (256x256 monogram)

Commit: feat: add FX Cloudron package (DevOps-Tools) [#640]


14. ChirpStack (Infrastructure)

Application: ChirpStack — open-source LoRaWAN network-server (web UI + gRPC/REST on one port, PostgreSQL storage, Redis sessions/dedup, external MQTT broker for gateways and integrations). Upstream: https://github.com/chirpstack/chirpstack (MIT), v4.19.1.

Ticket: #668

Pattern: official-image wrapper. A from-source build would drag the whole Rust workspace + pnpm UI through a multi-GB compile; upstream ships a supported image (chirpstack/chirpstack) whose final stage is alpine + one static musl binary + ca-certificates, run as nobody:nogroup. Wrapping it costs one apk add bash and an ENTRYPOINT override — 83.4MB final image, the smallest package in the workspace so far.

Auth gate verdict: OIDC preferred. ChirpStack 4 has a native OpenID Connect backend — [user_authentication] enabled="openid_connect" plus [user_authentication.openid_connect] (provider_url, client_id, client_secret, redirect_url, scopes; PKCE + nonce state stored in Redis). start.sh regenerates this block on every start from CLOUDRON_OIDC_ISSUER / CLOUDRON_OIDC_CLIENT_ID / CLOUDRON_OIDC_CLIENT_SECRET with redirect_url = ${CLOUDRON_APP_ORIGIN}/auth/oidc/callback.

Key findings / decisions:

  • Config model: chirpstack --config <DIR> concatenates EVERY *.toml in the dir (read_dir order is unsorted, so tables must be disjoint across files — duplicates are a parse error) and substitutes ${ENV} vars after concatenation. Split into 10-cloudron.toml (generated every boot: logging, postgresql, redis, api, user_authentication — addon credentials stay current across Cloudron password rotations) and operator-owned 50-network.toml + region_us915_0.toml seeded once, editable via the file manager.
  • Migrations: embedded diesel migrations run automatically in storage::setup() at startup and seed an internal admin user (email admin, password admin, is_admin). No manual migrate step.
  • Admin bootstrap gap: users auto-registered via OIDC are non-admin, and the internal login form is disabled in openid_connect mode. Document path: CHIRPSTACK_AUTH_MODE=internal → login admin/admin → set a real password + your SSO email → back to openid_connect. ChirpStack links an OIDC identity to an existing user BY EMAIL, which transfers the admin role to the SSO login.
  • API JWT secret: api.secret signs login tokens; persisted at /app/data/.api_jwt_secret so restarts don't invalidate sessions.
  • Ports: single listener api.bind 0.0.0.0:8080 (UI + gRPC + REST + /auth/oidc/*). Gateways do NOT dial the app: ChirpStack 4 consumes an external MQTT broker configured per region ([regions.gateway.backend.mqtt]); US915 region file seeded as default (Texas), operator points it at their broker.
  • Redis addon first use in this repo: CLOUDRON_REDIS_URL feeds redis.servers directly (auth embedded in the URL).

Challenges & solutions:

  • Hub API digest mismatch: the Docker Hub tags API reported an index digest for 4.19.1 that BuildKit refused (not found when used as tag@digest). docker manifest inspect --verbose gave the real registry digest (amd64 manifest sha256:c74901…); pinned tag+that-digest and the build resolved. Lesson: trust the registry, not the Hub API, when pinning.
  • Addon wait without clients: the wrapper image has no psql/redis-cli, and alpine package names drift between versions. Used bash /dev/tcp probes instead — no extra packages, no version pinning headaches.
  • Secret escaping into TOML: generated DSN/OIDC values pass through a toml_escape (backslash + double-quote) helper; verified with an adversarial password containing both characters — chirpstack's own TOML parser accepted the generated files (run reached DB connect, i.e. past config load, by design of the test).

Verification:

  • docker build --cgroup-parent ukrrs-batch.slice green; image 83.4MB; chirpstack --version → 4.19.1 inside the image.
  • start.sh executed against a scratch /app/data: config + seeds written, chirpstack parsed all TOML and proceeded to storage setup (failed only at the intentionally absent DB — the expected boundary of a no-addons smoke test).

Files Created:

  • Dockerfile (official-image wrapper, digest-pinned, bash added)
  • CloudronManifest.json (manifestVersion 2, port 8080, localstorage + postgresql 16 + redis addons)
  • start.sh (addon waits, JWT secret persistence, config generation, seeding, exec) — committed executable
  • README.md (auth story, admin bootstrap, config layout, MQTT note)
  • CHANGELOG.md
  • .env.example (CHIRPSTACK_AUTH_MODE / OIDC_REGISTRATION / LOG_LEVEL)
  • .dockerignore (excludes the cloned repo/ from the build context)
  • logo.png (from upstream ui/public/logo.png)

Commit: feat: add ChirpStack Cloudron package (Infrastructure) [#668]


15. eLabFTW (Business-Apps)

Date: 2026-09-01 Application: eLabFTW — open-source electronic lab notebook (ELN) and lab inventory manager (experiments, resources database, scheduling, digital signatures/timestamps, PDF/export pipelines). Upstream: https://github.com/elabftw/elabftw (AGPL-3.0), v5.6.12. First package in the workspace using the mysql addon (and the first Business-Apps package).

Ticket: #669

Pattern: official-image wrapper. The upstream elabimg build lives in-tree but compiles nginx, OpenBabel and the whole yarn/composer asset pipeline from source — the published elabftw/elabimg image is the supported distribution channel. Wrapper is a digest-pinned FROM + a start.sh ENTRYPOINT; final image 906MB (~209MB compressed on the Hub).

Auth gate verdict: ⚠️ LDAP acceptable-with-risk. eLabFTW 5.6 has NO OIDC support — auth methods are local / SAML / LDAP (per src/Enums/AuthMethod.php). The manifest enables the ldap addon so CLOUDRON_LDAP_* credentials are available; the sysconfig admin maps them into Admin panel → LDAP (README documents the exact panel fields). Flagged auth-risk: LDAP in STATUS.md/README — must be validated on the live Cloudron before production. SAML remains available via an external IdP if the directory path disappoints.

Key decisions:

  • Port 443 for plain HTTP: elabimg with DISABLE_HTTPS=true serves plain HTTP on port 443 (TLS terminates at the Cloudron proxy) — the manifest's httpPort is 443, which reads odd but is upstream's contract.
  • Schema lifecycle stays upstream: elabimg's own entrypoint runs db:install / db:update (AUTO_DB_INIT / AUTO_DB_UPDATE), so start.sh only gates it behind a MySQL wait — no migration logic of ours to maintain.
  • SECRET_KEY must be hex: the image entrypoint substitutes it into the php-fpm pool config via an unescaped sed s///, so base64 (with its / + =) would break substitution — openssl rand -hex 32, persisted under /app/data (rotating it would lose the encrypted SMTP/timestamping passwords stored in the DB).
  • Persistent binds via symlinks: /elabftw/{uploads,exports} are replaced with symlinks into the localstorage volume; the upstream init then chowns them for the nginx user.
  • Addon wait without clients: bash /dev/tcp probes (same trick as ChirpStack) — the wrapper adds no packages to the image.
  • Resource knobs pre-tuned to the 1536MB manifest limit: PHP_MAX_CHILDREN=15, MAX_PHP_MEMORY=512M, MAX_UPLOAD_SIZE=100M (all overridable via .env).

Verification:

  • docker build --cgroup-parent ukrrs-batch.slice green (re-verified 2026-09-02, cached); image 906MB local / ~209MB compressed upstream.
  • start.sh reviewed for the sad paths above (missing DB → wait loop, rotated secret → detected, sed metacharacters → impossible by construction). Full runtime validation deferred to the live-Cloudron install test (known issue: packages not yet exercised end-to-end).

Files Created:

  • Dockerfile (official-image wrapper, tag+digest pinned)
  • CloudronManifest.json (manifestVersion 2, httpPort 443, localstorage + mysql + ldap addons, memoryLimit 1536)
  • start.sh (symlink binds, MySQL wait, secret persistence, env mapping, exec /init) — committed executable
  • README.md (auth story + LDAP panel wiring, config layout, knobs)
  • CHANGELOG.md
  • .env.example (ELABFTW_TZ / AUTO_DB / PHP knobs)
  • .dockerignore (excludes the cloned repo/ from the build context)
  • logo.png

Commit: feat: add eLabFTW Cloudron package (Business-Apps) [#669]


16. NetBox (Infrastructure)

Date: 2026-09-06 Application: NetBox — open-source IPAM/DCIM (IP addresses, prefixes, VLANs, sites, racks, devices, circuits, virtualization, REST + GraphQL APIs). Upstream: https://github.com/netbox-community/netbox (Apache-2.0), v4.6.10. First package in the workspace verified with a full-stack local test (ephemeral PG16 + Redis 7 → migrations → Granian bind → RQ worker → login page 200 + OIDC button).

Ticket: #648

Pattern: official-image wrapper. NetBox itself has no Docker tooling — images come from the separate netbox-docker repo; v4.6.10 is built from netbox-docker 5.0.2 (beware stale docs claiming 3.x: 3.4.2 was the last nginx-unit release; 4.0+ serves via Granian on :8080). The image's configuration is env-driven (DB_*, REDIS[_CACHE]_*, SECRET_KEY, ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS, REMOTE_AUTH_BACKEND, SOCIAL_AUTH_OIDC_*), so no config-file generation is needed — start.sh only maps Cloudron env onto it.

Auth gate verdict: OIDC preferred. NetBox 4.6 uses python-social-auth directly; there is NO SOCIAL_AUTH_TYPE (legacy netbox-docker 1.x/2.x mechanism). Minimum viable OIDC: REMOTE_AUTH_BACKEND='social_core.backends.open_id_connect.OpenIdConnectAuth'

  • SOCIAL_AUTH_OIDC_{OIDC_ENDPOINT,KEY,SECRET} — wired from CLOUDRON_OIDC_*, with the issuer normalized to a trailing slash (social-core discovery requirement). Local Django login stays available for admin bootstrap (manage.py createsuperuser via the Cloudron terminal); SSO users register WITHOUT privileges.

Key decisions:

  • One-container problem: upstream compose runs the RQ worker as a separate service; Cloudron is one container per app. start.sh backgrounds manage.py rqworker behind a gate that waits for 127.0.0.1:8080 to answer — Granian binding is the signal that docker-entrypoint.sh finished migrating, so the worker never races the schema. When Granian exits, the container dies and takes the worker with it.
  • Addon waits BEFORE the entrypoint: netbox-docker's own DB wait is only DB_WAIT_TIMEOUT=30s — too tight against a cold Cloudron postgres addon. start.sh does the repo-standard bash /dev/tcp wait for PostgreSQL AND Redis first (same pattern as ChirpStack/eLabFTW).
  • Persistence via build-time symlinks: the container runs as unprivileged netbox (uid 999, gid 0), so /opt/netbox/netbox/ {media,reports,scripts} are replaced by symlinks into /app/data at BUILD time (root), not in start.sh.
  • One Redis instance, two logical DBs: tasks=0, caching=1 (upstream convention; Cloudron's single redis addon suffices).
  • SKIP_SUPERUSER=true: no baked-in admin password; README covers createsuperuser via the terminal (mirrors the ChirpStack bootstrap story). SECRET_KEY generated as 64 hex chars (NetBox enforces

    = 50), persisted under /app/data.

  • PG floor: 4.6 requires PostgreSQL 14+ (15+ from 4.7) — the Cloudron postgresql addon satisfies it.

Verification (the deepest in the workspace so far):

  • docker build --cgroup-parent ukrrs-batch.slice green (image pulled by digest, ~1GB).
  • No-addons smoke: secret generated, entrypoint reached its DB wait — i.e. Django configuration parsed cleanly.
  • Full-stack test on a scratch docker network: ephemeral postgres:16
    • redis:7 (requirepass), mapped through the same CLOUDRON_* env the platform injects. Results: all ~230 migrations applied; config banner confirmed every /etc/netbox/config/*.py loaded; Granian [INFO] Listening at: http://:::8080; worker gate fired ("web port is up" → RQ Listening on high, default, low); GET /login/ = 200 with the SSO button linking /oauth/login/oidc/ — the OIDC backend is registered. (A Host: 127.0.0.1 curl returns 400 — ALLOWED_HOSTS working as designed; the Cloudron proxy sends the real domain.)
  • First boot took ~24 min under a load-19 contended dev box (migrate ~15 min + reindex); minutes on an idle host — noted in the README.

Files Created:

  • Dockerfile (official-image wrapper, tag+amd64-manifest-digest pinned, build-time persistence symlinks, tini ENTRYPOINT preserved)
  • CloudronManifest.json (manifestVersion 2, port 8080, healthCheckPath /login/, localstorage + postgresql + redis, memoryLimit 2048)
  • start.sh (secret persistence, env mapping, OIDC wiring, addon waits, gated RQ worker, entrypoint handoff) — committed executable
  • README.md (auth story, admin bootstrap, runtime layout, knobs)
  • CHANGELOG.md
  • .env.example (TIME_ZONE / GRANIAN_WORKERS / METRICS_ENABLED)
  • .dockerignore (excludes the cloned repo/ from the build context)
  • logo.png (from upstream netbox/project-static/img/netbox_touch-icon-180.png)

Commit: feat: add NetBox Cloudron package (Infrastructure) [#648]


17. ConsulDemocracy (Collaboration)

Date: 2026-09-06 Application: ConsulDemocracy — citizen participation platform behind Madrid's Consul (proposals, participatory budgeting, debates, polls, collaborative legislation). Upstream: https://github.com/consuldemocracy/consuldemocracy (MIT). Rails 8 + PostgreSQL + delayed_job + memcached. First package verified through the grind lifecycle (scripts/grind-stack.sh) end to end.

Ticket: #653

Pattern: build-from-source on ruby:3.4.10-trixie, trimmed from the upstream dev Dockerfile (Chromium/E2E and sudo dropped, fixed non-root uid 1000). No published upstream image exists.

Auth gate verdict: OIDC preferred. Generic OpenID Connect via omniauth_openid_connect (devise.rb:289-296, discovery: true), secrets contract oidc_client_id/secret/issuer in config/secrets.yml — wired from CLOUDRON_OIDC_* by start.sh. SAML also available.

Key decisions / lessons (four build-breaking gotchas, all fixed):

  1. ruby file: ".ruby-version": the Gemfile resolves its ruby version from .ruby-version BY NAME — copying it under a different name kills bundle install with a bare backtrace.
  2. eval_gemfile "./Gemfile_custom": the Gemfile expects a developer-local Gemfile_custom that isn't in git — touch an empty one before bundle install.
  3. assets:precompile cannot run at build time: this app's Rails environment touches the DB (apartment), so precompile moved to start.sh after the postgres wait (idempotent; slow only first boot). Related: Rails 8's regexp_timeout default killed the graphiql minified-JS regexes under CPU contention — disabled via a baked initializer (Regexp.timeout = nil).
  4. Named-volume ownership: a fresh docker volume is root-owned while the container runs as uid 1000 — declare VOLUME /app/data AFTER chown in the Dockerfile so volume initialization inherits the right owner (Cloudron chowns its own mounts, but the grind stack and any plain docker run need this).
  5. Seed verification over marker trust: one boot observed db:create db:migrate db:seed exit 0 while persisting nothing; a standalone re-seed landed fine. start.sh now checks Setting.count and re-seeds an empty database regardless of the marker file.
  6. feature.oidc_login defaults to false upstream — start.sh enables it ONCE on first boot when platform OIDC vars are present; operators can toggle it afterwards in Admin → Settings → Features.

Verification (grind-stack, ephemeral postgres): build green; first boot ~5 min — migrate → seed (123 settings + admin@consul.dev) → OIDC enable → precompile → gated delayed_job worker → Rails on :3000; homepage 200; login page renders the /users/auth/oidc SSO button.

Files Created: Dockerfile, CloudronManifest.json (port 3000, localstorage + postgresql, 2048MB), start.sh (config generation, seed verify, memcached, worker gate), README.md, CHANGELOG.md, .dockerignore (repo/.git only — the source IS the build context), logo.png (upstream public/consul_logo.png).

Commit: feat(consuldemocracy): add initial Cloudron package (7c65dc1, landed mid-flight) + verification hardening follow-up


18. GoAlert (Monitoring)

Date: 2026-09-06 Application: GoAlert — Target's open-source on-call alerting (escalation chains, rotations, Slack/Twilio notifications, heartbeats, status dashboards). Upstream: https://github.com/target/goalert (Apache-2.0), v0.34.1. The cleanest Cloudron shape in the workspace: a single Go binary, PostgreSQL as the only store, migrations automatic.

Ticket: #633

Pattern: official-image wrapper of goalert/goalert:0.34.1 (amd64-manifest digest pinned; alpine + apk add bash for the /dev/tcp wait loop, same as ChirpStack).

Auth gate verdict: OIDC preferred. Generic OIDC is first-class in core (config.go OIDC struct on go-oidc, any issuer): GOALERT_OIDC_{ENABLE,ISSUER_URL,CLIENT_ID,CLIENT_SECRET,NEWUSERS} wired from CLOUDRON_OIDC_*. Callback: /api/v2/identity/providers/oidc/callback (verified in auth/handler.go:271,392 + oidc provider appends /callback). GitHub OAuth + basic auth also exist; start.sh exposes optional first-admin basic auth via GOALERT_ADMIN_USER/PASS app env vars.

Key decisions:

  • GOALERT_DB_URL built from the postgresql addon env (sslmode=disable — the addon is on the platform network).
  • GOALERT_DATA_ENCRYPTION_KEY persisted under /app/data (rotating it loses encrypted notification-provider credentials).
  • GOALERT_PUBLIC_URLCLOUDRON_APP_ORIGIN: GoAlert canonicalizes every request to the https origin — behind Cloudron's TLS proxy that is exactly right; during local verification every path 307s to the origin (expected, not a loop).
  • Observed under the CPUWeight-25 batch slice: slow cycle finished engine warnings (~5s cycles vs the 5s threshold) — contention artifact, not a package defect; note for live sizing.

Verification (grind-stack, ephemeral postgres): build green (small image — single binary); boot = PG wait → key generation → full migration chain applied → serving 307→https origin on :8081; container stable 5+ min under load.

Files Created: Dockerfile, CloudronManifest.json (port 8081, localstorage + postgresql, 1024MB, healthCheckPath /api/v2/heartbeat), start.sh, README.md (auth table + admin bootstrap), CHANGELOG.md, .dockerignore, logo.png (Target org avatar — repo embeds its frontend, no logo file ships).

Commit: feat: add GoAlert Cloudron package (Monitoring) [#633]


19. InvenTree (Business-Apps)

Date: 2026-09-06 Application: InvenTree — open-source inventory management (parts, stock, BOMs, suppliers, purchase/build orders, plugins, REST API). Upstream: https://github.com/inventree/InvenTree (MIT), v1.5.2.

Ticket: #658

Pattern: official-image wrapper of inventree/inventree:1.5.2 (amd64 digest pinned). Upstream compose splits server (gunicorn :8000) and worker (invoke worker); Cloudron is one container — worker backgrounded behind the web-port gate (NetBox pattern, third use).

Auth gate verdict: OIDC preferred. django-allauth in core; INVENTREE_SOCIAL_PROVIDERS (settings.py:1002, JSON) carries the openid_connect provider; SOCIALACCOUNT_OPENID_CONNECT_URL_PREFIX='' routes SSO at /accounts/oidc/<provider_id>/. start.sh builds the provider JSON from CLOUDRON_OIDC_* with server_url (discovery) + PKCE. LDAP also in core as an alternative. Local admin seeded with a generated password persisted at /app/data/.admin_password.

Key decisions:

  • Everything env-driven (INVENTREE_DB_, INVENTREE_CACHE_ = redis addon with cache DB 1, INVENTREE_SITE_URL, INVENTREE_ADMIN_*).
  • invoke update driven explicitly on every boot (migrations + collectstatic); the image's init.sh ENTRYPOINT is bypassed so ordering is ours.
  • Data dir pinned to /app/data — media/static/config.yaml/secret all persist; the image's own SECRET_KEY_FILE convention then lives in localstorage too.
  • Health endpoint: /api/system/health/ (upstream's unauthenticated probe path).
  • grind-stack fix: image names must be lowercase — dir basenames are lowercased for tags/volumes (grind-InvenTree:test was invalid).

Verification (grind-stack, ephemeral pg+redis): first boot ~7.5 min (migrations + static collection) → health 200; home 302→login; /accounts/oidc/cloudron/login/ 302s to the issuer (provider JSON valid); worker gate fired; version-check JSONDecodeError in logs = release-API fetch noise (sandboxed network), benign.

Files Created: Dockerfile, CloudronManifest.json (port 8000, localstorage + postgresql + redis, 2048MB, healthCheckPath /api/system/health/), start.sh, README.md, CHANGELOG.md, .dockerignore, logo.png (from repo docs/docs/assets).

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


Packaging Pattern: Download Pre-Compiled Binaries

When to Use

  • Application provides official pre-compiled binaries/releases
  • Compilation is complex or requires many dependencies
  • Want to reduce build time and complexity

Template

FROM ubuntu:22.04

# Install runtime dependencies
RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Download pre-compiled binary
ARG VERSION=1.0.0
ENV APP_PATH=https://releases.example.com/app-${VERSION}-linux-amd64.tar.gz

RUN curl -sL $APP_PATH -o /tmp/app.tar.gz && \
    tar zxvf /tmp/app.tar.gz -C /app && \
    rm /tmp/app.tar.gz

# Configure and start
COPY config.yaml /app/config/
COPY start.sh /app/start.sh

EXPOSE 8080
CMD ["/app/start.sh"]

Pros:

  • Very fast builds (just download and extract)
  • No build dependencies needed
  • Uses upstream-optimized binaries
  • Reproducible builds

Cons:

  • Dependent on upstream releasing binaries
  • Limited customization options
  • Architecture-specific (amd64 only usually)
  • Security concerns with third-party binaries

Examples: Corteza