The following is the Tika Pipes GRPC Server.
This server will manage a pool of Tika Pipes clients.
Security note: runtime fetcher/iterator management — mutations (Create/Update/Delete) and reading stored configs back (Read), which may contain secrets — plus per-request parse configuration are disabled by default. Enable them explicitly via
allowComponentManagement/allowPerRequestConfigin thegrpcsection of your tika-config; with management off, the Read RPCs return only component id and class, never the config. See the Tika gRPC security configuration docs.
tika-grpc is designed to be run via Docker — it is not a standalone runnable artifact published to Maven Central.
The Maven artifact for tika-grpc is a thin JAR (~238KB) containing only the compiled classes and resources. It does not include a bundled distribution ZIP or any plugin ZIPs. This was an intentional change (TIKA-4723) to avoid uploading hundreds of megabytes of native libraries and plugin bundles to Nexus on every release.
tika-grpc/docker-build/docker-build.sh, which assembles the full classpath and plugins at Docker build time — not during mvn package.tika-pipes-plugins (fetchers, emitters, iterators) are also not attached as Maven artifacts. They are packaged as pf4j ZIP plugins and included in the Docker image.docker Maven profile:mvn package -Pdocker -pl tika-grpcThis produces
tika-grpc/target/tika-grpc-<version>.zip but does not deploy it to Nexus.fetchAndParse runs on a pool of forked worker JVMs sized by pipes.numClients (when unset, derived from host cores, at most 4). A call that cannot get a worker within pipes.maxWaitForClientMillis (default 60s) returns the in-band status CLIENT_UNAVAILABLE_WITHIN_MS: the server is at capacity, not failing. With pipes.useSharedServer: true the workers share one JVM instead.
The fastest way to run tika-grpc in development mode with plugin hot-reloading:
# 1. Build Tika and all plugins (from tika project root) ./mvnw clean install -DskipTests # 2. Run in development mode (from tika-grpc directory) cd tika-grpc ./run-dev.sh
This will:
tika.plugin.dev.mode=true../tika-pipes/tika-pipes-plugins/*/target/classes directoriesdev-tika-config.json configurationYou can also specify a custom config file:
./run-dev.sh my-custom-config.json
When developing plugins, you can use pf4j's development mode to load plugins directly from their target/classes directories without needing to package them as ZIP files. This significantly speeds up the development cycle.
Set one of the following:
System Property:
-Dtika.plugin.dev.mode=true
Environment Variable:
export TIKA_PLUGIN_DEV_MODE=true
Maven Dev Profile: (Recommended)
./mvnw exec:java -Pdev -Dconfig.file=dev-tika-config.json
The dev-tika-config.json file shows how to configure plugin-roots with relative paths:
{ "plugin-roots": [ "../tika-pipes/tika-pipes-plugins/tika-pipes-file-system/target/classes", "../tika-pipes/tika-pipes-plugins/tika-pipes-http/target/classes", "../tika-pipes/tika-pipes-plugins/tika-pipes-s3/target/classes" ], "fetchers": [ { "fs": { "myFetcher": { "basePath": "/tmp/input" } } } ] }
Note: Paths are relative to the tika-grpc directory (where you run the server from). You can also use absolute paths if preferred:
{ "plugin-roots": [ "/home/user/tika/tika-pipes/tika-pipes-plugins/tika-pipes-s3/target/classes", "/home/user/tika/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/target/classes" ] }
Build the plugin modules (only needed once or when dependencies change):
cd tika-pipes/tika-pipes-plugins ./mvnw clean compile
Run in development mode using the convenience script:
cd tika-grpc ./run-dev.sh
Make code changes to your plugin
Recompile just the changed plugin (much faster than full rebuild):
# From the project root ./mvnw compile -pl :tika-pipes-s3
Restart the server - changes are immediately picked up
target/classesplugin.properties in the root (automatically present after ./mvnw compile)When pointing to target/classes, pf4j expects this structure:
tika-pipes-s3/target/classes/
├── plugin.properties # Required: plugin metadata
├── META-INF/
│ └── extensions.idx # Generated by pf4j annotation processor
└── org/
└── apache/
└── tika/
└── pipes/
└── fetcher/
└── s3/
├── S3Fetcher.class
└── S3FetcherFactory.class
For IntelliJ IDEA development, here's a complete workflow for developing plugins:
Import the Tika project as a Maven project in IntelliJ
Build all plugins once (required before first run):
cd tika-pipes/tika-pipes-plugins ./mvnw clean compile
Create a Run Configuration for tika-grpc:
+ → Applicationorg.apache.tika.pipes.grpc.TikaGrpcServer-Dtika.plugin.dev.mode=true--config dev-tika-config.json$PROJECT_DIR$/tika-grpctika-grpcRun the configuration - You should see output like:
INFO TikaPluginManager running in DEVELOPMENT mode INFO PF4J version 3.14.0 in 'development' mode INFO Plugin 'tika-pipes-file-system-plugin@4.0.0-SNAPSHOT' resolved INFO Plugin 'tika-pipes-s3-plugin@4.0.0-SNAPSHOT' resolved ... INFO Server started, listening on 50052
Scenario: You want to modify the S3 Fetcher plugin
Navigate to the plugin code:
tika-pipes/tika-pipes-plugins/tika-pipes-s3/src/main/java/
Make your code changes in the plugin source files
Build just the modified plugin module:
# From project root ./mvnw compile -pl :tika-pipes-s3
Restart the tika-grpc server:
Verify your changes:
Use IntelliJ's Build shortcuts:
Ctrl+F9 (Windows/Linux) or Cmd+F9 (Mac) - Build project/moduleDebug mode:
Multiple terminal windows:
./run-dev.sh./mvnw compile -pl :plugin-name from project rootKeyboard shortcut for restart:
“ClassNotFoundException” after changes:
target/classes was updated (look at file timestamps)./mvnw clean compileChanges not visible after restart:
target/classes directoryServer won't start:
./mvnw compile -pl tika-pipes/tika-pipes-pluginsdev-tika-config.json exists in the working directory$PROJECT_DIR$/tika-grpcWant to see what changed:
Instead of creating an Application run configuration, you can also run the Maven goal directly:
-Dconfig.file=my-config.json
Same workflow applies: make changes → build module → restart Maven goal
PF4J plugin loading happens at server startup:
TikaPluginManager.loadPlugins() discovers and loads plugins from directoriesPluginClassLoader instances for each pluginstart() methodsNo hot reload support (yet):
Fast iteration cycle:
For production deployments, use packaged ZIP files:
Remove or set development mode to false:
unset TIKA_PLUGIN_DEV_MODE # OR export TIKA_PLUGIN_DEV_MODE=false
Build plugin ZIPs:
./mvnw clean package -pl tika-pipes/tika-pipes-plugins
Update plugin-roots to point to the directory containing ZIP files:
{ "plugin-roots": [ "/opt/tika/plugins" ] }
Place plugin ZIPs in the configured directory:
cp tika-pipes-plugins/*/target/*.zip /opt/tika/plugins/
Plugin not loading?
./mvnw compile was run on the plugin moduleplugin.properties exists in target/classes/Changes not picked up?
./mvnw compile -pl :plugin-nameClassNotFoundException errors?
./mvnw clean install -DskipTestsThe official Tika gRPC Docker images are published to Docker Hub at apache/tika-grpc.
Pull the latest image:
docker pull apache/tika-grpc:latest
Run with default configuration:
docker run -p 50052:50052 apache/tika-grpc:latest
Run with custom configuration:
docker run -p 50052:50052 \ -v $(pwd)/my-config.json:/config/tika-config.json \ apache/tika-grpc:latest --config /config/tika-config.json
For simple deployments without distributed configuration:
apiVersion: apps/v1 kind: Deployment metadata: name: tika-grpc spec: replicas: 3 selector: matchLabels: app: tika-grpc template: metadata: labels: app: tika-grpc spec: containers: - name: tika-grpc image: apache/tika-grpc:latest ports: - containerPort: 50052 name: grpc resources: requests: memory: "2Gi" cpu: "1000m" limits: memory: "4Gi" cpu: "2000m" volumeMounts: - name: config mountPath: /config volumes: - name: config configMap: name: tika-grpc-config --- apiVersion: v1 kind: Service metadata: name: tika-grpc spec: selector: app: tika-grpc ports: - port: 50052 targetPort: 50052 name: grpc type: ClusterIP
Not available in the published
apache/tika-grpcimage. tika-grpc no longer ships the Ignite jars — they pulled in ~196 transitive artifacts for a store that, in practice, only ever talked to an embedded node in the same JVM (IgniteConfigStore.init()hardcodes127.0.0.1:10800). SettingconfigStoreType: ignitewithout those jars fails at startup with an explanatory error. To use it, build an image that addstika-pipes-config-store-igniteand its Ignite dependencies to the classpath (thedevMaven profile wires them up for local runs).memoryandfileare the config stores available out of the box.
For distributed deployments with shared configuration using Apache Ignite:
1. Create ConfigMap with Tika configuration:
apiVersion: v1 kind: ConfigMap metadata: name: tika-grpc-config data: tika-config.json: | { "pipes": { "configStoreType": "ignite", "configStoreParams": "{\"cacheName\":\"tika-config-cache\",\"cacheMode\":\"REPLICATED\",\"igniteInstanceName\":\"TikaCluster\"}" }, "fetchers": [ { "s3": { "myS3Fetcher": { "region": "us-east-1", "bucket": "my-bucket" } } } ], "emitters": [ { "s3": { "myS3Emitter": { "region": "us-east-1", "bucket": "my-output-bucket" } } } ] }
2. Create StatefulSet for Ignite cluster discovery:
apiVersion: v1 kind: Service metadata: name: tika-grpc-ignite labels: app: tika-grpc spec: clusterIP: None # Headless service for Ignite discovery selector: app: tika-grpc ports: - port: 47100 name: ignite-comm - port: 47500 name: ignite-disco - port: 50052 name: grpc --- apiVersion: apps/v1 kind: StatefulSet metadata: name: tika-grpc spec: serviceName: tika-grpc-ignite replicas: 3 selector: matchLabels: app: tika-grpc template: metadata: labels: app: tika-grpc spec: containers: - name: tika-grpc image: apache/tika-grpc:latest ports: - containerPort: 50052 name: grpc - containerPort: 47100 name: ignite-comm - containerPort: 47500 name: ignite-disco env: - name: JAVA_OPTS value: >- -Xmx2g -Xms2g --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/com.sun.jmx.mbeanserver=ALL-UNNAMED - name: IGNITE_KUBERNETES_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: IGNITE_KUBERNETES_SERVICE_NAME value: "tika-grpc-ignite" resources: requests: memory: "2Gi" cpu: "1000m" limits: memory: "4Gi" cpu: "2000m" volumeMounts: - name: config mountPath: /config readinessProbe: exec: command: - grpc_health_probe - -addr=:50052 initialDelaySeconds: 10 periodSeconds: 5 livenessProbe: exec: command: - grpc_health_probe - -addr=:50052 initialDelaySeconds: 30 periodSeconds: 10 volumes: - name: config configMap: name: tika-grpc-config --- apiVersion: v1 kind: Service metadata: name: tika-grpc spec: selector: app: tika-grpc ports: - port: 50052 targetPort: 50052 name: grpc type: LoadBalancer # Or ClusterIP for internal-only access
3. RBAC permissions for Kubernetes API access (Ignite discovery):
apiVersion: v1 kind: ServiceAccount metadata: name: tika-grpc --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: tika-grpc rules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list"] - apiGroups: [""] resources: ["endpoints"] verbs: ["get", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: tika-grpc subjects: - kind: ServiceAccount name: tika-grpc roleRef: kind: Role name: tika-grpc apiGroup: rbac.authorization.k8s.io
Then update the StatefulSet to use the service account:
spec: template: spec: serviceAccountName: tika-grpc containers: # ... rest of container spec
When using Apache Ignite ConfigStore in Kubernetes, the Ignite nodes automatically discover each other using Kubernetes API. Here's how it works:
clusterIP: None) allows each pod to have its own DNS entrytika-grpc-0, tika-grpc-1, tika-grpc-2IGNITE_KUBERNETES_NAMESPACE: Current namespaceIGNITE_KUBERNETES_SERVICE_NAME: Service name for discoveryKey Ports:
Benefits of Ignite ConfigStore in Kubernetes:
Scale based on CPU/memory or custom metrics:
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: tika-grpc spec: scaleTargetRef: apiVersion: apps/v1 kind: StatefulSet name: tika-grpc minReplicas: 3 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80
Prometheus metrics endpoint:
Add to the StatefulSet:
annotations: prometheus.io/scrape: "true" prometheus.io/port: "8081" prometheus.io/path: "/metrics"
Logging:
Configure structured JSON logging for better observability:
env: - name: LOG_LEVEL value: "INFO" - name: LOG_FORMAT value: "json"
Pods can't discover each other:
kubectl logs tika-grpc-0 | grep IgniteOut of Memory errors:
JAVA_OPTSSlow gRPC responses: