Cameleer

Deploying Cameleer

Cameleer runs anywhere a container or a single Linux, macOS or Windows binary runs. This guide covers the container image: Docker, Docker Compose, Kubernetes and serverless platforms.

The image

Namepmsudhi/cameleer on Docker Hub, also ghcr.io/pmsudhi/cameleer
Platformslinux/amd64, linux/arm64
Contentsone native binary (glibc) on gcr.io/distroless/cc-debian12:nonroot: CA certificates, tzdata, no shell
Sizeabout 20 MB to download; under 12.5 MB built with FEATURES=core (build features)
Entrypointcameleer (the CLI): run, version, get health, --help
Working directory/routes: mount route files here, read-only is fine
Port8080: the REST DSL, platform-http and the health endpoints share it
User65532 (nonroot); any other --user works
Filesystemnothing is written: CAMEL_LOCAL_STATUS=false, logs go to stdout; works with a read-only root filesystem
StopSIGTERM stops the routes gracefully (about half a second for a REST route)
Tagslatest, 0.3.0, 0.3, edge (manual builds of main), sha-<commit>
Supply chainSBOM, max-mode build provenance and a GitHub attestation on every push

Verify an image's provenance:

sh
gh attestation verify oci://ghcr.io/pmsudhi/cameleer:latest --owner pmsudhi

Configuration

Settings come from, in increasing priority:

  1. application.properties or application.yaml, passed with --properties /config/application.properties
  2. environment variables: CAMEL_SERVER_PORT=9090 sets camel.server.port, and so on
  3. --prop key=value on the command line

Routes read environment values with {{env:NAME:default}}, as in Apache Camel. Settings can use them too: camel.server.port={{env:PORT:8080}}.

Health endpoints need the management server on:

sh
--prop camel.server.enabled=true --prop camel.server.healthCheckEnabled=true

This serves /q/health, /q/health/live and /q/health/ready on port 8080. Readiness is DOWN (HTTP 503) while a route is stopped. cameleer get health --url http://localhost:8080 exits with status 1 when it is DOWN, so it can serve as a Docker health check.

Don't turn on camel.server.devConsoleEnabled (or cameleer run --console) in production without a network policy in front: the dev console has unauthenticated endpoints that stop and start routes.

Docker

