ChirpStack 4.19.1 (LoRaWAN network server) as the 14th package: official-image wrapper, digest-pinned, with only bash added to the upstream alpine runtime. Native OIDC login wired to the Cloudron platform provider (openid_connect backend, config regenerated every start); postgresql + redis addons; diesel migrations auto-run. Operator config (NetID, US915 region, gateway MQTT backend) lives in seeded files under /app/data/config. Build green; config generation validated through chirpstack's own TOML parser. Docs gardened to 14 packages. Ticket: https://projects.knownelement.com/issues/668 💘 Generated with Crush Assisted-by: Crush:glm-5.2
42 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: 14/~57 packages completed (~25%)
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]
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]
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