Karapace on Ubuntu 24.04 on Azure User Guide
Overview
Karapace is an open source implementation of the Schema Registry and Kafka REST Proxy APIs. The schema registry gives your event streams a central, versioned contract: producers and consumers store Avro, JSON Schema and Protobuf schemas under a subject, every schema keeps its own version history, and the registry checks each proposed change against a compatibility rule so a breaking change is caught before it reaches production rather than after a consumer has already failed on it. The REST proxy exposes the same cluster over plain HTTP, so any client that can make a web request can list topics, produce messages and consume them without a native Kafka driver.
Karapace is a registry and a proxy, not a store: its entire persistence layer is a compacted Kafka topic called _schemas. On its own it would start and then fail every real operation, so this cloudimg image bundles a broker with it: a single node Apache Kafka 4.3.1 broker running in KRaft mode, with no ZooKeeper, already wired up as Karapace's storage. The result is a complete, self contained schema registry appliance that is useful the moment the instance boots. If you already run a Kafka cluster, Step 9 shows how to point the registry at it instead.
The image ships Karapace 6.2.1 behind an nginx reverse proxy on a hardened, fully patched Ubuntu 24.04 LTS base. Karapace has no authentication of its own in this configuration, and an open schema registry lets anyone read, overwrite or delete every schema your producers and consumers depend on, so both APIs are protected with HTTP Basic authentication whose password is generated uniquely on the first boot of every VM. Both Karapace processes and the Kafka broker are bound to the loopback interface, so that authentication cannot be bypassed. Backed by 24/7 cloudimg support.
What is included:
- Karapace 6.2.1 serving the Schema Registry API on port 80 and the Kafka REST Proxy API on port 8082, both through nginx
- A bundled single node Apache Kafka 4.3.1 broker in KRaft mode, formatted fresh on each VM's first boot
- HTTP Basic authentication on both APIs, with a per VM password generated on first boot and recorded in a root only file
- No shipped default login: the password file is locked in the image, so no known or blank credential authenticates
- The Kafka broker bound to
127.0.0.1only (ports 9092 and 9093), never exposed to the network - The Karapace registry (8081) and REST proxy (8083) bound to
127.0.0.1only, reachable solely through the authenticated nginx front kafka.service,karapace-registry.service,karapace-rest.serviceandnginx.serviceas enabled systemd units- An unauthenticated
/healthzendpoint for load balancer probes - No compiler or build toolchain in the shipped image
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is what the image is tuned for and is sufficient for the bundled broker plus both Karapace services; size up for heavier REST proxy workloads or larger schema volumes. NSG inbound: allow 22/tcp from your management network, 80/tcp for the schema registry API, and 8082/tcp if you intend to use the REST proxy. Karapace serves plain HTTP; for production, terminate TLS in front of it with your own domain. The bundled Kafka broker is never exposed: it listens on 127.0.0.1 only, so ports 9092 and 9093 stay off the network.
Step 1 - Deploy the image
Option A: Azure portal
- In the Azure portal, open Create a resource and search for Karapace on Ubuntu 24.04 LTS by cloudimg.
- Select the plan and click Create.
- On the Basics tab, choose your subscription, resource group and region, name the VM, and select Standard_B2s.
- Set Authentication type to SSH public key, with azureuser as the username, and provide your public key.
- On the Networking tab, allow inbound
22/tcp(from your IP),80/tcp, and8082/tcpif you need the REST proxy. - Review and create. When deployment completes, note the VM's public IP address.
Option B: Azure CLI
az vm create \
--resource-group my-karapace-rg \
--name karapace \
--image cloudimg:karapace-ubuntu-24-04:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
az vm open-port --resource-group my-karapace-rg --name karapace --port 80 --priority 900
az vm open-port --resource-group my-karapace-rg --name karapace --port 8082 --priority 910
Step 2 - Confirm the services are running
SSH in as azureuser and confirm the four systemd units are active. kafka.service is the bundled broker, karapace-registry.service is the schema registry, karapace-rest.service is the Kafka REST proxy, and nginx.service is the authenticated reverse proxy.
systemctl is-active kafka.service karapace-registry.service karapace-rest.service nginx.service
Every service reports active.
The listener layout is the core of the security model: only nginx is on a public interface, and everything behind it is loopback only.
ss -lnt | grep -E ':(80|8082|8081|8083|9092|9093)\b'
You will see nginx listening on 0.0.0.0:80 and 0.0.0.0:8082, while the Karapace registry (127.0.0.1:8081), the Karapace REST proxy (127.0.0.1:8083) and the Kafka broker (127.0.0.1:9092 and 127.0.0.1:9093) are all bound to loopback. The broker's listener may be shown in the IPv4 mapped form [::ffff:127.0.0.1]:9092, which is the same loopback address.
An unauthenticated health endpoint is available for load balancer probes:
curl -s -o /dev/null -w 'GET /healthz -> HTTP %{http_code}\n' http://127.0.0.1/healthz
This returns HTTP 200.
The registry also exposes its own readiness detail. Note that this endpoint answers 200 with "healthy":true from the moment the process starts, while schema_registry_ready stays false until it has finished replaying the _schemas topic and elected itself primary. Writes only succeed once both inner flags are true, which normally takes a few seconds after boot:
curl -s http://127.0.0.1:8081/_health | python3 -m json.tool | head -12

