> ## Documentation Index
> Fetch the complete documentation index at: https://docs.controlplane.com/llms.txt
> Use this file to discover all available pages before exploring further.

# DuckDB

> Deploy DuckDB on Control Plane as a scheduled batch SQL job. One cron workload runs your script against object storage and attached databases, then exits — no server endpoint. Needs an existing GVC; object-storage, script and database secrets are created first only when you use them.

## Overview

DuckDB is an in-process analytical SQL engine that reads and writes Parquet, CSV and JSON directly, queries object storage over the S3 API, and can attach live PostgreSQL, MySQL and SQLite databases. This template runs a SQL script of yours on a cron schedule and exits: it is a batch job runner, billed only for the minutes a run is executing, with nothing running between runs.

<Warning>
  **This is a scheduled job runner, not a query service.** The workload is a cron job: it starts, runs your script, and terminates. It binds no port, has no endpoint, and nothing is listening after install — that is correct behavior, not a broken deployment. If you want an always-on SQL endpoint that BI tools and JDBC clients connect to, install [trino](/template-catalog/templates/trino) instead.
</Warning>

<Note>
  This template deploys into an existing [GVC](/reference/gvc) that you already have; it does not create or manage one. A cron workload runs in every location the GVC has, so a GVC with several locations runs this job once per location on each schedule.
</Note>

### What Gets Created

| Resource | Name | Purpose | Optional |
| - | - | - | - |
| Workload | `RELEASE_NAME-duckdb` | Cron job running `duckdb/duckdb:1.5.5` once per schedule; no ports, no inbound traffic | — |
| Secret | `RELEASE_NAME-duckdb-preamble` | Generated `SET` statements (memory, threads, scratch paths, object-store secret) run before your script | — |
| Secret | `RELEASE_NAME-duckdb-script` | Your SQL from `sql.inline`, mounted at `/etc/duckdb/job.sql` | Not created when `sql.secretName` is set |
| Identity | `RELEASE_NAME-duckdb-identity` | Workload identity; carries the AWS cloud-account binding when `objectStore.type: aws` | — |
| Policy | `RELEASE_NAME-duckdb-policy` | Grants the identity `reveal` on exactly the secrets this release mounts | — |

No volume set is created. Each run starts from an empty in-memory database, and extensions and query spill live in container-local scratch under `/tmp`.

## Prerequisites

**None for a default install.** The shipped `sql.inline` is a self-test that needs no credentials, no bucket and no cloud account. Every run does download its extensions from `extensions.duckdb.org:443`; the template allows all outbound traffic, so this works unless your organization restricts egress.

A real job has to write its results somewhere durable — object storage or an attached database — and the features that make that possible each need something created **before** install:

