STATUS/README inventory updated (APISIX deployed at apigw.knownelement.com); JOURNAL section 20 (rewrite + deploy). Umbrella: https://projects.knownelement.com/issues/632
63 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 installinstead ofnpm cifor Cloudron builds - Use
--no-optionalflag to reduce complexity - Use
--production=falseflag 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/datafor persistent storage - Cloudron manages this directory
- Backup and restore handled automatically
6. Startup Scripts
- Make scripts executable on host
- Use
set -efor 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
- Reading documentation and understanding application (10-15 min)
- Writing comprehensive README.md (10-15 min)
- Writing Dockerfile (5-10 min)
- Testing Docker build (10-15 min)
- 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
- Continue packaging remaining applications (53 remaining)
- Prioritize simpler applications to increase throughput
- Focus on categories with official Docker images
- Create packaging templates for common patterns
Future Improvements
- Automated package generation script
- Pre-commit hooks to validate package structure
- Integration testing for each package
- Performance benchmarking of packages
- Security scanning (hadolint, trivy, dockle, dive, syft)
Lessons Learned Summary
- Always read official Docker documentation before creating wrapper
- Make scripts executable on host to avoid permission errors
- Use .dockerignore to reduce build context and image size
- Wait for dependencies (database, services) before starting
- Use environment variable defaults with
${VAR:-default}pattern - Create comprehensive documentation - it saves time later
- Test Docker builds locally before committing
- Commit frequently - atomic commits make troubleshooting easier
- Use multi-stage builds for compiled applications
- Leverage official images when available - less maintenance
- Document ALL environment variables - helps with debugging
- Include examples in README - users appreciate practical guidance
- Handle errors gracefully in start scripts
- Use Cloudron base only when necessary - official images preferred
- 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 inspectto 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 curlfor 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→ successdocker 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/NOTIFYfor 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_URLis composed at runtime instart.shfrom 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 inspectto confirm WorkingDir/usr/src/app, binarywindmillon PATH, port 8000 start.shwaits for Postgres (bash/dev/tcp, nopg_isreadydependency), thenexec 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/version→CE 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-alpinebuilder,CGO_ENABLED=0 -trimpath -ldflags="-w -s"(mirrors upstream Makefile), static binary ontocloudron/base:3.2.0 - Config-driven design: the app re-parses
easy-gate.jsonevery second — no restart needed on config edits; start.sh only seeds the first copy behind_proxy: truematters on Cloudron: group subnet matching usesX-Forwarded-For(Cloudron's nginx passes the real client IP)- No
.env.exampleshipped: 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-gatev2.0.3 cloned intorepo/(gitignored) go mod downloadcached beforeCOPY repo/ .for layer reuse- Logo converted from upstream
assets/logo.svgto a 234x256 PNG via ImageMagick on the host - start.sh: seeds
/app/data/easy-gate.jsonon first run, thenexec /usr/local/bin/easy-gate(envEASY_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_tokenseeded on first run), with optional Noise/TLS transport encryption →httpAuth.type = proxygates 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
nodelaydirectly 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
portCountallocates sequential ports fromdefaultValue(max 1000) withcontainerPortbridging — 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.serversidecar on the status page satisfies both and gives the auth proxy something to gate
Build Process:
- Upstream
rathole-org/ratholev0.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.20container (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 --versioninside 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 --clientagainst the packaged server;curl host:15200 → server:5200 → client → client:9999returned 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]
SUPERSEDED / REMOVED 2026-09-06 by founder ruling on #650: "remove this. we will use netbird for all enterprise network access." The package directory was deleted and the app dropped from GitUrlList and all counts. This section is retained (append-only) as the historical record of the pre-compiled-binaries pattern work.
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_SECRETare written intousers.*of the seeded config; redirect URL derives fromCLOUDRON_APP_ORIGIN+/auth/callback; roles map from thegroupsclaim (Cloudronadmins→ 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=0fails 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 requiredrole_mappingandpolicy.path. start.sh builds it withjq -n --argon 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-upsubcommand is run explicitly in start.sh (the app also self-migrates onrun— 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 everytables[].tablemust be schema-qualified (public.foo) and every role_mapping value must be exactlyadminoruser
Build Process:
- Upstream
kazhuravlev/database-gatewayv0.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.pngcopied 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-up→exec run
Validation:
docker build --cgroup-parent ukrrs-batch.slice -t database-gateway-cloudron:test→ green--versionsmoke 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 = proxygates 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
Tuxasset 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 = plaindef 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.gzdownloaded in-Dockerfile behind a sha256 pin taken from the upstream checksums.txt;fx -vruns 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.48printed 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*.tomlin 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 into10-cloudron.toml(generated every boot: logging, postgresql, redis, api, user_authentication — addon credentials stay current across Cloudron password rotations) and operator-owned50-network.toml+region_us915_0.tomlseeded once, editable via the file manager. - Migrations: embedded diesel migrations run automatically in
storage::setup()at startup and seed an internaladminuser (emailadmin, passwordadmin, 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.secretsigns login tokens; persisted at/app/data/.api_jwt_secretso 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_URLfeedsredis.serversdirectly (auth embedded in the URL).
Challenges & solutions:
- Hub API digest mismatch: the Docker Hub tags API reported an index
digest for
4.19.1that BuildKit refused (not foundwhen used astag@digest).docker manifest inspect --verbosegave the real registry digest (amd64 manifestsha256: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/tcpprobes 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.slicegreen; 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=trueserves plain HTTP on port 443 (TLS terminates at the Cloudron proxy) — the manifest'shttpPortis 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/tcpprobes (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.slicegreen (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 fromCLOUDRON_OIDC_*, with the issuer normalized to a trailing slash (social-core discovery requirement). Local Django login stays available for admin bootstrap (manage.py createsuperuservia 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 rqworkerbehind 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/tcpwait 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/dataat 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
createsuperuservia the terminal (mirrors the ChirpStack bootstrap story).SECRET_KEYgenerated 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.slicegreen (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/*.pyloaded; Granian[INFO] Listening at: http://:::8080; worker gate fired ("web port is up" → RQListening on high, default, low);GET /login/= 200 with the SSO button linking/oauth/login/oidc/— the OIDC backend is registered. (AHost: 127.0.0.1curl returns 400 — ALLOWED_HOSTS working as designed; the Cloudron proxy sends the real domain.)
- redis:7 (requirepass), mapped through the same
- 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):
ruby file: ".ruby-version": the Gemfile resolves its ruby version from.ruby-versionBY NAME — copying it under a different name killsbundle installwith a bare backtrace.eval_gemfile "./Gemfile_custom": the Gemfile expects a developer-localGemfile_customthat isn't in git —touchan empty one beforebundle install.- 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_timeoutdefault killed the graphiql minified-JS regexes under CPU contention — disabled via a baked initializer (Regexp.timeout = nil). - Named-volume ownership: a fresh docker volume is root-owned while
the container runs as uid 1000 — declare
VOLUME /app/dataAFTERchownin 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). - Seed verification over marker trust: one boot observed
db:create db:migrate db:seedexit 0 while persisting nothing; a standalone re-seed landed fine. start.sh now checksSetting.countand re-seeds an empty database regardless of the marker file. feature.oidc_logindefaults 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_URLbuilt from the postgresql addon env (sslmode=disable — the addon is on the platform network).GOALERT_DATA_ENCRYPTION_KEYpersisted under /app/data (rotating it loses encrypted notification-provider credentials).GOALERT_PUBLIC_URL←CLOUDRON_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 finishedengine 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 updatedriven 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:testwas 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
20. APISIX: Package Rewrite + Production Deploy (2026-09-07)
Trigger: founder directive — "APISIX needs to be on Cloudron at
apigw.knownelement.com". The existing package (#2, 2026-09-01) turned out
to be unbuildable as authored: it referenced a Cloudron etcd addon that
does not exist (verified against the platform source on the host — no
etcd anywhere in box/src), used unpinned apache/apisix:latest, had a
broken config.yaml (quoted heredoc blocked env substitution, invalid
deployment schema), and pointed the healthcheck at the auth-protected
admin API (would always 401).
Final design (v1.0.2) — official-image wrapper, rewritten:
apache/apisix:3.18.0-debianpinned by tag AND digest- Embedded single-node etcd (quay.io/coreos/etcd v3.5.33, digest-pinned,
binaries COPYed into the image) — state under
/app/data/etcd; keeps the Admin API + dynamic routes with zero platform addon dependencies - Admin API bound to 127.0.0.1:9180 only, protected by a persisted
random 64-hex key in
/app/data/.admin_key; operators usecloudron exec healthCheckPath "/"— Cloudron treats 2xx/3xx/4xx as alive (verified in box/src/apphealthmonitor.js);/healthzseeded into etcd once as a serverless-pre-function direct response for external monitoring- Manifest: semver
1.0.2,localstorage: {}(object form — boolean form fails current schema validation),typefield removed (rejected), 1 GiB memory limit, NO tcpPorts (all traffic via the platform proxy)
Lessons (all verified the hard way):
- Cloudron 8 app rootfs is READ-ONLY (writable: /app/data, /tmp, /run).
APISIX must live its runtime life on /app/data: APISIX_PREFIX +
a path-patched copy of the CLI tree —
apisix_homeis HARDCODED/usr/local/apisixin apisix/cli/apisix.lua, so the whole apisix/ + deps/ trees (~62 MB) are copied to /app/data/apisix and sed-patched on every start. - Cloudron does not chown /app/data to arbitrary image users — the
apisix (uid 636) image user got EACCES. Apps run as root (platform
convention); then nginx's workers run as nobody (no
userdirective in the generated nginx.conf), which forced config.yaml to 644. - Pre-existing DNS records block app install ("DNS A record already
exists"): apigw pointed at a stale 2024 external host
(ruby.ontrixsolutions.com).
POST /api/v1/apps/:id/repairre-runs the task withoverwriteDns: true(apps.js:2550) and upserts the record through the platform's own DNS provider creds — the clean fix, no provider API access needed. - Custom app install API:
POST /api/v1/appswithappStoreId: "", a full manifest, andmanifest.dockerImagefor the image; updates viaPOST /api/v1/apps/:id/updatewith{manifest, skipBackup, force}. Server source on the host (/home/yellowtent/box/src) is the authoritative API spec — docs pages 404/JS-render. - grind-stack.sh fidelity upgrades (kept for every future package):
- /app/data chowned to the image's uid (Cloudron localstorage contract)
--read-only --tmpfs /tmp --tmpfs /runon the app container (would have caught the rootfs issue BEFORE production)sbomsubcommand wired into the dispatch table (it existed but was unreachable)- probe containers need
--entrypointwhen the image wraps everything in start.sh
Delivery path: built under ukrrs-batch.slice → pushed to the platform's
own cloudron-docker-registry app (Basic auth through the Cloudron proxy
validates platform user creds). Created svc account svpengops-registry
added to the registry app's accessRestriction; the platform's docker
registry entry (previously mrcharles, whose stored password 403'd) was
updated to the svc account. Password vaulting pending (sm session expired).
Verification trail: grind-stack green (healthz 200, admin 401/200 with
key, restart persistence in etcd, all under --read-only) → push digest
sha256:754731d8… → live update task 15710 → https://apigw.knownelement.com/healthz
= 200 from production; container stable; etcd healthy via exec.
Status: DEPLOYED — awaiting founder UAT. Redmine note deferred (vault session expired → mred key unreadable); dedicated ticket to be filed under the cloudron project on next vault login (gate ran under umbrella #632).