diff --git a/.agent/scripts/cloudron-package-helper.sh b/.agent/scripts/cloudron-package-helper.sh new file mode 100755 index 00000000..7fd003d1 --- /dev/null +++ b/.agent/scripts/cloudron-package-helper.sh @@ -0,0 +1,925 @@ +#!/usr/bin/env bash +# cloudron-package-helper.sh - Cloudron app packaging development workflow +# Usage: cloudron-package-helper.sh [command] [args] +# +# Commands: +# init [name] Initialize new Cloudron app package +# validate Validate CloudronManifest.json +# build Build Docker image +# install [location] Install app on Cloudron +# update Update installed app +# logs [app] View app logs +# exec [app] Shell into app container +# debug [app] Enable debug mode +# debug-off [app] Disable debug mode +# test [app] Run validation checklist +# scaffold [type] Generate boilerplate (php|node|python|go|static) +# status Show current package status + +set -euo pipefail + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Logging functions +log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } +log_success() { echo -e "${GREEN}[OK]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# Check if cloudron CLI is installed +check_cloudron_cli() { + if ! command -v cloudron &> /dev/null; then + log_error "Cloudron CLI not found. Install with: npm install -g cloudron" + return 1 + fi + return 0 +} + +# Check if we're in a Cloudron app directory +check_app_dir() { + if [[ ! -f "CloudronManifest.json" ]]; then + log_error "CloudronManifest.json not found. Are you in a Cloudron app directory?" + return 1 + fi + return 0 +} + +# Check if jq is installed +check_jq() { + if ! command -v jq &> /dev/null; then + log_error "jq not found. Install with: brew install jq (macOS) or apt-get install jq (Linux)" + return 1 + fi + return 0 +} + +# Initialize new Cloudron app package +cmd_init() { + local name="${1:-}" + + check_cloudron_cli || return 1 + + if [[ -n "$name" ]]; then + mkdir -p "$name" + cd "$name" + log_info "Created directory: $name" + fi + + if [[ -f "CloudronManifest.json" ]]; then + log_warn "CloudronManifest.json already exists" + read -rp "Overwrite? [y/N] " confirm + [[ "$confirm" != "y" && "$confirm" != "Y" ]] && return 0 + fi + + cloudron init + + # Create basic start.sh if not exists + if [[ ! -f "start.sh" ]]; then + cat > start.sh << 'STARTSH' +#!/bin/bash +set -eu + +echo "==> Starting Cloudron App" + +# First-run detection +if [[ ! -f /app/data/.initialized ]]; then + FIRST_RUN=true + echo "==> First run detected" +else + FIRST_RUN=false +fi + +# Create directories +mkdir -p /app/data/config /app/data/storage /app/data/logs +mkdir -p /run/app + +# First-run initialization +if [[ "$FIRST_RUN" == "true" ]]; then + echo "==> Copying default configs" + cp -rn /app/code/defaults/* /app/data/ 2>/dev/null || true +fi + +# Fix permissions +chown -R cloudron:cloudron /app/data /run/app + +# Mark initialized +touch /app/data/.initialized + +# Launch application (replace with your command) +echo "==> Launching application" +exec gosu cloudron:cloudron echo "Replace this with your app start command" +STARTSH + chmod +x start.sh + log_success "Created start.sh template" + fi + + # Create basic Dockerfile if not exists + if [[ ! -f "Dockerfile" ]]; then + cat > Dockerfile << 'DOCKERFILE' +FROM cloudron/base:5.0.0 + +# Install dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + # Add your dependencies here \ + && rm -rf /var/lib/apt/lists/* + +# Copy application code +WORKDIR /app/code +COPY --chown=cloudron:cloudron . /app/code/ + +# Preserve defaults for first-run +RUN mkdir -p /app/code/defaults + +# Add start script +COPY start.sh /app/code/start.sh +RUN chmod +x /app/code/start.sh + +EXPOSE 8000 + +CMD ["/app/code/start.sh"] +DOCKERFILE + log_success "Created Dockerfile template" + fi + + # Create .gitignore if not exists + if [[ ! -f ".gitignore" ]]; then + cat > .gitignore << 'GITIGNORE' +# Cloudron +.cloudron/ + +# aidevops +.agent/loop-state/ +*.local.md + +# Build artifacts +node_modules/ +vendor/ +dist/ +__pycache__/ +*.pyc +GITIGNORE + log_success "Created .gitignore" + fi + + log_success "Cloudron app package initialized" + log_info "Next steps:" + echo " 1. Edit CloudronManifest.json with your app details" + echo " 2. Edit Dockerfile to install your app" + echo " 3. Edit start.sh with your startup logic" + echo " 4. Run: cloudron-package-helper.sh build" + echo " 5. Run: cloudron-package-helper.sh install testapp" +} + +# Validate CloudronManifest.json +cmd_validate() { + check_app_dir || return 1 + check_jq || return 1 + + log_info "Validating CloudronManifest.json..." + + local errors=0 + local manifest + manifest=$(cat CloudronManifest.json) + + # Check required fields + local required_fields=("id" "title" "version" "healthCheckPath" "httpPort" "manifestVersion") + for field in "${required_fields[@]}"; do + if ! echo "$manifest" | jq -e ".$field" > /dev/null 2>&1; then + log_error "Missing required field: $field" + errors=$((errors + 1)) + fi + done + + # Check manifestVersion + local manifest_version + manifest_version=$(echo "$manifest" | jq -r '.manifestVersion // 0') + if [[ "$manifest_version" != "2" ]]; then + log_error "manifestVersion must be 2 (got: $manifest_version)" + errors=$((errors + 1)) + fi + + # Check httpPort is a number + local http_port + http_port=$(echo "$manifest" | jq -r '.httpPort // "null"') + if ! [[ "$http_port" =~ ^[0-9]+$ ]]; then + log_error "httpPort must be a positive integer" + errors=$((errors + 1)) + fi + + # Check localstorage addon if app likely needs persistence + if ! echo "$manifest" | jq -e '.addons.localstorage' > /dev/null 2>&1; then + log_warn "localstorage addon not declared - app won't have persistent storage" + fi + + # Check for icon + if [[ ! -f "logo.png" ]] && [[ ! -f "icon.png" ]]; then + log_warn "No logo.png or icon.png found (recommended: 256x256)" + fi + + # Check start.sh exists and is executable + if [[ ! -f "start.sh" ]]; then + log_error "start.sh not found" + errors=$((errors + 1)) + elif [[ ! -x "start.sh" ]]; then + log_warn "start.sh is not executable (run: chmod +x start.sh)" + fi + + # Check Dockerfile exists + if [[ ! -f "Dockerfile" ]] && [[ ! -f "Dockerfile.cloudron" ]]; then + log_error "Dockerfile not found" + errors=$((errors + 1)) + fi + + if [[ $errors -eq 0 ]]; then + log_success "Validation passed" + return 0 + else + log_error "Validation failed with $errors error(s)" + return 1 + fi +} + +# Build Docker image +cmd_build() { + check_cloudron_cli || return 1 + check_app_dir || return 1 + + log_info "Building Cloudron app..." + cloudron build + log_success "Build complete" +} + +# Install app on Cloudron +cmd_install() { + local location="${1:-}" + + check_cloudron_cli || return 1 + check_app_dir || return 1 + + if [[ -z "$location" ]]; then + log_error "Usage: cloudron-package-helper.sh install " + log_info "Example: cloudron-package-helper.sh install testapp" + return 1 + fi + + log_info "Installing app at location: $location" + cloudron install --location "$location" + log_success "App installed at $location" +} + +# Update installed app +cmd_update() { + local app="${1:-}" + + check_cloudron_cli || return 1 + check_app_dir || return 1 + + log_info "Building and updating app..." + cloudron build + + if [[ -n "$app" ]]; then + cloudron update --app "$app" + else + cloudron update + fi + log_success "App updated" +} + +# View app logs +cmd_logs() { + local app="${1:-}" + + check_cloudron_cli || return 1 + + if [[ -n "$app" ]]; then + cloudron logs -f --app "$app" + else + cloudron logs -f + fi +} + +# Shell into app container +cmd_exec() { + local app="${1:-}" + + check_cloudron_cli || return 1 + + if [[ -n "$app" ]]; then + cloudron exec --app "$app" + else + cloudron exec + fi +} + +# Enable debug mode +cmd_debug() { + local app="${1:-}" + + check_cloudron_cli || return 1 + + log_info "Enabling debug mode (filesystem becomes writable, app paused)" + if [[ -n "$app" ]]; then + cloudron debug --app "$app" + else + cloudron debug + fi + log_success "Debug mode enabled. Use 'cloudron exec' to access container" +} + +# Disable debug mode +cmd_debug_off() { + local app="${1:-}" + + check_cloudron_cli || return 1 + + log_info "Disabling debug mode" + if [[ -n "$app" ]]; then + cloudron debug --disable --app "$app" + else + cloudron debug --disable + fi + log_success "Debug mode disabled" +} + +# Run validation checklist +cmd_test() { + local app="${1:-}" + + check_cloudron_cli || return 1 + + log_info "Running validation checklist..." + echo "" + echo "Manual Validation Checklist:" + echo "============================" + echo "" + echo "[ ] Fresh install completes without errors" + echo " cloudron install --location testapp" + echo "" + echo "[ ] App survives restart" + echo " cloudron restart --app testapp" + echo "" + echo "[ ] Health check returns 200" + echo " curl -v https://testapp.yourdomain.com/health" + echo "" + echo "[ ] File uploads persist across restarts" + echo " (upload file, restart, verify file exists)" + echo "" + echo "[ ] Database connections work" + echo " cloudron exec --app testapp" + echo " env | grep CLOUDRON" + echo "" + echo "[ ] Email sending works (if applicable)" + echo "" + echo "[ ] Memory stays within limit" + echo " cloudron logs --app testapp | grep -i memory" + echo "" + echo "[ ] Upgrade from previous version works" + echo " cloudron update --app testapp" + echo "" + echo "[ ] Backup/restore cycle works" + echo " (create backup, restore, verify)" + echo "" + echo "[ ] Auto-updater is disabled" + echo " (check app settings)" + echo "" + echo "[ ] Logs stream to stdout/stderr" + echo " cloudron logs -f --app testapp" + echo "" +} + +# Generate boilerplate for specific app types +cmd_scaffold() { + local app_type="${1:-}" + + case "$app_type" in + php) + scaffold_php + ;; + node) + scaffold_node + ;; + python) + scaffold_python + ;; + go) + scaffold_go + ;; + static) + scaffold_static + ;; + *) + log_error "Usage: cloudron-package-helper.sh scaffold " + echo "Types: php, node, python, go, static" + return 1 + ;; + esac +} + +scaffold_php() { + log_info "Generating PHP app scaffold..." + + # Check for existing files + if [[ -f "Dockerfile" || -f "start.sh" ]]; then + log_warn "This will overwrite existing Dockerfile and start.sh" + read -rp "Continue? [y/N] " confirm + if ! [[ "$confirm" =~ ^[Yy]$ ]]; then + log_info "Scaffold cancelled." + return 0 + fi + fi + + cat > Dockerfile << 'EOF' +FROM cloudron/base:5.0.0 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + nginx \ + php8.2-fpm \ + php8.2-mysql \ + php8.2-pgsql \ + php8.2-curl \ + php8.2-gd \ + php8.2-mbstring \ + php8.2-xml \ + php8.2-zip \ + && rm -rf /var/lib/apt/lists/* + +# Fix PHP session path +RUN rm -rf /var/lib/php/sessions && \ + ln -s /run/php/sessions /var/lib/php/sessions + +WORKDIR /app/code +COPY --chown=cloudron:cloudron . /app/code/ + +# Preserve defaults +RUN mkdir -p /app/code/defaults && \ + mv /app/code/config /app/code/defaults/config 2>/dev/null || true && \ + mv /app/code/storage /app/code/defaults/storage 2>/dev/null || true + +COPY start.sh /app/code/start.sh +RUN chmod +x /app/code/start.sh + +EXPOSE 8000 + +CMD ["/app/code/start.sh"] +EOF + + cat > start.sh << 'EOF' +#!/bin/bash +set -eu + +echo "==> Starting PHP App" + +# First-run detection +if [[ ! -f /app/data/.initialized ]]; then + FIRST_RUN=true + echo "==> First run detected" +else + FIRST_RUN=false +fi + +# Create directories +mkdir -p /app/data/config /app/data/storage /app/data/logs +mkdir -p /run/php/sessions /run/nginx/client_body /run/nginx/proxy /run/nginx/fastcgi + +# Symlinks +ln -sfn /app/data/config /app/code/config +ln -sfn /app/data/storage /app/code/storage + +# First-run initialization +if [[ "$FIRST_RUN" == "true" ]]; then + cp -rn /app/code/defaults/config/* /app/data/config/ 2>/dev/null || true + cp -rn /app/code/defaults/storage/* /app/data/storage/ 2>/dev/null || true +fi + +# Fix permissions +chown -R www-data:www-data /app/data /run/php /run/nginx + +touch /app/data/.initialized + +# Start PHP-FPM +php-fpm8.2 -D + +# Start nginx +echo "==> Starting nginx" +exec nginx -g "daemon off;" +EOF + chmod +x start.sh + + log_success "PHP scaffold created" +} + +scaffold_node() { + log_info "Generating Node.js app scaffold..." + + # Check for existing files + if [[ -f "Dockerfile" || -f "start.sh" ]]; then + log_warn "This will overwrite existing Dockerfile and start.sh" + read -rp "Continue? [y/N] " confirm + if ! [[ "$confirm" =~ ^[Yy]$ ]]; then + log_info "Scaffold cancelled." + return 0 + fi + fi + + cat > Dockerfile << 'EOF' +FROM cloudron/base:5.0.0 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + nodejs \ + npm \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app/code +COPY package*.json ./ +RUN npm ci --production && npm cache clean --force + +COPY --chown=cloudron:cloudron . /app/code/ + +RUN mkdir -p /app/code/defaults + +COPY start.sh /app/code/start.sh +RUN chmod +x /app/code/start.sh + +ENV NODE_ENV=production + +EXPOSE 8000 + +CMD ["/app/code/start.sh"] +EOF + + cat > start.sh << 'EOF' +#!/bin/bash +set -eu + +echo "==> Starting Node.js App" + +if [[ ! -f /app/data/.initialized ]]; then + FIRST_RUN=true +else + FIRST_RUN=false +fi + +mkdir -p /app/data/config /app/data/storage +mkdir -p /run/app + +if [[ "$FIRST_RUN" == "true" ]]; then + cp -rn /app/code/defaults/* /app/data/ 2>/dev/null || true +fi + +chown -R cloudron:cloudron /app/data /run/app +touch /app/data/.initialized + +echo "==> Launching Node.js" +exec gosu cloudron:cloudron node /app/code/server.js +EOF + chmod +x start.sh + + log_success "Node.js scaffold created" +} + +scaffold_python() { + log_info "Generating Python app scaffold..." + + # Check for existing files + if [[ -f "Dockerfile" || -f "start.sh" ]]; then + log_warn "This will overwrite existing Dockerfile and start.sh" + read -rp "Continue? [y/N] " confirm + if ! [[ "$confirm" =~ ^[Yy]$ ]]; then + log_info "Scaffold cancelled." + return 0 + fi + fi + + cat > Dockerfile << 'EOF' +FROM cloudron/base:5.0.0 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + python3-pip \ + python3-venv \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app/code +COPY requirements.txt ./ +RUN pip3 install --no-cache-dir -r requirements.txt + +COPY --chown=cloudron:cloudron . /app/code/ + +RUN mkdir -p /app/code/defaults + +COPY start.sh /app/code/start.sh +RUN chmod +x /app/code/start.sh + +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +EXPOSE 8000 + +CMD ["/app/code/start.sh"] +EOF + + cat > start.sh << 'EOF' +#!/bin/bash +set -eu + +echo "==> Starting Python App" + +if [[ ! -f /app/data/.initialized ]]; then + FIRST_RUN=true +else + FIRST_RUN=false +fi + +mkdir -p /app/data/config /app/data/storage +mkdir -p /run/app + +if [[ "$FIRST_RUN" == "true" ]]; then + cp -rn /app/code/defaults/* /app/data/ 2>/dev/null || true +fi + +chown -R cloudron:cloudron /app/data /run/app +touch /app/data/.initialized + +echo "==> Launching Python" +exec gosu cloudron:cloudron python3 /app/code/app.py +EOF + chmod +x start.sh + + # Create empty requirements.txt + touch requirements.txt + + log_success "Python scaffold created" +} + +scaffold_go() { + log_info "Generating Go app scaffold..." + + # Check for existing files + if [[ -f "Dockerfile" || -f "start.sh" ]]; then + log_warn "This will overwrite existing Dockerfile and start.sh" + read -rp "Continue? [y/N] " confirm + if ! [[ "$confirm" =~ ^[Yy]$ ]]; then + log_info "Scaffold cancelled." + return 0 + fi + fi + + cat > Dockerfile << 'EOF' +FROM golang:1.21 AS builder + +WORKDIR /build +COPY go.* ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -o app . + +FROM cloudron/base:5.0.0 + +WORKDIR /app/code +COPY --from=builder /build/app /app/code/app + +RUN mkdir -p /app/code/defaults + +COPY start.sh /app/code/start.sh +RUN chmod +x /app/code/start.sh + +EXPOSE 8000 + +CMD ["/app/code/start.sh"] +EOF + + cat > start.sh << 'EOF' +#!/bin/bash +set -eu + +echo "==> Starting Go App" + +if [[ ! -f /app/data/.initialized ]]; then + FIRST_RUN=true +else + FIRST_RUN=false +fi + +mkdir -p /app/data/config /app/data/storage + +if [[ "$FIRST_RUN" == "true" ]]; then + cp -rn /app/code/defaults/* /app/data/ 2>/dev/null || true +fi + +chown -R cloudron:cloudron /app/data +touch /app/data/.initialized + +echo "==> Launching Go binary" +exec gosu cloudron:cloudron /app/code/app +EOF + chmod +x start.sh + + log_success "Go scaffold created" +} + +scaffold_static() { + log_info "Generating static site scaffold..." + + # Check for existing files + if [[ -f "Dockerfile" || -f "start.sh" ]]; then + log_warn "This will overwrite existing Dockerfile and start.sh" + read -rp "Continue? [y/N] " confirm + if ! [[ "$confirm" =~ ^[Yy]$ ]]; then + log_info "Scaffold cancelled." + return 0 + fi + fi + + cat > Dockerfile << 'EOF' +FROM cloudron/base:5.0.0 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + nginx \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app/code +COPY --chown=cloudron:cloudron . /app/code/ + +COPY nginx.conf /etc/nginx/sites-available/app.conf +RUN ln -sf /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/app.conf && \ + rm -f /etc/nginx/sites-enabled/default + +COPY start.sh /app/code/start.sh +RUN chmod +x /app/code/start.sh + +EXPOSE 8000 + +CMD ["/app/code/start.sh"] +EOF + + cat > nginx.conf << 'EOF' +client_body_temp_path /run/nginx/client_body; +proxy_temp_path /run/nginx/proxy; +fastcgi_temp_path /run/nginx/fastcgi; + +server { + listen 8000; + root /app/code/public; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} +EOF + + cat > start.sh << 'EOF' +#!/bin/bash +set -eu + +echo "==> Starting Static Site" + +mkdir -p /run/nginx/client_body /run/nginx/proxy /run/nginx/fastcgi + +echo "==> Starting nginx" +exec nginx -g "daemon off;" +EOF + chmod +x start.sh + + mkdir -p public + echo "