<Steps>
  <Step title="Object storage (when objectStore.type is aws or s3-compatible)">
    Anything other than `none` registers a DuckDB S3 secret so your SQL can read and write `s3://` paths.

    <Tabs>
      <Tab title="AWS S3 (keyless)">
        The job receives short-lived credentials from its own workload identity — no access keys are created, stored or injected anywhere.

        1. Create your S3 bucket and note its region.
        2. If you do not have one, [create a cloud account](/guides/create-cloud-account) for your AWS account.
        3. Create an IAM policy scoped to that bucket (replace `YOUR_BUCKET_NAME`). Drop `s3:PutObject` and `s3:DeleteObject` if the job only reads.

        ```json theme={null}
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": [
                "s3:ListBucket",
                "s3:GetBucketLocation"
              ],
              "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME"
            },
            {
              "Effect": "Allow",
              "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
              ],
              "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
            }
          ]
        }
        ```

        Set `objectStore.type: aws`, `objectStore.aws.region` to the bucket's region, `objectStore.aws.cloudAccountName` to the cloud account's name, and `objectStore.aws.policyName` to the IAM policy's name. The chart refuses to render if any of the three is blank.
      </Tab>

      <Tab title="S3-compatible">
        Covers [SeaweedFS](/template-catalog/templates/seaweedfs), [MinIO](/template-catalog/templates/minio), Cloudflare R2, Tigris, and Google Cloud Storage through its S3 interoperability endpoint. These servers cannot federate with a cloud account, so they use static keys held in a [dictionary secret](/guides/create-secret/dictionary).

        1. Create the bucket on your server, plus an access key and secret key pair with read and write access to it. For Google Cloud Storage these are HMAC keys, created under Cloud Storage → Settings → Interoperability.
        2. Create the credentials secret with exactly these two keys:

        ```bash theme={null}
        cpln secret create-dictionary --name SECRET_NAME \
          --entry access-key-id=YOUR_ACCESS_KEY \
          --entry secret-access-key=YOUR_SECRET_KEY
        ```

        Set `objectStore.type: s3-compatible`, `objectStore.s3Compatible.credentialsSecretName` to the name you used, and `objectStore.s3Compatible.endpoint` to the server's `host:port` with **no** `http://` or `https://` prefix — the chart rejects a scheme and tells you to set `useSsl` instead.

        | Provider | `endpoint` | `urlStyle` | `useSsl` |
        | - | - | - | - |
        | SeaweedFS in the same GVC | `RELEASE_NAME-seaweedfs.GVC_NAME.cpln.local:8333` | `path` | `false` |
        | MinIO in the same GVC | `RELEASE_NAME-minio.GVC_NAME.cpln.local:9000` | `path` | `false` |
        | Google Cloud Storage | `storage.googleapis.com` | `path` | `true` |
        | Cloudflare R2 | `ACCOUNT_ID.r2.cloudflarestorage.com` | `path` | `true` |
        | AWS S3 with static keys | `s3.us-east-1.amazonaws.com` | `vhost` | `true` |

        The `RELEASE_NAME` in the SeaweedFS and MinIO rows is the release name of *that* template's install, not this one.
      </Tab>
    </Tabs>
  </Step>

  <Step title="A script secret (when using sql.secretName instead of sql.inline)">
    Put your SQL in an [opaque secret](/guides/create-secret/opaque) with `--encoding plain`; its payload is mounted as the job script:

    ```bash theme={null}
    cat job.sql | cpln secret create-opaque --name SECRET_NAME --encoding plain -f -
    ```

    Set `sql.secretName` to this name. The template's own script secret is then not created at all.
  </Step>

  <Step title="Credential secrets for attached databases (when using secretEnv)">
    Every secret named in `secretEnv[].secretName` must exist before install. For a [postgres](/template-catalog/templates/postgres) or [mysql](/template-catalog/templates/mysql) template in the same GVC, that is the credentials secret you created for it — point `secretKey` at its `password` entry.
  </Step>
</Steps>

<Warning>
  **Create every referenced secret before installing, or the job wedges silently.** A `sql.secretName`, `secretEnv[].secretName` or `credentialsSecretName` pointing at a secret that does not exist installs "successfully", and the container then never starts — so `cpln logs` returns **zero lines**. The reason is named in `status.versions[].message`:

  ```bash theme={null}
  cpln workload get-deployments RELEASE_NAME-duckdb --gvc GVC_NAME -o yaml
  ```

  Use `get-deployments` — plain `cpln workload get` has no `versions` key. Once the secret exists the workload recovers on its own after several minutes, or run `cpln workload force-redeployment RELEASE_NAME-duckdb --gvc GVC_NAME` to skip the wait.
</Warning>

## Installation

A default install needs no `--set` values — it runs the built-in self-test script on the default schedule:

```bash theme={null}
cpln helm install RELEASE_NAME oci://ghcr.io/controlplane-com/templates/duckdb \
  --version 1.0.0 --gvc GVC_NAME
```

Or follow the instructions for your preferred method:

