Skip to main content
Version: 0.12.1

Installation

Quack on Demand ships as a Docker image and as a single uber-jar driven by the qod CLI (pip install qod, or no install at all with uvx). The fastest way to evaluate it is Demo mode via uvx qod@latest serve --demo. For a durable deployment, two paths are supported: Docker, and the native jar via qod start. Both run on Linux, macOS, and Windows (Windows support is experimental; see Run on Windows below). GitHub Releases hosts the raw jar artifacts, but you should rarely need to download one by hand - every path below fetches what it needs.

Docker​

The published image is starlakeai/quack-on-demand. Use scripts/docker/run-docker.sh to pull and run it against an external Postgres:

PG_HOST=db.internal PG_PASSWORD=*** ./scripts/docker/run-docker.sh

The script requires PG_HOST and PG_PASSWORD. Key options:

VariableDefaultDescription
QOD_VERSIONlatestImage tag to pull
PG_PORT5432Postgres port
PG_USERpostgresPostgres user
MANAGER_PORT20900Host port for REST + admin UI
EDGE_PORT31338Host port for FlightSQL edge
QUACK_PORT9494Host port for the native Quack front door (DuckDB ATTACH)
TLSfalseEnable FlightSQL TLS inside the container
ADMIN_PASSWORDadminAdmin login password, used on the first boot only (QOD_ADMIN_PASSWORD also works and wins). Rotate it afterwards with qod auth change-password.
AUTHtrueDatabase password auth for FlightSQL and the admin login. Only true is accepted: with false the manager would have no FlightSQL authenticator and refuse to boot, so the script refuses first.
BUILD0Set to 1 to build the image from the local Dockerfile instead of pulling

To pin a version or use the latest snapshot:

QOD_VERSION=0.3.2 PG_HOST=... PG_PASSWORD=*** ./scripts/docker/run-docker.sh
QOD_VERSION=latest-snapshot PG_HOST=... PG_PASSWORD=*** ./scripts/docker/run-docker.sh

Note: do not mix Docker and native-jar runs against the same catalog DB. DuckLake records the absolute data path in Postgres metadata. Inside the container that path is /app/ducklake/<db>; natively it is <host-cwd>/ducklake/<db>. Use a different PG_DBNAME (or QOD_PG_DBNAME) per mode, or wipe the data directory between switches.

Native jar (qod start)​

qod start runs a real manager on your machine against your own Postgres, with no repository checkout: it downloads the release jar from GitHub Releases (verified against the published sha256), self-installs the DuckDB CLI + libduckdb at the pinned ABI, and launches QoD with the right settings. Every node runs qod-node, QoD's own executable on the official DuckDB library: the jar bundles it for Linux and macOS (x86_64, aarch64) and installs it beside the libduckdb it loaded, and QOD_NODE_BIN points the manager at another build. The manager refuses to boot without one, or on one built for another node protocol or DuckDB release.

Prerequisites​

  • Postgres 16 or later, reachable. Installers for all platforms are at postgresql.org/download (Windows users can also grab the EDB installer directly). The control plane stores all state in a dedicated database (default name qod) on localhost:5432; point QOD_PG_HOST / QOD_PG_PORT / QOD_PG_USER / QOD_PG_PASSWORD at yours - or run uvx qod@latest setup once to store them (guided prompts; a real shell export still wins; see qod setup). When psql is on PATH, qod start creates the control-plane database up front (idempotent); without it the manager creates what it needs on first connection.

    If you do not have a local Postgres instance, the quickest path is:

    docker run -d --name qod-pg \
    -e POSTGRES_PASSWORD=azizam \
    -p 5432:5432 \
    postgres:16-alpine
  • Java 21+ is found, not required. qod start uses JAVA_HOME or java on PATH when a Java 21+ runtime is present; otherwise it downloads a cached Temurin 21 JRE (about 50 MB, one time, no prompt). JAVA_BIN forces a specific binary; JAVA_OPTS adds Java options (e.g. -Xmx2g).

  • DuckDB is self-installed (CLI + libduckdb at the ABI libquackwire links against) into the user cache dir - a system duckdb at the wrong ABI would crash the first node spawn, so it is never used. DUCKDB_VERSION / DUCKDB_CACHE_DIR override the pin and location; air-gapped operators can pre-populate $DUCKDB_CACHE_DIR/$VERSION/{bin,lib} and no network fetch happens.

