Ein Vergleich von Open-Source No- und Low-Code Plattformen die man selber hosten kann.

Use Cases

Ich habe mir drei Use Cases als Ziel gesetzt:

  1. Wir bauen eine Web-App die Daten aus einer API konsumiert und in funktionsgerechten Tabellen darstellt.
  2. Wir bauen Tabellen aus Excel Dateien und wollen diese schick darstellen.
  3. Wir sind App-Architekt und wollen eine neue Anwendung mit Workflows erstellen.

Die Kandidaten

Mit Hilfe von AI und Websuche finden wir mögliche Standardlösungen.

NameErste Bewertung nach HP StudiumLizenzLetzter CommitContributors
BudibaseViele Features in Community Edition gesperrtProprietär2 Tage120
TooljetViele Features in Community Edition gesperrtAGPL-3.0 license2 Tage654
NocobaseEinige Features in Community Edition gesperrtProprietär5 Tage107
AppsmithEinige Features in Community Edition gesperrtApache-2.02 Tage342
BaserowEinige Features in Community Edition gesperrtProprietär2 Tage52
GrommetAnderer Use Case, fällt raus-
SailAnderer Use Case, fällt raus-
rowyAnderer Use Case, fällt raus-
DirectusAnderer Use Case, fällt raus-

Quick-and-Dirty: Live Demos

Wir probieren alle Tools mal kurz aus.

Dazu erstellen wir entsprechende compose Projekte und fahren diese dann auf unserem Server hoch = Live Demo.

Nocobase

Die Einrichtung von Nocobase ist zunächst sehr simpel.

Nach dem ersten Start werden wir vom Login Screen begrüßt. Der voreingestellte User ist:

Email: admin@nocobase.com Password: admin123.

Zumindest das Passwort sollten wir schnell ändern.

Nun sind wir angemeldet und der nüchterne Startbildschirm sieht so aus:

Die Details zur Konfiguration hier im Unterkapitel Setup, im Kapitel Use Cases gucken wir uns das Tool dann genauer an.

Setup

Den Unterordner “storage” legen wir zuvor an.

compose.yml:

services:
    app:
        image: nocobase/nocobase:latest-full
        restart: unless-stopped
        networks:
            - default
            - caddy
        depends_on:
            - postgres
        environment:
            # The application's secret key, used to generate user tokens, etc.
            # If APP_KEY is changed, old tokens will also become invalid.
            # It can be any random string, and make sure it is not exposed.
            - APP_KEY=
            # Database type, supports postgres, mysql, mariadb
            - DB_DIALECT=postgres
            # Database host, can be replaced with the IP of an existing database server
            - DB_HOST=postgres
            # Database port
            - DB_PORT=5432
            # Database name
            - DB_DATABASE=nocobase
            # Database user
            - DB_USER=nocobase
            # Database password
            - DB_PASSWORD=
            # Timezone
            - TZ=Europe/Berlin
        volumes:
            - ./storage:/app/nocobase/storage
        labels:
            - "traefik.enable=true"
            - "traefik.docker.network=caddy"
            - "traefik.http.routers.noco.rule=Host(`nocobase.handtrixxx.com`)"
            - "traefik.http.routers.noco.entrypoints=websecure"
            - "traefik.http.routers.noco.tls.certresolver=letsencrypt"
            - "traefik.http.routers.noco.service=noco"
            - "traefik.http.routers.noco-http.rule=Host(`nocobase.handtrixxx.com`)"
            - "traefik.http.routers.noco-http.entrypoints=web"
            - "traefik.http.routers.noco-http.middlewares=https-redirect"
            - "traefik.http.routers.noco-http.service=noco"
            - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
            - "traefik.http.middlewares.https-redirect.redirectscheme.permanent=true"
            - "traefik.http.services.noco.loadbalancer.server.port=80"
        #ports:
        #    - "13000:80"
        # init: true

    # If using an existing database server, postgres service can be omitted
    postgres:
        image: postgres:16
        restart: unless-stopped
        command: postgres -c wal_level=logical
        environment:
            POSTGRES_USER: nocobase
            POSTGRES_DB: nocobase
            POSTGRES_PASSWORD: 
        volumes:
            - ./storage/db/postgres:/var/lib/postgresql/data
        networks:
            - default
networks:
    caddy:
        external: true

Appsmith

Auch Appsmith lässt sich sehr einfach einrichten.

Nach dem ersten Start werden wir von einem Assistenten begrüßt, über den wir unseren ersten User einrichten.

Direkt danach werden wir schon nach einer Datenquelle zum verbinden gefragt:

Diesen Schritt überspringen wir zunächst.

