+1 (726) 227-3027

Containerizing Talend Jobs: Docker Images, Kubernetes CronJobs, and Secrets

Most Talend shops still deploy jobs the way they did a decade ago: build a ZIP in Studio or Talend Management Console, unzip it onto a VM, and drive it from cron or a Windows Scheduled Task. That works — we have two tutorials on this site that walk through exactly that — but it does not survive contact with a platform team that has standardised on Kubernetes and wants every workload to be an immutable image with declarative scheduling.

This tutorial shows how to package a Talend Data Integration job as a Docker image, run it as a Kubernetes CronJob, feed it context values and secrets the right way, and keep the logs somewhere a human will actually find them.

It assumes Talend Studio 8 (or Qlik Talend Cloud) with the Maven-based build, and a cluster you can already kubectl apply against.

Step 1: Build the job as a standalone artifact

Everything below depends on a reproducible, headless build. Do not hand-build ZIPs from the Studio UI for anything that reaches production.

In Studio, right-click the job and choose Build Job, then pick Standalone Job. Untick "Extract the zip file" and tick Apply to children so that referenced joblets and child jobs come along. The output is a ZIP containing <jobname>/<jobname>_run.sh, a lib/ folder of JARs, and <jobname>.jar itself.

For CI, skip the UI entirely and use the Talend CommandLine / Maven build that ships with Studio 8:

mvn -f pom.xml clean package \
  -Dproduct.path=/opt/talend/studio \
  -Dgeneration.type=local \
  -Ditem.filter="label==CustomerLoad AND version==latest"

That produces the same artifact from a build agent with no Studio session open. If you already followed our CI/CD for Talend Jobs tutorial, you have this step wired to GitHub Actions and can reuse the resulting ZIP as a build stage input.

Step 2: Write a Dockerfile that is boring on purpose

The job is just Java. The image only needs a JRE, the unpacked artifact, and a sane entrypoint.

# syntax=docker/dockerfile:1
FROM eclipse-temurin:17-jre-jammy

ENV JOB_NAME=CustomerLoad \
    TZ=UTC \
    LANG=C.UTF-8

RUN useradd --system --uid 10001 --create-home talend
WORKDIR /opt/job

# build context contains the unpacked Standalone Job output
COPY --chown=talend:talend ./CustomerLoad/ /opt/job/

USER 10001
ENTRYPOINT ["/bin/sh", "/opt/job/CustomerLoad_run.sh"]

Three details matter more than they look:

  • Pin the JRE major version to the one Studio compiled against. Talend 8 targets Java 11 or 17 depending on your patch level. A job compiled for 17 will fail on 11 with an UnsupportedClassVersionError the first time it runs at 02:00.
  • Run as a non-root UID. Most clusters enforce this with a pod security standard, and _run.sh never needs to write outside its own directory as long as you point temp paths at an emptyDir.
  • Set TZ explicitly. Talend date parsing and TalendDate.getCurrentDate() follow the JVM zone. Containers default to UTC; your old VM probably did not. This is the single most common "the migrated job produces different data" bug we see.

Build and smoke-test it locally before it goes anywhere near a cluster:

docker build -t registry.example.com/etl/customer-load:1.4.2 .
docker run --rm registry.example.com/etl/customer-load:1.4.2 --context=DEV

Step 3: Pass context values as arguments, not baked-in files

_run.sh forwards everything after the script name to the job JAR, so the standard Talend context flags work unchanged:

--context=PROD
--context_param db_host=warehouse.internal
--context_param batch_size=5000

Resist the temptation to copy a PROD.properties into the image. The whole point of an image is that the same digest is promotable across environments; the moment it contains environment config, it isn't. Keep non-secret values in a ConfigMap and pass them as arguments or environment variables that the job reads with tContextLoad.

If you have many parameters, our tContextLoad and multi-environment database tutorials describe the pattern of loading a two-column key/value stream at the top of the job — that pattern maps cleanly onto a mounted ConfigMap file.

Step 4: Secrets stay in the cluster's secret store