Where downloads are cached​

Everything qod start fetches lands in the per-user cache directory, keyed by version, and is reused on every later run:

WhatLocation (macOS)Location (Linux)Override
Manager jars~/Library/Caches/qod/jars~/.cache/qod/jarsJAR_CACHE_DIR
Temurin JRE (only when no system Java 21+)~/Library/Caches/qod/jre~/.cache/qod/jre-
DuckDB CLI + libduckdb~/Library/Caches/qod/{bin,lib}~/.cache/qod/{bin,lib}DUCKDB_CACHE_DIR

On Windows the root is %LOCALAPPDATA%\qod\Cache. Jars are named quack-on-demand-assembly-<version>.jar, so several releases coexist; deleting the directory only costs a re-download. Durable state (DuckLake data, TLS certs) is deliberately kept elsewhere, under ~/Library/Application Support/qod (macOS) or ~/.local/share/qod (Linux) - wiping the cache is safe, wiping that is not.

Running through uvx qod@latest ... adds one more layer: uv caches the qod Python package itself under ~/.cache/uv (uv cache dir). That holds only the small CLI, never the manager jar.

Offline runs. When GitHub cannot be reached, qod start does not fail: it starts the newest manager jar already in the cache (preferring the release this CLI build pins, when that one is cached) and says which version it picked. Only an explicit --version X.Y.Z that is neither cached nor downloadable is a hard error, since substituting a different manager behind your back would be worse; the message then lists what is cached so you can pick one. With an empty cache and no network there is nothing to run, so that too is an error.

Run from Linux/MacOS​

uv tool install qod            # or: pip install qod

qod start # latest release
QOD_VERSION=0.3.8 qod start # pinned release
qod start --jar path/to/quack-on-demand-assembly-X.Y.Z.jar # a jar you built

When your Postgres is not on localhost:5432 with the default credentials, the commands above need QOD_PG_* in the environment - and because uvx runs the CLI without any installation, there is no obvious place to keep those exports: every new terminal means re-exporting them or prefixing each command (QOD_PG_HOST=db QOD_PG_PASSWORD=... uvx qod@latest start). That is what qod setup is for - run it once and answer the prompts:

uvx qod@latest setup                  # Postgres coordinates, admin username, auth/TLS
uvx qod@latest start # now works bare, in any terminal, from anywhere

It stores the answers in the CLI config file (mode 0600, path printed on completion, --show to inspect with secrets redacted). A real shell export still overrides any stored value on a given run, and the self-contained qod serve --demo (and its deprecated alias qod start --demo) deliberately ignores the stored config.

On first run against a freshly-provisioned Postgres, Liquibase applies the control-plane schema. The admin UI is at http://localhost:20900/ui/; log in as admin with the password the first qod start asked for (it is set on the first boot only; see Configuration model below).

The FlightSQL edge listens on localhost:31338 with TLS on by default, and the native Quack front door (for DuckDB clients) on localhost:9494 with TLS off by default. Durable state (the self-signed certs, and the default DuckLake data path unless QOD_DUCKLAKE_DATA_PATH is set) lives under the platform user data dir (~/.local/share/qod on Linux, ~/Library/Application Support/qod on macOS).

To stop the manager and its quack nodes, either press Ctrl-C in the terminal running qod start (the CLI supervises QoD and runs the same graceful teardown) or, from anywhere:

qod stop

Both paths SIGTERM the manager and every node, wait up to FORCE_AFTER seconds (default 10), then escalate to SIGKILL, so no orphaned node processes are left holding ports.

Seeding the demo dataset for a quick smoke test​

Pass LOAD_TPCH=1, LOAD_TPCDS=1, and/or LOAD_SSB=1 to seed each benchmark independently; the loaders run in the background using the self-installed DuckDB - no extra steps.

