Worker
Provisioning with Ansible
If you run more than one GPU box, you don't want to click through setup on each one. This page gives you a single Ansible playbook that provisions a Linux worker end to end: it installs the Pendra worker, writes its worker key, starts the service, and installs the models you list — the same steps the manual install walks through, but hands-off and repeatable across every host in your inventory.
It targets native package installs on Linux (the
.deb / .rpm served from the signed Pendra
repository). Re-running it is safe: each step only changes what's out of
date, so the same playbook both provisions a new host and reconciles an
existing one.
pdr_sk_ API key: worker keys
authenticate machines, API keys authenticate clients.
Before you start
- A control machine with Ansible installed and SSH access to each worker host.
- One worker key generated per host in the console.
- Linux hosts on glibc 2.38 or newer (Ubuntu 24.04+, Debian 13+, or equivalent) — the inference runtime needs it.
pendra-cuda) need the CUDA 13 runtime
first. The pendra-cuda package requires NVIDIA driver
R580 or newer plus the CUDA 13 runtime
(libcudart.so.13 / libcublas.so.13), which the
package can't bundle. Install it before this playbook runs — a fresh
install of pendra-cuda refuses rather than come up CPU-only.
See Install the worker for the runtime
commands. Hosts using the CPU (pendra) or Vulkan
(pendra-vulkan) packages have no such prerequisite.
1. Inventory and variables
List your workers and set a few group-wide variables. The GPU variant and
the model list live in group_vars; the worker key is a secret,
so keep it per host in an encrypted vault file (below).
# inventory.ini
[pendra_workers]
gpu-01.example.com
gpu-02.example.com
[pendra_workers:vars]
ansible_user=ubuntu
# group_vars/pendra_workers.yml
pendra_gpu_variant: cuda # cpu | cuda | vulkan
pendra_ws_url: wss://api.pendra.ai
# The models every worker in this group should serve, by catalogue id.
# These are the ids you see on console.pendra.ai → Models.
pendra_models:
- qwen3.6:27b
- nomic-embed-text
pendra_gpu_variant selects the package: cpu
installs pendra, cuda installs
pendra-cuda, and vulkan installs
pendra-vulkan (NVIDIA via Vulkan, or AMD / Intel GPUs). Set it
per host in host_vars if your fleet is mixed.
pendra_models is your list — every entry is a model
id from the catalogue on
console.pendra.ai
→ Models. Add or remove ids here and the next run
reconciles each worker to match.
pendra_worker_key in an encrypted host_vars file
so the secret never sits in plaintext in your repo:
# host_vars/gpu-01.example.com/vault.yml
# Encrypt this file: ansible-vault encrypt host_vars/gpu-01.example.com/vault.yml
pendra_worker_key: "MC4CAQAwBQYDK2VwBCIEIExampleBase64Ed25519PrivateKey=="
2. The playbook
Register the signed repository, install the worker package for the host's GPU, template the config file, start the service, then install any models the worker is missing. It handles both Debian/Ubuntu (apt) and RHEL/Fedora/openSUSE (yum/dnf) hosts.
# pendra.yml — install the worker, give it a key, install its models
- name: Provision Pendra workers
hosts: pendra_workers
become: true
handlers:
- name: restart pendra
ansible.builtin.systemd:
name: pendra
state: restarted
tasks:
# 1. Register the signed Pendra package repository.
- name: Add Pendra apt repository (Debian / Ubuntu)
when: ansible_os_family == "Debian"
block:
- name: Install the Pendra signing key
ansible.builtin.get_url:
url: https://get.pendra.ai/pendra-archive-keyring.gpg
dest: /usr/share/keyrings/pendra-archive-keyring.gpg
mode: "0644"
- name: Add the Pendra apt source
ansible.builtin.apt_repository:
repo: "deb [signed-by=/usr/share/keyrings/pendra-archive-keyring.gpg] https://get.pendra.ai/apt stable main"
filename: pendra
- name: Add Pendra yum repository (RHEL / Fedora / openSUSE)
when: ansible_os_family in ["RedHat", "Suse"]
ansible.builtin.yum_repository:
name: pendra
description: Pendra Worker
baseurl: https://get.pendra.ai/yum/$basearch
gpgcheck: true
repo_gpgcheck: true
gpgkey: https://get.pendra.ai/RPM-GPG-KEY-pendra
# 2. Install the package that matches this host's GPU.
- name: Install the Pendra worker
ansible.builtin.package:
name: "{{ 'pendra' if pendra_gpu_variant == 'cpu' else 'pendra-' + pendra_gpu_variant }}"
state: present
update_cache: true
# 3. Write the worker key + config, owned by the pendra service user.
- name: Configure the worker
ansible.builtin.template:
src: config.yaml.j2
dest: /var/lib/pendra/config.yaml
owner: pendra
group: pendra
mode: "0600"
notify: restart pendra
# 4. Enable the service so it starts now and on every boot.
- name: Enable and start pendra
ansible.builtin.systemd:
name: pendra
enabled: true
state: started
# 5. Install any models the worker doesn't already have.
# Running a task as the pendra user needs acl on the target, so
# make sure it's installed first.
- name: Ensure acl is installed (needed to run tasks as the pendra user)
ansible.builtin.package:
name: acl
state: present
- name: Find the models already installed
ansible.builtin.command: pendra models
become_user: pendra
changed_when: false
register: pendra_models_present
- name: Install any missing models
ansible.builtin.command: "pendra models install {{ item }}"
become_user: pendra
loop: "{{ pendra_models }}"
when: item not in pendra_models_present.stdout
And the tiny config template it renders:
# templates/config.yaml.j2 — rendered to /var/lib/pendra/config.yaml
app_server_ws_url: "{{ pendra_ws_url }}"
gpu_worker_private_key: "{{ pendra_worker_key }}"
worker_name: "{{ pendra_worker_name | default(inventory_hostname, true) }}"
# Pin a stable worker_id so re-running the playbook keeps the same
# worker instead of registering a new one each time.
worker_id: "{{ pendra_worker_id | default('wrk-' + (inventory_hostname | to_uuid)[:8], true) }}"
A few things worth knowing about why the playbook is shaped this way:
-
The config file is owned by the
pendraservice user. On a package install the worker runs as thependrauser and reads/var/lib/pendra/config.yamlat mode0600, so the template task setsowner: pendra,group: pendra, andmode: "0600". Templating the file directly (rather than shelling out topendra config set) keeps the task declarative and idempotent. -
worker_idis pinned. If you leave it out, the worker generates a fresh id on first start — and a later re-render would make it generate another one, showing up as a duplicate worker in the console. Deriving a stable id from the hostname keeps each host the same worker across runs. -
Models install as the
pendrauser. The install step runsbecome_user: pendraso the downloaded model files land in the service's own model directory, readable by the running worker. Running a task as thependrauser needs theaclpackage on the target, so the playbook installs it first. It also lists what's already installed and only pulls the models that are missing, so re-running skips anything the worker already has. -
A key change restarts the worker. The template task
notifies a handler that restarts
pendra, so rotating a worker key (or changing the WS URL) takes effect at the end of the run.
3. Run it
# Encrypt the per-host key file, then run the playbook
ansible-vault encrypt host_vars/gpu-01.example.com/vault.yml
ansible-playbook -i inventory.ini pendra.yml --ask-vault-pass
Ansible connects to each host, installs and configures the worker, and pulls the model list. The first run downloads model files, so give it a few minutes per large model; later runs skip anything already installed.
4. Verify
Ask every worker for its status over SSH — each should report
connected and a model count:
# Ask every worker for its status over SSH
ansible pendra_workers -i inventory.ini --become -a "pendra status"
gpu-01.example.com | CHANGED | rc=0 >>
Status ✔ connected
Worker ID wrk-a1b2c3d4
Name gpu-01.example.com
Server wss://api.pendra.ai
Version 3.60.5
Variant cuda
GPU NVIDIA L4
Models (2)
· qwen3.6:27b
· nomic-embed-text
Your machines should also appear under console.pendra.ai → Workers within a few seconds, with the models listed once each download finishes.
Keeping the fleet current
Because the playbook registered the Pendra package repository, worker upgrades are a normal package upgrade — a small second playbook pulls the latest worker onto every host, and the package restarts the running worker onto the new version for you:
# upgrade.yml — pull the latest worker onto the whole fleet
- name: Update Pendra workers
hosts: pendra_workers
become: true
tasks:
- name: Upgrade the worker package
ansible.builtin.package:
name: "{{ 'pendra' if pendra_gpu_variant == 'cpu' else 'pendra-' + pendra_gpu_variant }}"
state: latest
update_cache: true
To change what a fleet serves, edit pendra_models and re-run
pendra.yml — new ids get installed, and everything already
present is left untouched.
Next steps
- Full manual install and the per-OS installers: Install the worker.
- Every config key and env var: Configuration.
- Size the host to the model: System requirements.
- Pick the right variant: Choosing a model size.