Never put a password in a --context_param. It shows up in kubectl describe pod, in the container's /proc command line, and in most log shippers.

Mount it as an environment variable from a Secret and have the job read it:

env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: warehouse-credentials
        key: password

Inside Talend, use System.getenv("DB_PASSWORD") in the context default expression, or read the environment into a context variable with a tJava at the start of the job. If you are on an external manager (Vault, AWS Secrets Manager, Azure Key Vault), use the external secrets operator to project it into a Kubernetes Secret rather than teaching every job an SDK.

Step 5: Schedule it as a CronJob

apiVersion: batch/v1
kind: CronJob
metadata:
  name: customer-load
spec:
  schedule: "15 2 * * *"
  timeZone: "Europe/London"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 10
  startingDeadlineSeconds: 600
  jobTemplate:
    spec:
      backoffLimit: 0
      activeDeadlineSeconds: 5400
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: job
              image: registry.example.com/etl/customer-load:1.4.2
              args: ["--context=PROD"]
              envFrom:
                - configMapRef:
                    name: customer-load-config
              resources:
                requests: { cpu: "500m", memory: "2Gi" }
                limits:   { memory: "4Gi" }
              volumeMounts:
                - { name: tmp, mountPath: /tmp }
          volumes:
            - name: tmp
              emptyDir: { sizeLimit: 10Gi }

The settings that will save you a bad morning:

  • concurrencyPolicy: Forbid — a Talend job that overruns its window and then starts a second copy against the same target table is a data-corruption incident, not a performance problem.
  • backoffLimit: 0 — retry only if the job is genuinely idempotent. Most load jobs are not. Fail loudly and let a human decide.
  • activeDeadlineSeconds — a hung JDBC connection will otherwise sit there until the next run.
  • timeZone on the schedule, separate from TZ in the image. Business schedules follow local time and daylight saving; the job's internal date logic usually should not.

Step 6: Memory limits and the JVM

A container memory limit is not a JVM heap setting. If the heap grows past the limit, the kernel OOM-kills the pod and you get exit code 137 with no stack trace — which looks identical to a cluster eviction.

Give the JVM an explicit ceiling below the pod limit. _run.sh respects JAVA_OPTS:

env:
  - name: JAVA_OPTS
    value: "-XX:MaxRAMPercentage=70 -XX:+ExitOnOutOfMemoryError -Xss512k"

MaxRAMPercentage makes the heap track the cgroup limit, so resizing the pod resizes the heap. ExitOnOutOfMemoryError turns a limping job into a clean failure. If you find yourself pushing memory past 8Gi, the real fix is usually on the job side — tMap lookup models, store-on-disk, and bulk components — which we cover in the Talend job performance tuning material.

Step 7: Exit codes and logs

Kubernetes decides success purely from the exit code. Talend's _run.sh returns the JVM exit code, so tDie gives you a non-zero status and a failed pod, while a swallowed error inside a tLogCatcher that only writes a row does not. Make the failure path end in tDie with a distinct code, or set System.exit(2) in a tJava on the error subjob.

For logs, write structured JSON to stdout rather than to a file inside the container — the filesystem disappears when the pod does. Our error handling and observability patterns for Talend cover the tLogCatcher joblet that emits one JSON object per event; in a container that lands straight in your cluster's log pipeline with pod, namespace and image tags already attached.

Add a lightweight alert on kube_job_status_failed for the namespace and you have replaced the "did last night's load run?" email with something that pages.

What this does not replace

Containerising jobs gives you immutable artifacts, per-job resource isolation and declarative schedules. It does not give you dependency orchestration — CronJob has no concept of "run B after A succeeded." Once you have more than a handful of interdependent jobs, put Airflow, Argo Workflows, or Talend Management Console's execution plans in front of the containers and let the CronJob pattern handle only the standalone, time-driven work. We walk through choosing between those in our post on scheduling Talend jobs in 2026.

If you are planning a move off VM-based Talend deployment and want a second pair of eyes on the build pipeline, image strategy, or the orchestration layer above it, get in touch.