LOAD_TPCH=1 qod start                          # TPC-H only (acme, ~10 s)
LOAD_TPCDS=1 qod start # TPC-DS only (globex, ~30 s)
LOAD_SSB=1 qod start # SSB star schema only (acme, ~15 s)
LOAD_TPCH=1 LOAD_TPCDS=1 qod start # TPC-H + TPC-DS
LOAD_TPC=1 qod start # legacy shortcut for all three

Each flag seeds the matching demo dataset: acme loaded with TPC-H (8 tables in schema tpch1), globex loaded with TPC-DS (24 tables in schema tpcds1), and/or the SSB star schema (5 tables in schema ssb1, derived from TPC-H dbgen and living next to tpch1 in the acme_tpch tenant-db). The bundled manifest bootstrap-demo.yaml (imported from the jar's classpath) declares the tenants, roles, groups, and users; see the Access control model for the full ACL matrix.

For a completely fresh start (drops the control-plane DB and wipes local state directories before booting; requires psql for the DB drops):

NUKE=1 LOAD_TPCH=1 LOAD_TPCDS=1 qod start

To front a single DuckDB instance instead of the multi-tenant demo, DEMO=minimal imports bootstrap-demo-minimal.yaml: one tenant (acme), one pool (bi), one dual node serving both reads and writes, and the analyst RLS/CLS demo. DEMO=full is the default and covers the multi-tenant, multi-pool, and federation demos. The profile is only consulted when a LOAD_* flag is set and QOD_BOOTSTRAP_YAML is unset; bootstrap only imports into a fresh control plane, so switch profiles with NUKE=1. DEMO=minimal with LOAD_TPCDS warns and skips the TPC-DS loader (no globex tenant in this profile).

NUKE=1 DEMO=minimal LOAD_TPCH=1 qod start

Run on Windows​

Experimental

Windows support is experimental. The qod launcher needs release 0.3.8 or later (older releases predate the demo/start subcommand support and are refused).

The manager runs natively on Windows - no WSL, no Docker. pip install qod installs the same qod.exe entry point, and qod serve --demo / qod serve / qod start work as on Linux/macOS: Java 21 is auto-provisioned when missing, DuckDB (CLI + duckdb.dll) is self-installed, and Quack nodes are spawned through the bundled PowerShell mirror of the node-spawn script. The node executable, qod-node-windows-<arch>.exe, is too large to bundle in the jar: qod start and qod serve download the one of the manager's release (sha256-checked, cached under qod-node/<release>/) and set QOD_NODE_BIN.

pip install qod
$env:QOD_PG_HOST = 'localhost'
$env:QOD_PG_PASSWORD = 'hunter2'
$env:QOD_ADMIN_PASSWORD = 'change-me'
qod start

Current Windows gaps: qod stop is not implemented (kill the java process with taskkill /PID <pid> /T), and the LOAD_* benchmark seeders are bash-only through qod start (it warns and skips them; run the PowerShell loaders scripts\load-*-dbgen.ps1 from a checkout instead).

Client path. The manager talks to its nodes through the native client quackwire.dll only (there is no embedded JDBC fallback). The jar bundles it for windows-x86_64 and windows-aarch64 and loads the one for the JVM's own os.arch, so a Windows on ARM host (e.g. Parallels on Apple Silicon) needs an ARM64 Java runtime; a platform with no bundled binary, or a native that cannot load, refuses to boot with a message naming the expected file.

Building the Windows native (quackwire.dll), optional. Requires MSVC (Visual Studio Build Tools) + CMake + sbt. Assemble a DUCKDB_HOME with duckdb.lib plus the full duckdb/ include tree (see .github/workflows/quackwire.yml, step Install libduckdb v${{ env.DUCKDB_VERSION }} (Windows)), then:

$env:DUCKDB_HOME = 'C:\path\to\duckdb-home'
cmake -S native\quackwire -B native\quackwire\build -DCMAKE_BUILD_TYPE=Release -DDUCKDB_HOME="$env:DUCKDB_HOME"
cmake --build native\quackwire\build --config Release
New-Item -ItemType Directory -Force libquackwire\binaries\windows-x86_64 | Out-Null
Copy-Item native\quackwire\build\Release\quackwire.dll libquackwire\binaries\windows-x86_64\
sbt assembly # bundles native/windows-x86_64/quackwire.dll
qod start --jar distrib\quack-on-demand-assembly-<version>.jar

Build from source​

sbt assembly
qod start --jar distrib/quack-on-demand-assembly-*.jar

To run the jar manually instead:

java -jar distrib/quack-on-demand-assembly-*.jar

The assembly output lands in distrib/quack-on-demand-assembly-<version>.jar. The jar carries the Add-Opens manifest attribute covering java.base/java.nio and java.base/sun.nio.ch, which Arrow Flight's unsafe allocator needs on Java 17+. No extra --add-opens flags are needed on the command line.

Note: sbt compile and sbt assembly also run npm ci and npm run build inside the ui/ directory as a resource generator, so the React admin UI is bundled automatically. There is no separate UI build step.

The CLI beyond launching​

The same qod command drives the whole REST surface and runs SQL against the FlightSQL edge:

qod login --url http://localhost:20900 --username admin
qod tenant list

See the CLI section for a tour.

Configuration model​

Every scalar in application.conf accepts a matching QOD_* environment variable override (FlightSQL edge keys use the PROXY_* prefix). The bundled application.conf is baked into the jar at build time, so you should use environment variables rather than editing it directly.

Defaults you must change before production:

SettingEnv varInsecure default
Admin passwordQOD_ADMIN_PASSWORDnone for the jar (no admin is seeded until you set it on the first boot); admin under the Docker launchers
Postgres passwordQOD_PG_PASSWORDazizam

There is no static REST API key: every REST, MCP and SCIM caller is a qod login session or a personal access token (qod auth pat create), and a manager with a non-empty QOD_API_KEY refuses to boot. The session signing secret needs no pinning either: when QOD_SESSION_JWT_SECRET is unset, the first boot generates one and stores it in the control-plane database, so sessions survive restarts and every HA replica shares it (boot logs session secret: ... naming the source, never the value). Pin it from an external secret store when control-plane backups or replicas are less trusted than the manager, since a read of that database otherwise yields a key that forges sessions.

Other commonly used variables:

SettingEnv varDefault
Manager REST portQOD_ON_DEMAND_PORT20900
FlightSQL edge portPROXY_PORT31338
FlightSQL TLS on/offPROXY_TLS_ENABLEDtrue
Native Quack front door portQOD_QUACK_PORT9494
Native Quack front door TLS on/offQOD_QUACK_TLS_ENABLEDfalse
Postgres hostQOD_PG_HOSTlocalhost
Postgres userQOD_PG_USERpostgres
Control-plane databaseQOD_PG_DBNAMEqod
Admin usernamesQOD_ADMIN_USERNAME[email protected],admin
Folder under which tenants authorize local file foldersQOD_TENANT_FILES_ROOTunset (no local folder can be authorized)
S3 endpoints users' own keys may targetQOD_USER_S3_ALLOWED_ENDPOINTSunset (AWS only)

Per-statement authorization is always on: every node judges each tenant statement against the user's grants (or the tenant's OPA policy), and the former QOD_ACL_ENABLED, QOD_ACL_FILTERED_METADATA, QOD_ACL_DIALECT and QOD_NODE_LOCKDOWN switches make the manager refuse to boot. See Upgrading to 0.11.0.

Extension settings are fixed when a node starts: allow_community_extensions, allow_unsigned_extensions and autoinstall_known_extensions are false on every node, before its database opens, so no init SQL or session can turn them on. Set QOD_NODE_ALLOW_COMMUNITY_EXTENSIONS, QOD_NODE_ALLOW_UNSIGNED_EXTENSIONS or QOD_NODE_AUTOINSTALL_KNOWN_EXTENSIONS to true on the manager to allow one (it applies at each node's next start), and install an extension a statement needs at node start with INSTALL <name>; in a database or pool initSql rather than relying on DuckDB downloading it.

For the full list of configuration keys, see the Configuration reference.