Step 3 - Retrieve the per VM password
Both APIs require HTTP Basic authentication. The username is karapace and the password was generated on this VM during its first boot. It is stored in a root only file, and no other VM shares it.
sudo cat /root/karapace-info.txt
The file records the registry URL, the REST proxy URL, the username and the generated password. The file is mode 600 and owned by root:
stat -c '%n mode=%a owner=%U:%G' /root/karapace-info.txt
Confirm the secure by default posture. Without credentials, and with a wrong password, the registry refuses the request; with the per VM password it answers:
KPASS=$(sudo awk -F= '/^karapace_password=/{print $2; exit}' /root/karapace-info.txt)
curl -s -o /dev/null -w 'no credentials -> HTTP %{http_code}\n' http://127.0.0.1/subjects
curl -s -o /dev/null -w 'wrong password -> HTTP %{http_code}\n' -u karapace:wrong-password http://127.0.0.1/subjects
curl -s -o /dev/null -w 'per VM password -> HTTP %{http_code}\n' -u "karapace:$KPASS" http://127.0.0.1/subjects
The first two return HTTP 401 and the third returns HTTP 200.

From your workstation, use the VM's public IP address in place of 127.0.0.1, for example curl -u karapace:<karapace-password> http://<vm-public-ip>/subjects.
Step 4 - Register your first schema
A subject is the namespace a schema lives under. The common convention is <topic>-value for the message value and <topic>-key for the key. Register an Avro schema for a payments topic:
KPASS=$(sudo awk -F= '/^karapace_password=/{print $2; exit}' /root/karapace-info.txt)
curl -s -u "karapace:$KPASS" -X POST \
-H 'Content-Type: application/vnd.schemaregistry.v1+json' \
--data '{"schema": "{\"type\":\"record\",\"name\":\"Payment\",\"fields\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"double\"}]}"}' \
http://127.0.0.1/subjects/payments-value/versions
The registry replies with the global identifier it assigned to the schema, for example {"id":1}. Registering the identical schema again is idempotent and returns the same id.
Step 5 - Read schemas back
List every subject the registry knows about:
KPASS=$(sudo awk -F= '/^karapace_password=/{print $2; exit}' /root/karapace-info.txt)
curl -s -u "karapace:$KPASS" http://127.0.0.1/subjects
Read the latest version of a subject, which returns the subject, its version, the schema id and the schema itself:
KPASS=$(sudo awk -F= '/^karapace_password=/{print $2; exit}' /root/karapace-info.txt)
curl -s -u "karapace:$KPASS" http://127.0.0.1/subjects/payments-value/versions/latest
List the versions of a subject, and fetch a schema directly by its global id:
KPASS=$(sudo awk -F= '/^karapace_password=/{print $2; exit}' /root/karapace-info.txt)
curl -s -u "karapace:$KPASS" http://127.0.0.1/subjects/payments-value/versions
curl -s -u "karapace:$KPASS" http://127.0.0.1/schemas/ids/1
Step 6 - Check compatibility before you break a consumer
This is the reason a schema registry exists. Each subject is governed by a compatibility rule, and the registry evaluates a proposed schema against the existing version before you ever deploy it. The image default is BACKWARD, meaning a consumer using the new schema must still be able to read data written with the old one.
Check the current rule:
KPASS=$(sudo awk -F= '/^karapace_password=/{print $2; exit}' /root/karapace-info.txt)
curl -s -u "karapace:$KPASS" http://127.0.0.1/config
Adding a field with a default is backward compatible, because a consumer reading old data simply uses the default:
KPASS=$(sudo awk -F= '/^karapace_password=/{print $2; exit}' /root/karapace-info.txt)
curl -s -u "karapace:$KPASS" -X POST \
-H 'Content-Type: application/vnd.schemaregistry.v1+json' \
--data '{"schema": "{\"type\":\"record\",\"name\":\"Payment\",\"fields\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"double\"},{\"name\":\"currency\",\"type\":\"string\",\"default\":\"GBP\"}]}"}' \
http://127.0.0.1/compatibility/subjects/payments-value/versions/latest
This returns {"is_compatible":true}.
Adding a field without a default is not backward compatible, because a consumer using the new schema has no way to populate that field when it reads data written before the field existed. The registry catches it:
KPASS=$(sudo awk -F= '/^karapace_password=/{print $2; exit}' /root/karapace-info.txt)
curl -s -u "karapace:$KPASS" -X POST \
-H 'Content-Type: application/vnd.schemaregistry.v1+json' \
--data '{"schema": "{\"type\":\"record\",\"name\":\"Payment\",\"fields\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"double\"},{\"name\":\"reference\",\"type\":\"string\"}]}"}' \
http://127.0.0.1/compatibility/subjects/payments-value/versions/latest
This returns {"is_compatible":false} along with the offending field name, reference. That is the check you run in CI before a producer change is merged.
It is worth knowing which direction each rule protects, because the intuition is easy to get backwards. Under BACKWARD, removing a field is allowed, since a consumer on the new schema simply ignores the extra data it finds in old records; it is adding a mandatory field that breaks, as shown above. Under FORWARD the reverse holds. Use FULL when you need both to be safe.
To change the rule, for a single subject or globally, PUT a new compatibility level. Valid values are BACKWARD, BACKWARD_TRANSITIVE, FORWARD, FORWARD_TRANSITIVE, FULL, FULL_TRANSITIVE and NONE:
KPASS=$(sudo awk -F= '/^karapace_password=/{print $2; exit}' /root/karapace-info.txt)
curl -s -u "karapace:$KPASS" -X PUT \
-H 'Content-Type: application/vnd.schemaregistry.v1+json' \
--data '{"compatibility": "FULL"}' \
http://127.0.0.1/config/payments-value