<CardGroup cols={2}>
  <Card title="UI" href="/template-catalog/install-manage/ui" icon="laptop">
    Browse, install, and manage templates visually
  </Card>

  <Card title="CLI" href="/template-catalog/install-manage/cli" icon="terminal">
    Manage templates from your terminal
  </Card>

  <Card title="Terraform" href="/template-catalog/install-manage/terraform" icon={<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill-rule="evenodd"><path d="M77.941 44.5v36.836L46.324 62.918V26.082zm0 0" fill="#5c4ee5"/><path d="M81.41 81.336l31.633-18.418V26.082L81.41 44.5zm0 0" fill="#4040b2"/><path d="M11.242 42.36L42.86 60.776V23.941L11.242 5.523zm0 0M77.941 85.375L46.324 66.957v36.82l31.617 18.418zm0 0" fill="#5c4ee5"/></g></svg>}>
    Declare templates in your Terraform configurations
  </Card>

  <Card
    title="Pulumi"
    href="/template-catalog/install-manage/pulumi"
    icon={<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Pulumi-Icon--Streamline-Svg-Logos" height="24" width="24">
    <desc>
        Pulumi Icon Streamline Icon: https://streamlinehq.com
    </desc>
    <path fill="#f26e7e" d="M4.683025 13.3318c0.869125 -0.5018 0.870575 -2.1264 0.003225 -3.62865s-2.27504 -2.313275 -3.1441725 -1.811475C0.672945 8.3935 0.6715 10.0181 1.53885 11.52035c0.86735 1.502275 2.27505 2.313275 3.144175 1.81145Zm0.0052 3.2167c0.86735 1.502275 0.865925 3.126875 -0.003225 3.628675 -0.86915 0.5018 -2.2768275 -0.309225 -3.144175 -1.81145 -0.8673525 -1.50225 -0.8659075 -3.126875 0.003225 -3.628675 0.8691325 -0.5018 2.276825 0.309225 3.144175 1.81145Zm5.922875 3.4243c0.86735 1.50225 0.8659 3.126775 -0.003225 3.62875 -0.869125 0.501775 -2.27685 -0.309325 -3.1442 -1.81155 -0.867325 -1.50225 -0.865875 -3.12685 0.00325 -3.628675 0.869125 -0.5018 2.276825 0.309225 3.144175 1.811475Zm-0.001925 -6.845275c0.86735 1.50225 0.8659 3.12685 -0.003225 3.628675 -0.869125 0.5018 -2.276825 -0.309225 -3.144175 -1.811475 -0.86735 -1.50225 -0.8659 -3.12685 0.003225 -3.62865 0.869125 -0.501825 2.276825 0.3092 3.144175 1.81145Z" stroke-width="0.25"></path>
    <path fill="#8a3391" d="M22.45775 11.524125c0.86725 -1.502225 0.865925 -3.12685 -0.003225 -3.62865 -0.869125 -0.501825 -2.276825 0.3092 -3.144175 1.811475 -0.86735 1.50225 -0.8659 3.126825 0.003225 3.62865 0.869125 0.501825 2.276825 -0.3092 3.144175 -1.811475Zm0.000175 3.2151c0.869075 0.5018 0.870625 2.1264 0.003225 3.62865 -0.86735 1.50225 -2.27505 2.313275 -3.144175 1.81145 -0.869125 -0.5018 -0.870575 -2.126425 -0.003225 -3.62865 0.86735 -1.50225 2.27505 -2.313275 3.144175 -1.81145ZM16.536225 18.157875c0.86915 0.501825 0.8706 2.126425 0.00325 3.628675 -0.86735 1.502125 -2.275075 2.313225 -3.1442 1.81145 -0.869125 -0.50175 -0.870575 -2.126425 -0.003225 -3.62865 0.867375 -1.502275 2.27505 -2.3133 3.144175 -1.811475Zm-0.003325 -6.843775c0.869125 0.5018 0.870575 2.126425 0.003225 3.628675s-2.27505 2.313275 -3.1442 1.811475c-0.869125 -0.501825 -0.870575 -2.126425 -0.003225 -3.628675 0.86735 -1.502275 2.27505 -2.313275 3.1442 -1.811475Z" stroke-width="0.25"></path>
    <path fill="#f7bf2a" d="M15.138225 2.06721c0 1.003615 -1.40625 1.817215 -3.14095 1.817215 -1.7347 0 -3.14095 -0.8136 -3.14095 -1.817215C8.856325 1.06359 10.262575 0.25 11.997275 0.25c1.7347 0 3.14095 0.81359 3.14095 1.81721ZM9.2166 5.482375c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172s1.40625 -1.817225 3.14095 -1.817225c1.7347 0 3.14095 0.8136 3.14095 1.817225Zm8.71005 1.8172c1.7347 0 3.14095 -0.813575 3.14095 -1.8172s-1.40625 -1.817225 -3.14095 -1.817225c-1.7347 0 -3.14095 0.8136 -3.14095 1.817225s1.40625 1.8172 3.14095 1.8172Zm-2.788425 1.605625c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172 0 -1.0036 1.40625 -1.8172 3.14095 -1.8172 1.7347 0 3.14095 0.8136 3.14095 1.8172Z" stroke-width="0.25"></path>
    </svg>}
  >
    Declare templates in your Pulumi programs
  </Card>