Anschließend landen wir direkt in einem App Designer:

Die Details zur Konfiguration hier im Unterkapitel Setup, im Kapitel Use Cases gucken wir uns das Tool dann genauer an.

Setup

Den Unterordner “stacks” legen wir zuvor an.

compose.yml

services:
    appsmith:
        image: index.docker.io/appsmith/appsmith-ce
        container_name: appsmith
        #ports:
        #    - "80:80"
        #    - "443:443"
        volumes:
            - ./stacks:/appsmith-stacks
        networks:
            - caddy
        restart: unless-stopped
        labels:
            - com.getarcaneapp.arcane.updater=false
            - "traefik.enable=true"
            - "traefik.docker.network=caddy"
            - "traefik.http.routers.appsmith.rule=Host(`appsmith.handtrixxx.com`)"
            - "traefik.http.routers.appsmith.entrypoints=websecure"
            - "traefik.http.routers.appsmith.tls.certresolver=letsencrypt"
            - "traefik.http.routers.appsmith.service=appsmith"
            - "traefik.http.routers.appsmith-http.rule=Host(`appsmith.handtrixxx.com`)"
            - "traefik.http.routers.appsmith-http.entrypoints=web"
            - "traefik.http.routers.appsmith-http.middlewares=https-redirect"
            - "traefik.http.routers.appsmith-http.service=appsmith"
            - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
            - "traefik.http.middlewares.https-redirect.redirectscheme.permanent=true"
            - "traefik.http.services.appsmith.loadbalancer.server.port=80"
networks:
    caddy:
        external: true

Tooljet

Tooljet bietet viele Optionen zu Einrichtung.

Nach dem ersten Start werden wir von einem Assistenten begrüßt, über den wir unseren ersten User einrichten. Dieser ist etwas verwirrend, weil er einen zunächst auf die Tooljet Webseite führt, nach erneutem Klick aber wieder direkt zurück zu unserer Installation.

Die UI des System wirkt moderner als z.B. bei Appsmith oder Nocobase, dafür werde ich aber schon im Installationsassistenten nach meinem Unternehmen befragt und noch penetranter ob ich denn nicht eine Trial Version des Cloud Angebots wahrnehmen möchte.

Wenn das alles überwunden ist, dann begrüßt uns ein Designer einer automatisch eingerichteten Beispielapplikation:

ℹ️
Es scheint die Doku ist missverständlich darüber wie man das Image der Community Installation wählt, jedenfalls scheine ich ungewollt doch die Enterprise Variante erwischt zu haben. Hier muss ich nochmal nacharbeiten.

Die Details zur Konfiguration des Setup hier im Unterkapitel Setup, im Kapitel Use Cases gucken wir uns das Tool dann genauer an.

Setup

Im Endeffekt sind eine compose.yml und eine .env Datei anzulegen, ebenso wie ein Unterordner “postgres_data”.

compose.yml

name: Tooljet app

services:
    tooljet:
        tty: true
        stdin_open: true
        container_name: Tooljet-app
        image: tooljet/tooljet:v3.20.126-lts
        platform: linux/amd64
        restart: always
        env_file: .env
        #ports:
        #  - 80:80
        networks:
            - default
            - caddy
        depends_on:
            - postgres
        environment:
            SERVE_CLIENT: "true"
            PORT: "80"
        command: npm run start:prod
        labels:
            - "traefik.enable=true"
            - "traefik.docker.network=caddy"
            - "traefik.http.routers.tooljet.rule=Host(`tooljet.handtrixxx.com`)"
            - "traefik.http.routers.tooljet.entrypoints=websecure"
            - "traefik.http.routers.tooljet.tls.certresolver=letsencrypt"
            - "traefik.http.routers.tooljet.service=tooljet"
            - "traefik.http.routers.tooljet-http.rule=Host(`tooljet.handtrixxx.com`)"
            - "traefik.http.routers.tooljet-http.entrypoints=web"
            - "traefik.http.routers.tooljet-http.middlewares=https-redirect"
            - "traefik.http.routers.tooljet-http.service=tooljet"
            - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
            - "traefik.http.middlewares.https-redirect.redirectscheme.permanent=true"
            - "traefik.http.services.tooljet.loadbalancer.server.port=80"

    postgres:
        container_name: ${PG_HOST}
        image: postgres:16
        restart: always
        volumes:
            - postgres:/var/lib/postgresql/data
        env_file: .env
        environment:
            - POSTGRES_USER=${PG_USER}
            - POSTGRES_PASSWORD=${PG_PASS}
        networks:
            - default

networks:
    default:
    caddy:
        external: true

