Skip to main content

Deployment

This page covers issues that surface only after an integration leaves your machine: Docker containers, Kubernetes clusters, GraalVM native images, Choreo, and configuration injected at runtime.

Before you start digging into the deployment layer, confirm the integration runs locally with bal build && bal run. If it fails locally, fix the integration first using the other pages in this section. If it works locally but breaks once deployed, the issue is in the deployment environment, and the sections below help you isolate it.

Docker deployments

WSO2 Integrator supports Code-to-Cloud (C2C) Docker generation through cloud = "docker" in Ballerina.toml. The generated Dockerfile uses the ballerina/jvm-runtime base image, which is Debian-based. A custom base image (for example, Alpine) can introduce native library compatibility issues, particularly around DNS resolution and TLS.

Build the image:

# In Ballerina.toml: [build-options] cloud = "docker"
bal build

# Docker artifacts in: target/docker/<package-name>/
docker build -t myapp:latest target/docker/myapp/
docker run -p 9090:9090 myapp:latest

Common Docker issues

SymptomCauseFix
Container starts but service unreachablePort not exposed or bound to wrong interfaceExpose the port in the Dockerfile or Cloud.toml; verify the binding is on 0.0.0.0
Config.toml not found in the containerConfig file not mountedMount the file as a Docker volume or use the BAL_CONFIG_DATA environment variable
OutOfMemoryError in the containerContainer memory limit too low for the JVMIncrease the container memory limit; set -Xmx through JAVA_OPTS
"File not found" for resourcesRelative paths don't resolve inside the containerUse paths relative to the working directory (/home/ballerina)
TLS certificate errorsCertificates not available in the containerMount certificate files and update the Config.toml paths to match

Pass configuration to a container

# Option 1: Environment variable (preferred for secrets)
docker run -e BAL_CONFIG_DATA='[myorg.myapp]
port = 8080
apiKey = "secret"
' myapp:latest

# Option 2: Mount Config.toml
docker run -v /path/to/Config.toml:/home/ballerina/Config.toml myapp:latest

Kubernetes deployments

Use cloud = "k8s" in Ballerina.toml. Kubernetes artifacts are generated in target/kubernetes/.

Common Kubernetes issues

SymptomCauseFix
Pod in CrashLoopBackOffThe integration panics on startup, runs out of memory, or can't find its configurationInspect kubectl logs <pod> and look for the error on stdout or stderr
Pod in PendingThe cluster doesn't have enough resourcesRun kubectl describe pod <pod> and check the resource constraints
Service unreachable externallyMissing Ingress or LoadBalancerCheck the external_accessible setting in Cloud.toml
Configuration not loadedConfig.toml not mounted as a ConfigMap or SecretSee the Cloud.toml example below
ImagePullBackOffImage not pushed to the registry, or the registry URL is wrongPush the image; check container.image.repository in Cloud.toml
Health check failingLiveness or readiness probe endpoints not configuredAdd probe configuration to Cloud.toml

Cloud.toml for Kubernetes

[container.image]
repository = "myregistry.io/myorg"
name = "myservice"
tag = "v1.0.0"

[cloud.deployment]
min_memory = "256Mi" # default: "100Mi"
max_memory = "512Mi" # default: "256Mi"
min_cpu = "250m" # default: "1000m"
max_cpu = "1500m" # default: "1500m"
external_accessible = true

[cloud.deployment.autoscaling]
min_replicas = 2 # default: 2
max_replicas = 5 # default: 3
cpu = 50 # CPU threshold percentage; default: 50
memory = 80 # memory threshold percentage; default: 80

[cloud.deployment.probes.readiness]
port = 9090
path = "/health/ready"

[cloud.deployment.probes.liveness]
port = 9090
path = "/health/live"

# Environment variables from secrets
[[cloud.secret.envs]]
key_ref = "DB_PASSWORD"
name = "DB_PASSWORD"
secret_name = "db-secret"

# Config files mounted as ConfigMaps
[[cloud.config.files]]
file = "Config.toml"

# Secret files mounted to a directory
[[cloud.secret.files]]
file = "secrets.toml"
mount_dir = "/home/ballerina/secrets"

For the full Cloud.toml reference, see the Code-to-Cloud specification.

GraalVM native image

WSO2 Integrator supports GraalVM native executables for faster startup and a lower memory footprint.

Build with:

bal build --graalvm