Hello from Cloudron!

" > public/index.html + + log_success "Static site scaffold created" +} + +# Show current package status +cmd_status() { + check_app_dir || return 1 + check_jq || return 1 + + log_info "Package Status" + echo "" + + # Read manifest + local manifest + manifest=$(cat CloudronManifest.json) + + echo "App ID: $(echo "$manifest" | jq -r '.id // "not set"')" + echo "Title: $(echo "$manifest" | jq -r '.title // "not set"')" + echo "Version: $(echo "$manifest" | jq -r '.version // "not set"')" + echo "HTTP Port: $(echo "$manifest" | jq -r '.httpPort // "not set"')" + echo "Health Path: $(echo "$manifest" | jq -r '.healthCheckPath // "not set"')" + echo "" + + echo "Addons:" + echo "$manifest" | jq -r '.addons // {} | keys[]' | while read -r addon; do + echo " - $addon" + done + echo "" + + echo "Files:" + [[ -f "Dockerfile" ]] && echo " [x] Dockerfile" || echo " [ ] Dockerfile" + [[ -f "Dockerfile.cloudron" ]] && echo " [x] Dockerfile.cloudron" + [[ -f "start.sh" ]] && echo " [x] start.sh" || echo " [ ] start.sh" + [[ -f "logo.png" ]] && echo " [x] logo.png" || echo " [ ] logo.png (recommended)" + echo "" +} + +# Show help +show_help() { + cat << 'HELP' +Cloudron App Packaging Helper + +Usage: cloudron-package-helper.sh [command] [args] + +Commands: + init [name] Initialize new Cloudron app package + validate Validate CloudronManifest.json + build Build Docker image + install Install app on Cloudron + update [app] Build and update installed app + logs [app] View app logs (follows) + exec [app] Shell into app container + debug [app] Enable debug mode + debug-off [app] Disable debug mode + test [app] Show validation checklist + scaffold Generate boilerplate (php|node|python|go|static) + status Show current package status + help Show this help + +Examples: + cloudron-package-helper.sh init myapp + cloudron-package-helper.sh scaffold php + cloudron-package-helper.sh validate + cloudron-package-helper.sh build + cloudron-package-helper.sh install testapp + cloudron-package-helper.sh update + cloudron-package-helper.sh logs testapp + +Documentation: + https://docs.cloudron.io/packaging/ + https://forum.cloudron.io/category/96/app-packaging-development +HELP +} + +# Main entry point +main() { + local command="${1:-help}" + shift || true + + case "$command" in + init) + cmd_init "$@" + ;; + validate) + cmd_validate "$@" + ;; + build) + cmd_build "$@" + ;; + install) + cmd_install "$@" + ;; + update) + cmd_update "$@" + ;; + logs) + cmd_logs "$@" + ;; + exec) + cmd_exec "$@" + ;; + debug) + cmd_debug "$@" + ;; + debug-off) + cmd_debug_off "$@" + ;; + test) + cmd_test "$@" + ;; + scaffold) + cmd_scaffold "$@" + ;; + status) + cmd_status "$@" + ;; + help|--help|-h) + show_help + ;; + *) + log_error "Unknown command: $command" + show_help + return 1 + ;; + esac +} + +main "$@" diff --git a/.agent/subagent-index.toon b/.agent/subagent-index.toon index e5fd76bb..49e361dd 100644 --- a/.agent/subagent-index.toon +++ b/.agent/subagent-index.toon @@ -22,7 +22,7 @@ flash,gemini-2.5-flash,Fast cheap large context pro,gemini-2.5-pro,Capable large context --> - - diff --git a/.agent/tools/deployment/cloudron-app-packaging.md b/.agent/tools/deployment/cloudron-app-packaging.md new file mode 100644 index 00000000..c250073b --- /dev/null +++ b/.agent/tools/deployment/cloudron-app-packaging.md @@ -0,0 +1,800 @@ +--- +description: Package custom applications for Cloudron deployment +mode: subagent +tools: + read: true + write: true + edit: true + bash: true + glob: true + grep: true + webfetch: true + task: true +--- + +# Cloudron App Packaging Guide + + + +## Quick Reference + +- **Purpose**: Package any web application for Cloudron deployment +- **Docs**: [docs.cloudron.io/packaging](https://docs.cloudron.io/packaging/tutorial/) +- **Examples**: [git.cloudron.io/cloudron](https://git.cloudron.io/cloudron) (all official apps) +- **CLI**: `npm install -g cloudron` then `cloudron login my.cloudron.example` +- **Workflow**: `cloudron build && cloudron update --app testapp` +- **Debug**: `cloudron exec --app testapp` or `cloudron debug --app testapp` +- **Forum**: [forum.cloudron.io/category/96/app-packaging-development](https://forum.cloudron.io/category/96/app-packaging-development) + +**Golden Rules** (violations cause package failure): +1. `/app/code` is READ-ONLY at runtime - use `/app/data` for persistent storage +2. Run processes as `cloudron` user (UID 1000) via `exec gosu cloudron:cloudron` +3. Use Cloudron addons (mysql, postgresql, redis) - never bundle databases +4. Disable built-in auto-updaters - Cloudron manages updates via image replacement +5. App receives HTTP (not HTTPS) - Cloudron's nginx terminates SSL + +**File Structure**: +```text +my-app/ + CloudronManifest.json # App metadata and addon requirements + Dockerfile # Build instructions (or Dockerfile.cloudron) + start.sh # Runtime entry point + logo.png # 256x256 app icon +``` + +**Quick Start**: +```bash +# Initialize new app package +cloudron init + +# Build and test +cloudron build +cloudron install --location testapp + +# Iterate +cloudron build && cloudron update --app testapp +cloudron logs -f --app testapp +``` + + +## Overview + +Cloudron app packaging creates Docker containers that integrate with Cloudron's platform features: automatic SSL, user management (LDAP/OIDC), backups, and addon services. + +**Key Concepts**: +- Apps run in isolated Docker containers with read-only filesystems +- Persistent data stored in `/app/data` (backed up automatically) +- Services (databases, email, auth) provided via addons with environment variables +- Health checks determine app readiness +- Start scripts handle initialization and configuration injection + +## Decision Trees + +### Base Image Selection + +```text +Need web terminal access or complex deps? + YES -> cloudron/base:5.0.0 (recommended default) + NO -> Does app provide official slim image? + YES -> Use official (e.g., php:8.2-fpm-bookworm) + NO -> Need minimal size + no glibc deps? + YES -> Alpine variant (e.g., node:20-alpine) + NO -> cloudron/base:5.0.0 +``` + +**Why cloudron/base is the safe default**: +- Pre-configured locales (prevents unicode crashes) +- Includes `gosu` for privilege dropping +- Web terminal compatibility (bash, utilities) +- Consistent glibc environment +- Security updates managed by Cloudron team + +**Version check**: https://hub.docker.com/r/cloudron/base/tags + +### Addon Selection + +| App Needs | Addon | Environment Variables | +|-----------|-------|----------------------| +| Persistent storage | `localstorage` | (provides `/app/data`) | +| MySQL/MariaDB | `mysql` | `CLOUDRON_MYSQL_*` | +| PostgreSQL | `postgresql` | `CLOUDRON_POSTGRESQL_*` | +| MongoDB | `mongodb` | `CLOUDRON_MONGODB_*` | +| Redis cache | `redis` | `CLOUDRON_REDIS_*` | +| Send email | `sendmail` | `CLOUDRON_MAIL_SMTP_*` | +| Receive email | `recvmail` | `CLOUDRON_MAIL_IMAP_*` | +| LDAP auth | `ldap` | `CLOUDRON_LDAP_*` | +| OIDC auth | `oidc` | `CLOUDRON_OIDC_*` | +| Cron jobs | `scheduler` | (config in manifest) | +| TLS certs | `tls` | `/etc/certs/tls_*.pem` | + +**Note**: `localstorage` is MANDATORY for all apps that need persistent data. + +### Process Model Selection + +```text +Single process app (Node.js, Go, Rust)? + YES -> Direct exec in start.sh + NO -> Multiple processes needed? + YES -> Use supervisord + NO -> Web server manages children (Apache, nginx)? + YES -> Direct exec (they handle children) + NO -> Use supervisord +``` + +## Filesystem Permissions + +| Path | Runtime State | Purpose | +|------|---------------|---------| +| `/app/code` | READ-ONLY | Application code | +| `/app/data` | READ-WRITE | Persistent storage (backed up) | +| `/run` | READ-WRITE (wiped on restart) | Sockets, PIDs, sessions | +| `/tmp` | READ-WRITE (wiped on restart) | Temporary files, caches | + +### The Symlink Dance + +When apps expect to write to paths under `/app/code`: + +**Build Time (Dockerfile)**: +```dockerfile +# Preserve defaults for first-run initialization +RUN mkdir -p /app/code/defaults && \ + mv /app/code/config /app/code/defaults/config 2>/dev/null || true && \ + mv /app/code/storage /app/code/defaults/storage 2>/dev/null || true +``` + +**Runtime (start.sh)**: +```bash +# Create persistent directories +mkdir -p /app/data/config /app/data/storage /app/data/logs + +# First-run: copy defaults +if [[ ! -f /app/data/.initialized ]]; then + cp -rn /app/code/defaults/config/* /app/data/config/ 2>/dev/null || true + cp -rn /app/code/defaults/storage/* /app/data/storage/ 2>/dev/null || true +fi + +# Create symlinks (always recreate - safe and idempotent) +ln -sfn /app/data/config /app/code/config +ln -sfn /app/data/storage /app/code/storage +ln -sfn /app/data/logs /app/code/logs + +# Fix permissions +chown -R cloudron:cloudron /app/data + +# Mark initialized +touch /app/data/.initialized +``` + +### Ephemeral vs Persistent Decision + +| Data Type | Location | Rationale | +|-----------|----------|-----------| +| User uploads | `/app/data/uploads` | Must survive restarts | +| Config files | `/app/data/config` | Must survive restarts | +| SQLite databases | `/app/data/db` | Must survive restarts | +| Sessions | `/run/sessions` | Ephemeral is fine | +| View/template cache | `/run/cache` | Regenerated on start | +| Compiled assets | `/run/compiled` | Regenerated on start | + +## CloudronManifest.json + +### Complete Template + +```json +{ + "id": "io.example.myapp", + "title": "My Application", + "author": "Your Name ", + "description": "What this application does", + "tagline": "Short marketing description", + "version": "1.0.0", + "upstreamVersion": "2.5.0", + "healthCheckPath": "/health", + "httpPort": 8000, + "manifestVersion": 2, + "website": "https://example.com", + "contactEmail": "support@example.com", + "icon": "file://logo.png", + "documentationUrl": "https://docs.example.com", + "minBoxVersion": "7.4.0", + "memoryLimit": 536870912, + "addons": { + "localstorage": {}, + "postgresql": {} + }, + "tcpPorts": {} +} +``` + +### Memory Limit Guidelines + +| App Type | Recommended | Notes | +|----------|-------------|-------| +| Static/Simple PHP | 128-256 MB | | +| Node.js/Go/Rust | 256-512 MB | | +| PHP with workers | 512-768 MB | | +| Python/Ruby | 512-768 MB | | +| Java/JVM | 1024+ MB | JVM heap overhead | +| Electron-based | 1024+ MB | | + +**Note**: `memoryLimit` is in bytes. 256MB = 268435456, 512MB = 536870912, 1GB = 1073741824 + +### Health Check Requirements + +- Must return HTTP 200 when app is ready +- Should be unauthenticated (or use internal bypass) +- Common paths: `/health`, `/api/health`, `/ping`, `/` + +## Dockerfile Patterns + +### Basic Structure + +```dockerfile +FROM cloudron/base:5.0.0 + +# Install dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + nginx \ + php8.2-fpm \ + php8.2-mysql \ + && rm -rf /var/lib/apt/lists/* + +# Copy application code +WORKDIR /app/code +COPY --chown=cloudron:cloudron . /app/code/ + +# Preserve defaults for first-run +RUN mkdir -p /app/code/defaults && \ + mv /app/code/config /app/code/defaults/config 2>/dev/null || true + +# Add start script +COPY start.sh /app/code/start.sh +RUN chmod +x /app/code/start.sh + +# Expose port (documentation only, Cloudron uses httpPort from manifest) +EXPOSE 8000 + +CMD ["/app/code/start.sh"] +``` + +### Framework-Specific Patterns + +#### PHP Applications + +```dockerfile +# PHP temp paths must be writable +RUN rm -rf /var/lib/php/sessions && \ + ln -s /run/php/sessions /var/lib/php/sessions + +# In start.sh: +mkdir -p /run/php/sessions /run/php/uploads /run/php/tmp +chown -R www-data:www-data /run/php +``` + +PHP-FPM pool config: +```ini +php_value[session.save_path] = /run/php/sessions +php_value[upload_tmp_dir] = /run/php/uploads +php_value[sys_temp_dir] = /run/php/tmp +``` + +#### Node.js Applications + +```dockerfile +# Build time +RUN npm ci --production && npm cache clean --force + +# Runtime +ENV NODE_ENV=production +``` + +**Note**: `node_modules` stays in `/app/code` (never move to `/app/data`) + +#### Python Applications + +```dockerfile +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +RUN pip install --no-cache-dir -r requirements.txt +``` + +#### nginx as Reverse Proxy + +```nginx +# MANDATORY: Writable temp paths +client_body_temp_path /run/nginx/client_body; +proxy_temp_path /run/nginx/proxy; +fastcgi_temp_path /run/nginx/fastcgi; + +server { + listen 8000; # Internal port, never 80/443 + root /app/code/public; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } +} +``` + +In start.sh: +```bash +mkdir -p /run/nginx/client_body /run/nginx/proxy /run/nginx/fastcgi +``` + +#### Apache Configuration + +```dockerfile +RUN rm /etc/apache2/sites-enabled/* \ + && sed -e 's,^ErrorLog.*,ErrorLog "/dev/stderr",' -i /etc/apache2/apache2.conf \ + && sed -e "s,MaxSpareServers[^:].*,MaxSpareServers 5," -i /etc/apache2/mods-available/mpm_prefork.conf \ + && a2disconf other-vhosts-access-log \ + && echo "Listen 8000" > /etc/apache2/ports.conf +``` + +## start.sh Architecture + +### Complete Template + +```bash +#!/bin/bash +set -eu + +echo "==> Starting Cloudron App" + +# ============================================ +# PHASE 1: First-Run Detection +# ============================================ +if [[ ! -f /app/data/.initialized ]]; then + FIRST_RUN=true + echo "==> First run detected" +else + FIRST_RUN=false +fi + +# ============================================ +# PHASE 2: Directory Structure +# ============================================ +mkdir -p /app/data/config /app/data/storage /app/data/logs +mkdir -p /run/app /run/php /run/nginx # Ephemeral + +# ============================================ +# PHASE 3: Symlinks (always recreate - idempotent) +# ============================================ +ln -sfn /app/data/config /app/code/config +ln -sfn /app/data/storage /app/code/storage +ln -sfn /app/data/logs /app/code/logs + +# ============================================ +# PHASE 4: First-Run Initialization +# ============================================ +if [[ "$FIRST_RUN" == "true" ]]; then + echo "==> Copying default configs" + cp -rn /app/code/defaults/config/* /app/data/config/ 2>/dev/null || true +fi + +# ============================================ +# PHASE 5: Configuration Injection +# ============================================ +# Method A: Template substitution +envsubst < /app/code/config.template > /app/data/config/app.conf + +# Method B: Direct generation +cat > /app/data/config/database.json < true|'auto_update' => false|" /app/data/config/settings.php 2>/dev/null || true + +# ============================================ +# PHASE 7: Database Migrations +# ============================================ +echo "==> Running migrations" +gosu cloudron:cloudron /app/code/bin/migrate --force + +# ============================================ +# PHASE 8: Finalization +# ============================================ +chown -R cloudron:cloudron /app/data /run/app + +# Mark initialized +touch /app/data/.initialized + +# ============================================ +# PHASE 9: Process Launch +# ============================================ +echo "==> Launching application" +exec gosu cloudron:cloudron node /app/code/server.js +``` + +### Multi-Process with Supervisord + +```ini +# /app/code/supervisord.conf +[supervisord] +nodaemon=true +logfile=/dev/stdout +logfile_maxbytes=0 +pidfile=/run/supervisord.pid + +[program:web] +command=/app/code/bin/web-server +directory=/app/code +user=cloudron +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:worker] +command=/app/code/bin/worker +directory=/app/code +user=cloudron +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +``` + +End of start.sh for multi-process: +```bash +exec /usr/bin/supervisord --configuration /app/code/supervisord.conf +``` + +## Addon Environment Variables + +### PostgreSQL + +```bash +CLOUDRON_POSTGRESQL_URL=postgres://user:pass@host:5432/dbname +CLOUDRON_POSTGRESQL_HOST=postgresql +CLOUDRON_POSTGRESQL_PORT=5432 +CLOUDRON_POSTGRESQL_USERNAME=username +CLOUDRON_POSTGRESQL_PASSWORD=password +CLOUDRON_POSTGRESQL_DATABASE=dbname +``` + +### MySQL + +```bash +CLOUDRON_MYSQL_URL=mysql://user:pass@host:3306/dbname +CLOUDRON_MYSQL_HOST=mysql +CLOUDRON_MYSQL_PORT=3306 +CLOUDRON_MYSQL_USERNAME=username +CLOUDRON_MYSQL_PASSWORD=password +CLOUDRON_MYSQL_DATABASE=dbname +``` + +### Redis + +```bash +CLOUDRON_REDIS_URL=redis://:password@host:6379 +CLOUDRON_REDIS_HOST=redis +CLOUDRON_REDIS_PORT=6379 +CLOUDRON_REDIS_PASSWORD=password +``` + +**Note**: Cloudron Redis REQUIRES authentication. + +### Sendmail (SMTP) + +```bash +CLOUDRON_MAIL_SMTP_SERVER=mail +CLOUDRON_MAIL_SMTP_PORT=587 +CLOUDRON_MAIL_SMTP_USERNAME=username +CLOUDRON_MAIL_SMTP_PASSWORD=password +CLOUDRON_MAIL_FROM=app@domain.com +CLOUDRON_MAIL_DOMAIN=domain.com +``` + +### LDAP + +```bash +CLOUDRON_LDAP_URL=ldap://host:389 +CLOUDRON_LDAP_SERVER=ldap +CLOUDRON_LDAP_PORT=389 +CLOUDRON_LDAP_BIND_DN=cn=admin,dc=cloudron +CLOUDRON_LDAP_BIND_PASSWORD=password +CLOUDRON_LDAP_USERS_BASE_DN=ou=users,dc=cloudron +CLOUDRON_LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron +``` + +### OIDC (OAuth) + +```bash +CLOUDRON_OIDC_ISSUER=https://my.cloudron.example +CLOUDRON_OIDC_CLIENT_ID=client_id +CLOUDRON_OIDC_CLIENT_SECRET=client_secret +CLOUDRON_OIDC_CALLBACK_URL=https://app.domain.com/callback +``` + +### General Variables (Always Available) + +```bash +CLOUDRON_APP_ORIGIN=https://app.domain.com # Full URL with protocol +CLOUDRON_APP_DOMAIN=app.domain.com # Domain only +CLOUDRON=1 # Always set to "1" +``` + +## Development Workflow + +### Initial Setup + +```bash +# Install Cloudron CLI +npm install -g cloudron + +# Login to your Cloudron instance +cloudron login my.cloudron.example + +# Initialize new app (creates manifest and basic structure) +cloudron init +``` + +### Build-Test-Iterate Cycle + +```bash +# Build Docker image (auto-tags with timestamp) +cloudron build + +# First install +cloudron install --location testapp + +# View logs +cloudron logs -f --app testapp + +# After changes, rebuild and update +cloudron build && cloudron update --app testapp + +# Debug mode (pauses app, makes filesystem writable) +cloudron debug --app testapp + +# Shell into running container +cloudron exec --app testapp + +# Disable debug mode +cloudron debug --disable --app testapp + +# Cleanup +cloudron uninstall --app testapp +``` + +### Validation Checklist + +```text +[ ] Fresh install completes without errors +[ ] App survives restart (cloudron restart --app) +[ ] Health check returns 200 +[ ] File uploads persist across restarts +[ ] Database connections work +[ ] Email sending works (if applicable) +[ ] Memory stays within limit +[ ] Upgrade from previous version works +[ ] Backup/restore cycle works +[ ] Auto-updater is disabled +[ ] Logs stream to stdout/stderr +``` + +## Anti-Patterns to Avoid + +### Writing to /app/code + +```bash +# WRONG - Read-only filesystem +echo "data" > /app/code/cache/file.txt + +# CORRECT +echo "data" > /app/data/cache/file.txt +``` + +### Running as root + +```bash +# WRONG +node /app/code/server.js + +# CORRECT +exec gosu cloudron:cloudron node /app/code/server.js +``` + +### Missing exec + +```bash +# WRONG - Signals won't propagate, container won't stop gracefully +gosu cloudron:cloudron node server.js + +# CORRECT +exec gosu cloudron:cloudron node server.js +``` + +### Non-idempotent start.sh + +```bash +# WRONG - Fails on second run if file exists +cp /app/code/defaults/config.json /app/data/ + +# CORRECT - Safe to repeat +cp -n /app/code/defaults/config.json /app/data/ 2>/dev/null || true +``` + +### Hardcoded URLs + +```javascript +// WRONG +const baseUrl = "https://myapp.example.com"; + +// CORRECT +const baseUrl = process.env.CLOUDRON_APP_ORIGIN; +``` + +### Bundling databases + +```dockerfile +# WRONG - Use Cloudron addons instead +RUN apt-get install -y postgresql redis-server +``` + +### Caching environment variables + +```javascript +// WRONG - Variables can change on restart +const dbHost = process.env.CLOUDRON_MYSQL_HOST; +// ... later in code +connect(dbHost); + +// CORRECT - Read fresh each time +connect(process.env.CLOUDRON_MYSQL_HOST); +``` + +## Upgrade & Migration Handling + +### Version Tracking Pattern + +```bash +CURRENT_VERSION="2.0.0" +VERSION_FILE="/app/data/.app_version" + +if [[ -f "$VERSION_FILE" ]]; then + PREVIOUS_VERSION=$(cat "$VERSION_FILE") + if [[ "$PREVIOUS_VERSION" != "$CURRENT_VERSION" ]]; then + echo "==> Upgrading from $PREVIOUS_VERSION to $CURRENT_VERSION" + # Run version-specific migrations + if [[ "$PREVIOUS_VERSION" < "1.5.0" ]]; then + echo "==> Running 1.5.0 migration" + # migration commands + fi + fi +fi + +echo "$CURRENT_VERSION" > "$VERSION_FILE" +``` + +### Migration Safety + +- Migrations MUST be idempotent +- Use framework migration tracking (Laravel, Django, Rails, etc.) +- For raw SQL: `CREATE TABLE IF NOT EXISTS`, `ADD COLUMN IF NOT EXISTS` + +## Local Development with aidevops + +When developing Cloudron app packages locally: + +```bash +# Create repo in ~/Git/ +mkdir ~/Git/cloudron-myapp +cd ~/Git/cloudron-myapp + +# Initialize with aidevops +aidevops init + +# Initialize Cloudron package +cloudron init + +# Structure +# ~/Git/cloudron-myapp/ +# .agent/ # aidevops config +# CloudronManifest.json # Cloudron manifest +# Dockerfile # Build instructions +# start.sh # Entry point +# logo.png # App icon +``` + +### Recommended .gitignore + +```gitignore +# Cloudron +.cloudron/ + +# aidevops +.agent/loop-state/ +*.local.md + +# Build artifacts +node_modules/ +vendor/ +dist/ +``` + +## Publishing to Cloudron App Store + +Once your app is tested and stable: + +1. **Fork the app store repo**: https://git.cloudron.io/cloudron/appstore +2. **Add your app**: Create directory with manifest and icon +3. **Submit merge request**: Cloudron team reviews +4. **Approval**: App appears in Cloudron App Store + +See: https://docs.cloudron.io/packaging/publishing/ + +## Troubleshooting + +### Common Issues + +**App won't start**: +```bash +cloudron logs --app testapp +cloudron debug --app testapp +# Check start.sh for errors +``` + +**Permission denied errors**: +```bash +# Ensure proper ownership +chown -R cloudron:cloudron /app/data +# Check if writing to read-only path +``` + +**Database connection fails**: +```bash +# Verify addon is declared in manifest +# Check environment variables are being read correctly +cloudron exec --app testapp +env | grep CLOUDRON +``` + +**Health check fails**: +```bash +# Verify healthCheckPath returns 200 +curl -v http://localhost:8000/health +# Check if app is actually listening on httpPort +``` + +**Memory limit exceeded**: +```bash +# Increase memoryLimit in manifest +# Check for memory leaks +# Optimize worker counts +``` + +## Resources + +- **Official Docs**: https://docs.cloudron.io/packaging/ +- **Example Apps**: https://git.cloudron.io/cloudron (all official packages) +- **Forum**: https://forum.cloudron.io/category/96/app-packaging-development +- **Base Image**: https://hub.docker.com/r/cloudron/base +- **CLI Reference**: https://docs.cloudron.io/packaging/cli/ + +### Example Repos by Framework + +- **PHP**: https://git.cloudron.io/explore/projects?tag=php +- **Node.js**: https://git.cloudron.io/explore/projects?tag=node +- **Python**: https://git.cloudron.io/explore/projects?tag=python +- **Go**: https://git.cloudron.io/explore/projects?tag=go +- **Ruby/Rails**: https://git.cloudron.io/explore/projects?tag=rails +- **Java**: https://git.cloudron.io/explore/projects?tag=java