</CardGroup>

## Configuration

### Image and Schedule

```yaml theme={null}
image: duckdb/duckdb:1.5.5

schedule: "0 2 * * *" # cron expression in UTC — always quote it (e.g. "*/15 * * * *")
suspend: false # true = never run automatically; start it with `cpln workload cron start`
activeDeadlineSeconds: 3600 # a run still going is terminated within ~2 min of this
```

The schedule must be exactly five fields; the chart rejects anything else at render time. Runs never overlap and a failed run is not retried — the next scheduled run simply starts as normal.

<Warning>
  `activeDeadlineSeconds` is not a hard cut. The deadline is detected on time, but the container is terminated within roughly two minutes of it and keeps consuming its full CPU and memory allocation for that window. A run killed this way can still print the success marker on its way out — see [Confirming a Run Succeeded](#confirming-a-run-succeeded).
</Warning>

### SQL Script

```yaml theme={null}
sql:
  inline: |
    -- Default self-test. Replace with your own transform.
    SELECT 'duckdb-template-ok' AS status, version() AS duckdb_version, now() AS run_at;
  secretName: "" # opaque secret (encoding plain, payload = SQL) used INSTEAD of inline, e.g. my-duckdb-script
```

`sql.secretName` wins when both are set, and is the right choice when the script is too large for a values file or you want to change it without a Helm upgrade. The chart refuses to render when both are empty.

Before your script, the job runs a template-generated preamble containing `.bail on` and the derived `memory_limit`, `threads`, `temp_directory` and `extension_directory` settings, then the object-store secret when one is configured. Because the preamble runs first, a `SET` in your own script always wins. `.bail on` stops execution at the first SQL error.

### Credentials in SQL

```yaml theme={null}
secretEnv: []
# secretEnv:
#   - name: PGPASSWORD
#     secretName: my-postgres-credentials # must exist BEFORE install
#     secretKey: password # omit for an opaque secret (uses its payload)
```

Each entry becomes a container environment variable sourced from a Control Plane secret, readable in your SQL as `getenv('NAME')`. Only a `cpln://secret/...` reference appears in the rendered workload; the value itself never lands in your values file. Names must be `UPPER_SNAKE_CASE` and unique, and `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN` and `AWS_DEFAULT_REGION` are rejected because `objectStore` owns them.

To attach a database, name the entry after the driver's own password variable and leave the password out of the connection string — the driver reads it from the environment. `ATTACH` takes a string literal, so `getenv()` cannot be concatenated into it (see [Troubleshooting](#troubleshooting)).

| Attach type | Name the `secretEnv` entry |
| - | - |
| `postgres` | `PGPASSWORD` |
| `mysql` | `MYSQL_PWD` |

```sql theme={null}
ATTACH 'dbname=postgres user=postgres host=RELEASE_NAME-postgres.GVC_NAME.cpln.local' AS pg (TYPE postgres, READ_ONLY);
COPY (SELECT * FROM pg.public.events) TO 's3://YOUR_BUCKET_NAME/events.parquet' (FORMAT parquet);
```

Here `RELEASE_NAME-postgres` is the workload of a [postgres](/template-catalog/templates/postgres) template installed in the same GVC. `getenv('NAME')` still works anywhere an ordinary expression is allowed — a `WHERE` clause, a computed column, a `COPY` destination built with `||`.

### Resources and Tuning

```yaml theme={null}
resources:
  minCpu: 500m
  maxCpu: 2000m # also sets DuckDB threads: one per whole core, minimum 1
  minMemory: 1Gi
  maxMemory: 4Gi

tuning:
  memoryLimitPercent: 60 # DuckDB memory_limit = this percent of maxMemory (20–80)
```

DuckDB reads the *host machine's* RAM and core count rather than the container's limits, so left alone it would size itself for hardware it does not have and get OOM-killed. The template derives both settings from your values and writes them into the preamble:

| Setting | Derived from | At the defaults |
| - | - | - |
| `memory_limit` | `tuning.memoryLimitPercent` of `resources.maxMemory` | `2457MiB` |
| `threads` | One per whole core of `resources.maxCpu`, minimum 1 | `2` |

The chart validates this block at render time: `minMemory` may not exceed `maxMemory`, `minCpu` may not exceed `maxCpu`, `maxCpu:minCpu` may not exceed 4:1, and `memoryLimitPercent` must be between 20 and 80.

**Every job must fit in memory.** No volume is attached, so size the work with `resources.maxMemory` and `tuning.memoryLimitPercent` rather than relying on spill. DuckDB's out-of-core operators still function, but they spill to container-local scratch at `/tmp/duckdb-temp`, bounded by container disk, not to a sized, persistent volume. Extensions are written to `/tmp/duckdb-extensions`, which is likewise container-local, so they are downloaded again on every run; `httpfs` and `aws` autoload on first use of an `s3://` path, so most scripts never issue an explicit `INSTALL`.

### Object Storage

```yaml theme={null}
objectStore:
  type: none # options: none, aws, s3-compatible

  aws: # keyless — credentials come from the workload identity, no keys anywhere
    region: us-east-1
    cloudAccountName: my-s3-cloud-account # must exist BEFORE install
    policyName: my-duckdb-bucket-policy # IAM policy scoped to your bucket (see README)

  s3Compatible: # SeaweedFS, MinIO, Cloudflare R2, GCS interoperability, Tigris
    endpoint: my-seaweedfs.my-gvc.cpln.local:8333 # host:port, no http:// prefix
    region: us-east-1
    urlStyle: path # options: path, vhost
    useSsl: false
    credentialsSecretName: my-duckdb-s3-credentials # dictionary secret: access-key-id, secret-access-key
```

`objectStore.type` decides whether the preamble registers a DuckDB S3 secret; anything other than `none` lets your SQL read and write `s3://` paths directly. The bucket, cloud account, IAM policy and credentials secret each mode needs are set up under [Prerequisites](#prerequisites). With `s3-compatible`, the identity is granted `reveal` on exactly the credentials secret you name, and the two keys are injected as `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.

## Connecting

This template exposes nothing to connect to — it is a job, not a server. Observe and drive it instead:

| What | How |
| - | - |
| Public URL | None. The workload binds no port and accepts no inbound traffic. |
| Internal host:port | None. |
| Job output | `cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-duckdb"}' --limit 50 --since 10m` |
| Run history | `cpln workload cron get RELEASE_NAME-duckdb --gvc GVC_NAME` |
| Run it now | `cpln workload cron start RELEASE_NAME-duckdb --gvc GVC_NAME` |
| Credentials | None are issued. The job reads the secrets you name in `secretEnv` and `objectStore`. |

To verify a fresh install, start a run by hand and look for the completion marker in its output:

```bash theme={null}
cpln workload cron start RELEASE_NAME-duckdb --gvc GVC_NAME
cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-duckdb"} |= "duckdb-job-complete"' --limit 50 --since 10m
```

Log lines can take a few minutes to appear after a run finishes.

## Operations

### Confirming a Run Succeeded

A run succeeded only when **both** of these are true:

1. The line `duckdb-job-complete` appears in the job's log output.
2. The run's status is `Successful` in `cpln workload cron get RELEASE_NAME-duckdb --gvc GVC_NAME`.

Check both, never either one alone. Each covers a hole in the other:

* **The marker alone is not enough.** A run terminated for exceeding `activeDeadlineSeconds` can finish its script inside the termination lag and print the marker on its way out, while the platform has already recorded the run as failed.
* **The status alone is not enough.** DuckDB's CLI can exit `0` on some failed scripts ([upstream issue #16574](https://github.com/duckdb/duckdb/issues/16574)), which would leave the run looking successful.

The marker is emitted by a final statement the template appends after your script, and the preamble's `.bail on` stops execution at the first error — so a SQL error aborts the script and the marker is never printed.

### Scaling and Availability

DuckDB is one process over one database, with no clustering and no failover in any edition, so there is deliberately no `replicas` knob: the workload is pinned to one replica per GVC location. Installing the template several times — different scripts, schedules or GVCs — is real scale-out, but it is not high availability. If a run's container dies, that run did not happen, and a failed run is not retried; the next scheduled run starts normally.

Nothing is stateful, so uninstalling and reinstalling is free: `cpln helm uninstall RELEASE_NAME --gvc GVC_NAME` removes everything the template created and leaves your prerequisite secrets in place.

### Troubleshooting

<AccordionGroup>
  <Accordion title="Nothing is listening after install">
    **Symptom:** the install succeeds, but there is no endpoint, no port and nothing to connect to.

    **Cause:** this is a cron workload. It runs your script on the schedule and exits; nothing is meant to be listening.

    **Fix:** nothing to fix. Read run history with `cpln workload cron get RELEASE_NAME-duckdb --gvc GVC_NAME` and output with `cpln logs`. If you need an always-on SQL endpoint, install [trino](/template-catalog/templates/trino).
  </Accordion>

  <Accordion title="Parser Error: syntax error at or near &#x22;||&#x22;">
    **Symptom:** an `ATTACH` statement that builds its connection string with `|| getenv('PGPASSWORD')` fails:

    ```text theme={null}
    Parser Error: syntax error at or near "||"
    ```

    **Cause:** `ATTACH` takes a string *literal*, not an expression. This applies equally to the `postgres`, `mysql` and `sqlite` attach types.

    **Fix:** name the `secretEnv` entry after the driver's own password variable (`PGPASSWORD` for postgres, `MYSQL_PWD` for mysql) and leave `password=` out of the connection string. If a credential truly must sit inline, put the whole connection string into a `sql.secretName` secret instead.
  </Accordion>

  <Accordion title="The run was killed with no SQL error, or the container reports OOMKilled">
    **Symptom:** the run stops mid-script with no error in the logs, and `cpln workload get-deployments RELEASE_NAME-duckdb --gvc GVC_NAME -o yaml` shows an `OOMKilled` restart reason.

    **Cause:** the job needed more memory than the container has. The most common trigger is a `SET memory_limit` in your own script that exceeds what `resources.maxMemory` can back — your `SET` overrides the template's derived limit.

    **Fix:** remove the `SET memory_limit` from your script and change `tuning.memoryLimitPercent` instead, or raise `resources.maxMemory`. Do not plan around spill; every job must fit in memory.
  </Accordion>

  <Accordion title="A run that worked yesterday now fails installing an extension">
    **Symptom:** a run fails at its first `s3://` read or at an explicit `INSTALL`, with an HTTP or download error naming `extensions.duckdb.org`.

    **Cause:** there is no cache volume, so every run downloads its extensions from `extensions.duckdb.org:443`. The template allows all outbound traffic, so this usually means egress to that host was blocked at the organization or network level after the job was set up.

    **Fix:** restore outbound access to `extensions.duckdb.org:443`. The dependency is per run and cannot be removed by this template.
  </Accordion>

  <Accordion title="The run status is failed but the log shows duckdb-job-complete">
    **Symptom:** `cpln workload cron get` records the run as failed while the completion marker is in the output.

    **Cause:** the run exceeded `activeDeadlineSeconds`. The platform records the failure when the deadline passes, but terminates the container up to roughly two minutes later, and the script finished inside that lag.

    **Fix:** raise `activeDeadlineSeconds` if the run time is legitimate, or reduce the work. Treat this run as failed when deciding whether its output is complete.
  </Accordion>

  <Accordion title="helm install is refused with a message starting duckdb:">
    **Symptom:** the install fails at render time with a `duckdb: ...` message naming a value.

    **Cause:** the chart validates its values before anything is created: the schedule must have five fields; `tuning.memoryLimitPercent` must be 20–80; `minMemory`/`minCpu` may not exceed their max counterparts; `maxCpu:minCpu` may not exceed 4:1; an `objectStore.s3Compatible.endpoint` may not carry a scheme; `secretEnv[].name` must be `UPPER_SNAKE_CASE`, unique and not one of the four `AWS_*` names; and the required fields for the chosen `objectStore.type` must be set.

    **Fix:** change the value the message names and install again. Nothing was created, so there is nothing to clean up.
  </Accordion>

  <Accordion title="The job never runs and cpln logs returns nothing">
    **Symptom:** the scheduled time passes, `cpln logs` shows no lines at all, and a manual `cpln workload cron start` produces no output either.

    **Cause:** a secret named in `sql.secretName`, `secretEnv[]` or `objectStore.s3Compatible.credentialsSecretName` does not exist, so the container cannot start.

    **Fix:** read `status.versions[].message` in `cpln workload get-deployments RELEASE_NAME-duckdb --gvc GVC_NAME -o yaml`, create the missing secret, then run `cpln workload force-redeployment RELEASE_NAME-duckdb --gvc GVC_NAME`.
  </Accordion>
</AccordionGroup>

## Important Notes

* **This is a batch job, not a query service.** Nothing is listening after install; that is correct behavior. For an always-on SQL endpoint, use [trino](/template-catalog/templates/trino).
* **Check both success signals** — the `duckdb-job-complete` marker *and* a run status of `Successful`. Neither one alone is reliable.
* **`activeDeadlineSeconds` terminates a run within roughly two minutes of the deadline**, not exactly at it, and the container bills for that window.
* **Every job must fit in memory.** No volume is attached, so raise `resources.maxMemory` rather than relying on spill.
* **Never `SET memory_limit` higher than the container.** Change `tuning.memoryLimitPercent` instead.
* **No `.duckdb` file is kept.** Every run starts from an empty in-memory database; write results to object storage or an attached database.
* **Extensions are downloaded on every run** from `extensions.duckdb.org:443`. Every execution depends on that host being reachable.
* **Prerequisite secrets must exist before install**, and uninstalling the release does not delete them — the template only removes the secrets it created itself.
* **A cron workload runs in every location of its GVC.** Install into a single-location GVC unless you want one run per location per schedule.
* **Installing this template several times is scale-out, not high availability.** There is no failover and a failed run is not retried.
* **One script per install, by design.** Multi-step, conditional or retrying pipelines belong in [airflow](/template-catalog/templates/airflow).

## External References

<CardGroup cols={2}>
  <Card title="DuckDB Documentation" icon="book" href="https://duckdb.org/docs/stable/">
    Official DuckDB documentation
  </Card>

  <Card title="Configuration Reference" icon="sliders" href="https://duckdb.org/docs/stable/configuration/overview.html">
    Every setting available to a preamble or script
  </Card>

  <Card title="S3 API Support" icon="cloud" href="https://duckdb.org/docs/stable/core_extensions/httpfs/s3api.html">
    Reading and writing s3:// paths with httpfs
  </Card>

  <Card title="Tuning Workloads" icon="gauge" href="https://duckdb.org/docs/stable/guides/performance/how_to_tune_workloads.html">
    How memory and thread settings affect performance
  </Card>

  <Card title="DuckDB Template" icon="github" href="https://github.com/controlplane-com/templates/tree/main/duckdb">
    View the source files, default values, and chart definition
  </Card>
</CardGroup>