Prerequisites

  • GraalVM JDK installed, with GRAALVM_HOME set or native-image on the PATH.
  • The following block in Ballerina.toml (alternative to the --graalvm CLI flag):
[build-options]
graalvm = true
graalvmBuildOptions = "--no-fallback" # optional: additional native-image flags

Verify in JVM mode first

If a native image fails at runtime, reproduce the issue with bal run (without --graalvm) first. If JVM mode works but the native image doesn't, the problem is GraalVM-specific. This single check saves time on most native-image investigations.

Common build failures

Error or symptomCauseFix
native-image command not foundGraalVM not installed or not on the PATHInstall GraalVM, set GRAALVM_HOME, and run gu install native-image
Reflection error at runtimeA class accessed through reflection isn't registeredAdd a reflect-config.json file to resources/META-INF/native-image/ in the project
Build fails with "unsupported feature"A library uses a JVM feature that GraalVM doesn't supportCheck whether the library has GraalVM support; some ballerinax connectors may not be compatible
Native image crashes at runtimeRuntime behavior differs from JVM modeTest in JVM mode first. If JVM mode works, file a GraalVM compatibility issue.
Build takes very long or runs out of memoryNative image compilation is resource-intensiveIncrease the build memory: graalvmBuildOptions = "-J-Xmx8g"

JAVA_OPTS (for example, -Xmx) does not apply to native images. Native images manage their own memory. To control native image heap size, use -R:MaxHeapSize=512m in graalvmBuildOptions or at runtime.

Choreo deployments

Choreo is WSO2's integration platform, and it natively supports WSO2 Integrator services. When a service misbehaves on Choreo, start with one diagnostic question:

Does bal build && bal run work locally?

  • Yes. The code is correct, and the issue is in the Choreo build pipeline, configuration injection, or runtime environment. Raise it with the Choreo team.
  • No. Fix the integration locally first using the other pages in this section, then redeploy.

Common Choreo issues

IssueSymptomFix
Configurable values not loadedThe service uses default values instead of the values set in ChoreoVerify the configurable keys match the [org.package.module] format in Choreo's configuration panel
Build fails in the Choreo pipelineBuild succeeds locally but fails in ChoreoCheck whether the Choreo build uses a different runtime version; verify all dependencies are available from Ballerina Central (no local-only dependencies)
Service health check failingPod restarts in ChoreoConfigure the health check endpoint in Choreo to match the service's actual health endpoint
Cannot access logsNo visibility into runtime errorsCheck Choreo's Monitoring and Insights sections. Log availability varies by Choreo plan and component type. Consult the Choreo documentation or the Choreo team for current capabilities.
Environment variables not availableos:getEnv() returns emptyConfigure environment variables in Choreo's component settings, not in source code

Configuration in deployed environments

Configurable values are resolved in this priority order (highest to lowest):

  1. BAL_CONFIG_VAR_<VARIABLE_NAME> environment variables (per-variable override; highest priority).
  2. The BAL_CONFIG_DATA environment variable (inline TOML).
  3. Files listed in BAL_CONFIG_FILES (colon-separated on Linux and macOS, semicolon-separated on Windows).
  4. Config.toml in the working directory.

For sensitive values such as API keys and passwords, prefer injecting each value through a BAL_CONFIG_VAR_<VARIABLE_NAME>=<value> environment variable instead of mounting a file. This keeps secrets off disk inside the container.

Kubernetes: separate ConfigMap and Secret

# Store non-sensitive config in a ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
data:
config.toml: |
[myorg.myapp]
port = 8080
dbHost = "postgres.internal"
---
# Store secrets separately
apiVersion: v1
kind: Secret
metadata:
name: myapp-secrets
data:
secrets.toml: <base64-encoded TOML with passwords>
# In the pod spec
env:
- name: BAL_CONFIG_FILES
value: "/config/config.toml:/secrets/secrets.toml"
volumeMounts:
- name: config-vol
mountPath: /config
- name: secrets-vol
mountPath: /secrets

Debug configuration inside a pod

# Verify the pod can see the configuration
kubectl exec -it <pod-name> -- env | grep BAL_CONFIG
kubectl exec -it <pod-name> -- cat /config/config.toml

# Confirm the configurable values are actually read
# Add a debug log in the service init that prints the resolved value

What's next

  • Logging - add log statements to trace what the deployed integration is doing.
  • Errors and stack traces - read the error output captured from a deployed pod or container.
  • Profiling - investigate performance issues that only appear under deployed load.