sh
docker run --rm -p 8080:8080 --read-only \
  -v "$PWD/routes:/routes:ro" \
  -e OPENAI_API_KEY \
  pmsudhi/cameleer run /routes/my-route.camel.yaml
  • Mount route folders read-only (:ro) unless a route writes there. For a route that writes into a mounted folder (the file component's move, for example), run as your own user so the files are yours: --user "$(id -u):$(id -g)".
  • Reach services on the host with --network host on Linux. With Docker Desktop on macOS or Windows, use host.docker.internal instead. Kafka brokers must also advertise an address the container can reach.

Baking routes into your own image

For production, ship an image that contains the routes:

dockerfile
FROM pmsudhi/cameleer:0.3.0
COPY routes/ /routes/
CMD ["run", "/routes/my-route.camel.yaml", "--prop", "camel.server.enabled=true", "--prop", "camel.server.healthCheckEnabled=true"]

Docker Compose

yaml
services:
  orders:
    image: pmsudhi/cameleer:0.3.0
    command: ["run", "/routes/rest-api.camel.yaml",
              "--prop", "camel.server.enabled=true", "--prop", "camel.server.healthCheckEnabled=true"]
    ports: ["8080:8080"]
    volumes: ["./routes:/routes:ro"]
    read_only: true
    healthcheck:
      test: ["CMD", "/usr/local/bin/cameleer", "get", "health", "--url", "http://localhost:8080"]
      interval: 10s
      timeout: 3s
      retries: 3

Kubernetes

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: orders-routes
data:
  rest-api.camel.yaml: |
    # paste examples/rest-api.camel.yaml here
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders
spec:
  replicas: 2
  selector:
    matchLabels: { app: orders }
  template:
    metadata:
      labels: { app: orders }
    spec:
      terminationGracePeriodSeconds: 30
      securityContext:
        runAsNonRoot: true
        seccompProfile: { type: RuntimeDefault }
      containers:
        - name: cameleer
          image: pmsudhi/cameleer:0.3.0
          args: ["run", "/routes/rest-api.camel.yaml",
                 "--prop", "camel.server.enabled=true",
                 "--prop", "camel.server.healthCheckEnabled=true"]
          ports:
            - { name: http, containerPort: 8080 }
          env:
            - name: OPENAI_API_KEY
              valueFrom: { secretKeyRef: { name: openai, key: api-key, optional: true } }
          resources:
            requests: { cpu: 50m, memory: 32Mi }
            limits: { memory: 256Mi }
          readinessProbe:
            httpGet: { path: /q/health/ready, port: http }
            periodSeconds: 5
          livenessProbe:
            httpGet: { path: /q/health/live, port: http }
            periodSeconds: 10
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities: { drop: ["ALL"] }
          volumeMounts:
            - { name: routes, mountPath: /routes, readOnly: true }
      volumes:
        - name: routes
          configMap: { name: orders-routes }

A route uses about 10 MB at rest, most of it pages of the binary itself; the private memory of a running route is 1–2 MB. Size the memory limit for your message sizes and concurrency, not for a JVM.

Serverless

  • Google Cloud Run, Knative, Azure Container Apps, Fly.io: these run the image as it is. Cloud Run sends requests to PORT, which is 8080 unless you change it. If you do change it, add --prop camel.server.port={{env:PORT:8080}}. Startup takes milliseconds, so scale-to-zero has no practical cold-start cost from Cameleer itself.
  • AWS Lambda: see below. cameleer is its own Lambda custom runtime.

AWS Lambda

cameleer serves Lambda invocations itself on the provided.al2023 runtime: no runtime library, adapter or layer. When Lambda sets AWS_LAMBDA_RUNTIME_API, cameleer run starts the routes and then loops over the Runtime API instead of waiting for a signal.

  • HTTP events (API Gateway REST and HTTP APIs, function URLs, ALB) go to your platform-http routes as if they had come over a socket, and the HTTP response becomes the proxy-integration response. The same route file runs on Lambda and in a container.
  • Other events (S3, SQS, EventBridge, a direct invoke) are sent request-reply to the handler endpoint with the event JSON as the body; the reply body is the function result. The handler endpoint is the function's Handler setting when it is an endpoint URI (for example direct:orders), else camel.main.lambda-handler, else direct:lambda.
  • A failed exchange is reported to the invocation's /error endpoint (errorType is the exception kind); a startup failure (a bad route file, an unknown component) to /init/error.
  • Exchange headers: CamelAwsLambdaRequestId, CamelAwsLambdaDeadlineMs, CamelAwsLambdaFunctionArn, CamelAwsLambdaTraceId, and when Lambda sends them CamelAwsLambdaClientContext and CamelAwsLambdaCognitoIdentity.
  • Status files for cameleer ps are off on Lambda (the file system is read-only); logs go to CloudWatch through stderr.
yaml example: deployment-lambda-handler
# orders.camel.yaml: an S3 or EventBridge event in, a JSON summary out
- route:
    from:
      uri: direct:lambda
      steps:
        - setBody:
            simple: '{"bucket":"${jsonpath($.Records[0].s3.bucket.name)}","ok":true}'

Package and deploy (the binary must be a static musl build: AL2023 has glibc 2.34, older than what current build machines link against):

sh
scripts/package-lambda.sh --arch x86_64 --features cloud --out function.zip orders.camel.yaml
aws lambda create-function --function-name orders --runtime provided.al2023 \
  --architectures x86_64 --handler direct:lambda --zip-file fileb://function.zip \
  --role arn:aws:iam::<account>:role/<role>

The zip holds bootstrap (the cameleer binary under the name Lambda starts; with no arguments it runs the route files in $LAMBDA_TASK_ROOT) and your route files. --features keeps only the component families the routes use (see build features), which makes the zip and the cold start smaller. Measured init times are in BENCHMARKS.md.

camel.main.duration-max-messages stops after that many invocations (useful in tests). Lambda sends SIGTERM before it shuts a sandbox down; routes are stopped gracefully then.

Smaller builds

The default build and image contain every component family. A function, an edge device or a slim image can leave the rest out with the cargo features of cameleer-main. The build features guide lists every feature and what it adds, shows how to choose them for your routes, and has the commands for each kind of build. In short:

sh
docker build --build-arg FEATURES=cloud,sql,jq -t my-cameleer .
cargo install cameleer-main --no-default-features --features cloud,sql,jq
scripts/package-lambda.sh --features cloud,jq --out function.zip orders.camel.yaml
  • A feature list replaces the default: name every family you use. The template engines, jq, the core XPath subset, http2 and validate (cameleer validate, cameleer schema) are features too.
  • A route that uses something left out fails at startup with the feature to add, for example No component found with scheme: kafka (this cameleer binary was built without the `kafka-mqtt` feature; rebuild cameleer-main with `--features kafka-mqtt`).
  • cameleer version lists the features of a build.
  • FEATURES=core is the smallest: 5.8 MB (glibc) or 6.0 MB (static musl) on Linux x86-64, against 23.9 MB with every component. More sizes and cold-start times are in BENCHMARKS.md.

Without a container

sh
cargo install cameleer-main
cameleer run my-route.camel.yaml

Camel JBang habits carry over with the cameleer command: cameleer ps, cameleer log, cameleer stop. Those commands use status files under ~/.camel (CAMEL_HOME). Set CAMEL_LOCAL_STATUS=false to run without them.

Found a problem?

If something on this page doesn't match what Cameleer does, or you hit a bug while building with it, please help fix it, whether you are a person or a coding agent: open an issue that describes what you saw, then fork the repository, fix it, add or update a test, run cargo test --workspace, and open a pull request. CONTRIBUTING.md explains the ground rules.

Edit this page on GitHub