Step 7 - Use the Kafka REST proxy
The REST proxy on port 8082 exposes the bundled broker over plain HTTP, so a client with no native Kafka driver can still work with topics. List the topics:
KPASS=$(sudo awk -F= '/^karapace_password=/{print $2; exit}' /root/karapace-info.txt)
curl -s -u "karapace:$KPASS" http://127.0.0.1:8082/topics
Inspect a single topic's partitions:
KPASS=$(sudo awk -F= '/^karapace_password=/{print $2; exit}' /root/karapace-info.txt)
curl -s -u "karapace:$KPASS" http://127.0.0.1:8082/topics/_schemas
To produce a message, the topic must already exist: the REST proxy does not create it for you, and producing to an unknown topic returns {"error_code":40401,"message":"Topic payments not found"}. Create it first with the bundled Kafka tools:
/opt/kafka/bin/kafka-topics.sh --bootstrap-server 127.0.0.1:9092 \
--create --if-not-exists --topic payments --partitions 1 --replication-factor 1
Now produce a JSON message to it:
KPASS=$(sudo awk -F= '/^karapace_password=/{print $2; exit}' /root/karapace-info.txt)
curl -s -u "karapace:$KPASS" -X POST \
-H 'Content-Type: application/vnd.kafka.json.v2+json' \
--data '{"records":[{"value":{"id":"pay-001","amount":42.5}}]}' \
http://127.0.0.1:8082/topics/payments
The response reports the partition and offset each record landed at:
{"key_schema_id":null,"offsets":[{"offset":0,"partition":0}],"value_schema_id":null}