volumes:
    postgres:
        driver: local
        driver_opts:
            o: bind
            type: none
            device: ${PWD}/postgres_data
    certs:
    logs:
    fallbackcerts:

.env

# Create .env from this example file and replace values for the environment.
# The application expects a separate .env.test for test environment configuration
# Get detailed information about each variable here: https://docs.tooljet.com/docs/setup/env-vars

TOOLJET_HOST=https://tooljet.handtrixxx.com
LOCKBOX_MASTER_KEY=
SECRET_KEY_BASE=

# DATABASE CONFIG
ORM_LOGGING=all
PG_DB=tooljet_production
PG_USER=postgres
PG_HOST=postgresql
PG_PASS=

# The above postgres values is set to its default state. If necessary, kindly modify it according to your personal preference.

# TOOLJET DATABASE
TOOLJET_DB=tooljet_db
TOOLJET_DB_USER=postgres
TOOLJET_DB_HOST=postgresql
TOOLJET_DB_PASS=

PGRST_DB_URI=postgres://postgres:TfNNf9qb5ScO4MZf@postgresql/tooljet_db
PGRST_HOST=localhost:3002
PGRST_JWT_SECRET=
PGRST_SERVER_PORT=3002
PGRST_DB_PRE_CONFIG=postgrest.pre_config


# Checks every 24 hours to see if a new version of ToolJet is available
# (Enabled by default. Set false to disable)
CHECK_FOR_UPDATES=true

# Checks every 24 hours to update app telemetry data to ToolJet hub.
# (Telemetry is enabled by default. Set value to true to disable.)
# DISABLE_TOOLJET_TELEMETRY=false

GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

# EMAIL CONFIGURATION
DEFAULT_FROM_EMAIL=hello@tooljet.io
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_DOMAIN=
SMTP_PORT=

# DISABLE USER SIGNUPS (true or false). only applicable if Multi-Workspace feature is enabled
DISABLE_SIGNUPS=


# OBSERVABILITY
APM_VENDOR=
SENTRY_DNS=
SENTRY_DEBUG=

# FEATURE TOGGLE
COMMENT_FEATURE_ENABLE=
ENABLE_MULTIPLAYER_EDITING=true
ENABLE_MARKETPLACE_FEATURE=true


# SSO (Applicable only for Multi-Workspace)
SSO_GOOGLE_OAUTH2_CLIENT_ID=
SSO_GIT_OAUTH2_CLIENT_ID=
SSO_GIT_OAUTH2_CLIENT_SECRET=
SSO_GIT_OAUTH2_HOST=
SSO_ACCEPTED_DOMAINS=
SSO_DISABLE_SIGNUPS=

#ONBOARDING
ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=

#session expiry in minutes
USER_SESSION_EXPIRY=2880

#TELEMETRY
DEPLOYMENT_PLATFORM=docker

# Workflow scheduling configuration
# Worker Mode
# Set to 'true' to enable job processing
# Set to 'false' or unset for HTTP-only mode (scaled deployments)
WORKER=true

# Workflow Processor Concurrency (optional)
# Number of workflow jobs processed concurrently per worker
# Default: 5
TOOLJET_WORKFLOW_CONCURRENCY=5

# Workflow Execution Timeout (optional)
# Maximum time (in seconds) for a workflow to execute
# Default: 60 seconds
WORKFLOW_TIMEOUT_SECONDS=60

Baserow

Baserow lässt sich ebenfalls sehr einfach einrichten.

Nach dem ersten Start werden wir von einem Assistenten begrüßt, über den wir unseren ersten User einrichten. Außerdem erstellt der Assistent eine Beispielapplikation anhand typischer Szenarien die wie auswählen dürfen.

Das Ganze lässt sich irgendwann überspringen und der Startbildschirm den wir sehen, sieht dann so aus:

Setup

compose.yml

Das Unterverzeichnis “data” sollte man zuvor erstellen.

services:
    baserow:
        container_name: baserow
        image: baserow/baserow:2.1.6
        environment:
            BASEROW_PUBLIC_URL: "https://baserow.handtrixxx.com"
        #ports:
        #  - "80:80"
        #  - "443:443"
        volumes:
            - ./data:/baserow/data
        networks:
            - caddy
        labels:
            - "traefik.enable=true"
            - "traefik.docker.network=caddy"
            - "traefik.http.routers.baserow.rule=Host(`baserow.handtrixxx.com`)"
            - "traefik.http.routers.baserow.entrypoints=websecure"
            - "traefik.http.routers.baserow.tls.certresolver=letsencrypt"
            - "traefik.http.routers.baserow.service=baserow"
            - "traefik.http.routers.baserow-http.rule=Host(`baserow.handtrixxx.com`)"
            - "traefik.http.routers.baserow-http.entrypoints=web"
            - "traefik.http.routers.baserow-http.middlewares=https-redirect"
            - "traefik.http.routers.baserow-http.service=baserow"
            - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
            - "traefik.http.middlewares.https-redirect.redirectscheme.permanent=true"
            - "traefik.http.services.baserow.loadbalancer.server.port=80"