Step 8 - Work with the bundled broker from the command line
The standard Apache Kafka tools are installed at /opt/kafka/bin and point at the loopback broker. Confirm that the registry's data really is a Kafka topic:
/opt/kafka/bin/kafka-topics.sh --bootstrap-server 127.0.0.1:9092 --list
_schemas is the compacted topic Karapace stores every schema in. Describe it:
/opt/kafka/bin/kafka-topics.sh --bootstrap-server 127.0.0.1:9092 --describe --topic _schemas
Create your own topic:
/opt/kafka/bin/kafka-topics.sh --bootstrap-server 127.0.0.1:9092 \
--create --if-not-exists --topic orders --partitions 3 --replication-factor 1
Because the broker is single node, the replication factor must be 1.
Step 9 - Point Karapace at an external Kafka cluster
The bundled broker makes the image self contained, but Karapace is equally happy talking to a cluster you already run. Karapace is configured entirely through KARAPACE_ prefixed environment variables in its systemd units.
Rather than editing the shipped unit files, add a systemd drop-in for each service, which survives future package changes. Create /etc/systemd/system/karapace-registry.service.d/external-kafka.conf with your own bootstrap address:
[Service]
Environment=KARAPACE_BOOTSTRAP_URI=kafka-1.example.internal:9092,kafka-2.example.internal:9092
Environment=KARAPACE_REPLICATION_FACTOR=3
Raising KARAPACE_REPLICATION_FACTOR to 3 matters on a real cluster: it is the replication factor Karapace uses when it creates the _schemas topic, and a single replica would put your entire schema history on one broker. Create the matching /etc/systemd/system/karapace-rest.service.d/external-kafka.conf with the same KARAPACE_BOOTSTRAP_URI line.
If your cluster requires TLS or SASL, set KARAPACE_SECURITY_PROTOCOL (for example SSL or SASL_SSL) together with the matching KARAPACE_SSL_CAFILE, KARAPACE_SSL_CERTFILE, KARAPACE_SSL_KEYFILE, or KARAPACE_SASL_MECHANISM, KARAPACE_SASL_PLAIN_USERNAME and KARAPACE_SASL_PLAIN_PASSWORD variables in the same drop-in.
Apply the change, then confirm the registry reports itself ready against the new cluster:
sudo systemctl daemon-reload
sudo systemctl restart karapace-registry.service karapace-rest.service
curl -s http://127.0.0.1:8081/_health | python3 -m json.tool | head -12
Once you are using an external cluster, the bundled broker is redundant and can be switched off to free its memory:
sudo systemctl disable --now kafka.service
Step 10 - Security model
- No shipped credential. The nginx password file is locked in the image, so no known or blank password authenticates. On each VM's first boot,
karapace-firstboot.servicegenerates a unique password, writes it as a bcrypt hash into/etc/nginx/karapace.htpasswd, and records it in/root/karapace-info.txtwith mode600. Because the locked file is the shipped state, the appliance is fail closed: if first boot has not completed, every request is refused rather than served anonymously. - Authentication cannot be bypassed. Karapace serves both APIs with no authentication of its own here, so both processes are bound to
127.0.0.1. The only way in is through nginx, which requires HTTP Basic authentication on every path except/healthz. - The broker is never exposed. The bundled Kafka broker listens on
127.0.0.1:9092and127.0.0.1:9093only. Its sole client is Karapace on the same host, which is what makes an unauthenticated PLAINTEXT broker safe here. Do not change its listeners to a public address without adding Kafka level authentication. - Each VM has its own cluster. The image ships with the KRaft storage wiped, so every VM formats a fresh, empty cluster on first boot. No two deployments share a cluster identity, and no schemas from the build ever reach a customer instance.
- No build toolchain. Karapace is built from source at image build time, which needs a Go and C toolchain; both are removed from the image afterwards, so no compiler ships to customers.
- Rotate the password at any time.
htpasswd -Bprompts for the new password twice and writes a bcrypt hash; reload nginx to pick it up, then record the new password somewhere safe, because/root/karapace-info.txtstill shows the original:
sudo htpasswd -B /etc/nginx/karapace.htpasswd karapace
sudo systemctl reload nginx
- Put TLS in front. Karapace serves plain HTTP. For anything beyond a trusted network, terminate TLS at nginx with your own certificate, or place the VM behind an Azure Application Gateway or Load Balancer that does.
Step 11 - Base image and versions
/opt/karapace/venv/bin/python -c 'from karapace.version import __version__; print("Karapace", __version__)'
ls -d /opt/kafka_* && java -version 2>&1 | head -1
The base is fully patched at build time, with unattended security upgrades left enabled for the life of the instance. Confirm there is nothing held back:
apt-mark showhold | wc -l
This prints 0.
Managing the services
systemctl status kafka.service karapace-registry.service karapace-rest.service nginx.service --no-pager
Follow the registry's log:
journalctl -u karapace-registry.service -n 50 --no-pager
The services start automatically on boot. karapace-firstboot.service runs once, before the others, and is skipped on later boots because it leaves a sentinel at /var/lib/cloudimg/karapace-firstboot.done.
Support
This image is published and supported by cloudimg with 24/7 support. It is not affiliated with or endorsed by Aiven or the Karapace project; Karapace and Apache Kafka are used here under the Apache License 2.0, and their names are used only to identify the software included in this image.
For help with this image, contact support@cloudimg.co.uk.