networks:
    caddy:
        external: true

Budibase

Budibase ist etwas komplizierter, aber machbar.

compose.yml

Die Labels sind nur erfrderlich, wenn man Traefik nutzen will.

services:
    app-service:
        restart: unless-stopped
        image: budibase/apps:latest
        container_name: bbapps
        networks:
            - default
        environment:
            SELF_HOSTED: 1
            COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
            WORKER_URL: http://worker-service:4003
            MINIO_URL: http://minio-service:9000
            MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
            MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
            INTERNAL_API_KEY: ${INTERNAL_API_KEY}
            BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
            PORT: 4002
            API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
            JWT_SECRET: ${JWT_SECRET}
            LOG_LEVEL: info
            ENABLE_ANALYTICS: "true"
            REDIS_URL: redis-service:6379
            REDIS_PASSWORD: ${REDIS_PASSWORD}
            REDIS_USERNAME: ${REDIS_USERNAME:-}
            LITELLM_URL: http://litellm-service:4000
            LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY}
            BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
            BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
            PLUGINS_DIR: ${PLUGINS_DIR}
            OFFLINE_MODE: ${OFFLINE_MODE:-}
        depends_on:
            - worker-service
            - redis-service
            - litellm-service
    #    volumes:
    #      - /some/path/to/plugins:/plugins

    worker-service:
        restart: unless-stopped
        image: budibase/worker
        container_name: bbworker
        networks:
            - default
        environment:
            SELF_HOSTED: 1
            PORT: 4003
            CLUSTER_PORT: ${MAIN_PORT}
            API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
            JWT_SECRET: ${JWT_SECRET}
            MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
            MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
            MINIO_URL: http://minio-service:9000
            APPS_URL: http://app-service:4002
            COUCH_DB_USERNAME: ${COUCH_DB_USER}
            COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
            COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
            INTERNAL_API_KEY: ${INTERNAL_API_KEY}
            REDIS_URL: redis-service:6379
            REDIS_PASSWORD: ${REDIS_PASSWORD}
            REDIS_USERNAME: ${REDIS_USERNAME:-}
            LITELLM_URL: http://litellm-service:4000
            LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY}
            OFFLINE_MODE: ${OFFLINE_MODE:-}
        depends_on:
            - redis-service
            - minio-service
            - litellm-service

    minio-service:
        restart: unless-stopped
        image: minio/minio
        networks:
            - default
        volumes:
            - ./minio_data:/data
        environment:
            MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
            MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
            MINIO_BROWSER: "off"
        command: server /data --console-address ":9001"
        healthcheck:
            test: "timeout 5s bash -c ':> /dev/tcp/127.0.0.1/9000' || exit 1"
            interval: 30s
            timeout: 20s
            retries: 3

    proxy-service:
        restart: unless-stopped
        #ports:
        #    - "${MAIN_PORT}:10000"
        container_name: bbproxy
        image: budibase/proxy
        networks:
            - default
            - caddy
        environment:
            - PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10
            - PROXY_RATE_LIMIT_API_PER_SECOND=50
            - APPS_UPSTREAM_URL=http://app-service:4002
            - WORKER_UPSTREAM_URL=http://worker-service:4003
            - MINIO_UPSTREAM_URL=http://minio-service:9000
            - COUCHDB_UPSTREAM_URL=http://couchdb-service:5984
            - RESOLVER=127.0.0.11
        depends_on:
            - minio-service
            - worker-service
            - app-service
            - couchdb-service
        labels:
            - "traefik.enable=true"
            - "traefik.docker.network=caddy"
            - "traefik.http.routers.buddy.rule=Host(`buddy.handtrixxx.com`)"
            - "traefik.http.routers.buddy.entrypoints=websecure"
            - "traefik.http.routers.buddy.tls.certresolver=letsencrypt"
            - "traefik.http.routers.buddy.service=buddy"
            - "traefik.http.routers.buddy-http.rule=Host(`buddy.handtrixxx.com`)"
            - "traefik.http.routers.buddy-http.entrypoints=web"
            - "traefik.http.routers.buddy-http.middlewares=https-redirect"
            - "traefik.http.routers.buddy-http.service=buddy"
            - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
            - "traefik.http.middlewares.https-redirect.redirectscheme.permanent=true"
            - "traefik.http.services.buddy.loadbalancer.server.port=10000"
    couchdb-service:
        restart: unless-stopped
        image: budibase/database:2.1.0
        networks:
            - default
        environment:
            - COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
            - COUCHDB_USER=${COUCH_DB_USER}
            - TARGETBUILD=docker-compose
            - DATA_DIR=/data
        volumes:
            - ./couchdb3_data:/data

    redis-service:
        restart: unless-stopped
        image: redis
        networks:
            - default
        command: redis-server --requirepass "${REDIS_PASSWORD}"
        volumes:
            - ./redis_data:/data

    litellm-service:
        restart: unless-stopped
        image: docker.litellm.ai/berriai/litellm:1.80.15-stable.1
        #ports:
        #    - "${LITELLM_PORT:-4000}:4000"
        volumes:
            - ./litellm_config.yaml:/app/config.yaml
        environment:
            DATABASE_URL: "postgresql://${LITELLM_DB_USER}:${LITELLM_DB_PASSWORD}@litellm-db:5432/${LITELLM_DB_NAME}"
            STORE_MODEL_IN_DB: "True"
            LITELLM_REASONING_AUTO_SUMMARY: "true"
            LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY}
            LITELLM_SALT_KEY: ${LITELLM_SALT_KEY}
        command: ["--config", "/app/config.yaml"]
        depends_on:
            - litellm-db
        networks:
            - default
        healthcheck:
            test:
                [
                    "CMD-SHELL",
                    "wget --no-verbose --tries=1 http://localhost:4000/health/liveliness || exit 1",
                ]
            interval: 30s
            timeout: 10s
            retries: 3
            start_period: 40s

    litellm-db:
        restart: unless-stopped
        image: postgres:16
        environment:
            POSTGRES_DB: ${LITELLM_DB_NAME}
            POSTGRES_USER: ${LITELLM_DB_USER}
            POSTGRES_PASSWORD: ${LITELLM_DB_PASSWORD}
        volumes:
            - ./litellm_data:/var/lib/postgresql/data
        networks:
            - default
        healthcheck:
            test:
                [
                    "CMD-SHELL",
                    "pg_isready -d ${LITELLM_DB_NAME} -U ${LITELLM_DB_USER}",
                ]
            interval: 1s
            timeout: 5s
            retries: 10
networks:
    caddy:
        external: true

.env

auch wenn der “REDIS_USERNAME” sich setzen lässt, fährt die Anwendung dann nicht ohne weiteres Zutun hoch. Deshalb diesen zunächst als Kommentar stehen lassen.

# Use the main port in the builder for your self hosting URL, e.g. localhost:10000
MAIN_PORT=10000

# This section contains all secrets pertaining to the system
# These should be updated
# #openssl rand -hex 32

API_ENCRYPTION_KEY=
JWT_SECRET=
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
COUCH_DB_PASSWORD=
COUCH_DB_USER=
REDIS_PASSWORD=
#REDIS_USERNAME=budiredis
INTERNAL_API_KEY=
LITELLM_MASTER_KEY=
LITELLM_SALT_KEY=
# This section contains variables that do not need to be altered under normal circumstances
APP_PORT=4002
WORKER_PORT=4003
MINIO_PORT=4004
COUCH_DB_PORT=4005
COUCH_DB_SQS_PORT=4006
REDIS_PORT=6379
BUDIBASE_ENVIRONMENT=PRODUCTION
SQL_MAX_ROWS=
LITELLM_PORT=4000
LITELLM_DB_NAME=litellm
LITELLM_DB_USER=llmproxy
LITELLM_DB_PASSWORD=

# An admin user can be automatically created initially if these are set
BB_ADMIN_USER_EMAIL=
BB_ADMIN_USER_PASSWORD=

# A path that is watched for plugin bundles. Any bundles found are imported automatically/
PLUGINS_DIR=
ROLLING_LOG_MAX_SIZE=

litellm_config.yaml

Ohne diese Datei fährt Budibase nicht hoch, der Inhalt muss aber nur semantisch korrekt sein. Also lassen sich die folgenden Beispielwerte einfach 1:1 übernehmen.

model_list:
    - model_name: azure-gpt-4o
      litellm_params:
          model: azure/<your-azure-model-deployment>
          api_base: os.environ/AZURE_API_BASE # runs os.getenv("AZURE_API_BASE")
          api_key: os.environ/AZURE_API_KEY # runs os.getenv("AZURE_API_KEY")
          api_version: "2025-01-01-preview"