Pt
Artificial Intelligence (AI) Azure

Piper TTS on Ubuntu 24.04 on Azure User Guide

| Product: Piper TTS on Ubuntu 24.04 LTS on Azure

Overview

Piper is a fast, local neural text-to-speech engine. It turns text into natural-sounding speech entirely on the CPU using ONNX Runtime, with no GPU and no external speech API, so the text you synthesize never leaves your own network. The cloudimg image installs Piper 1.5.0 in a dedicated Python virtual environment, runs its HTTP server as a systemd service bound to loopback, publishes it through an nginx reverse proxy on port 443 with TLS and HTTP Basic authentication, and generates a unique credential on the first boot of every VM. One voice model is included and ready to speak immediately. Backed by 24/7 cloudimg support.

What is included:

  • Piper 1.5.0 (GPL-3.0-or-later), installed unmodified from PyPI into /opt/piper/venv
  • The en_US-ljspeech-medium voice, preinstalled and ready to use (LJ Speech corpus, public domain, trained from scratch)
  • A browser interface and a JSON HTTP API returning 16-bit PCM WAV audio at 22050 Hz
  • The Piper server bound to 127.0.0.1:5000 only, never exposed directly to the network
  • nginx on port 443 terminating TLS with a certificate generated uniquely for each VM, enforcing bcrypt HTTP Basic auth
  • A unique admin password generated on first boot into a root-only credentials file, with no default login shipped
  • The voice-download endpoint disabled, so the server cannot be induced to fetch files from the internet
  • /usr/local/lib/cloudimg/piper-audio-check.py, a self-testing tool that verifies synthesized audio is real and not silence
  • The licence position for the engine and the voice recorded on the VM at /usr/share/doc/cloudimg-piper/PIPER-LICENSE-NOTICE.txt
  • piper.service + nginx.service as systemd units, enabled and active
  • 24/7 cloudimg support

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet + subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a good starting point; Piper is CPU-only, so no GPU VM size is needed or recommended. NSG inbound: allow 22/tcp from your management network and 443/tcp for the API and browser interface.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Piper TTS by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTPS (443). Then Review + create -> Create.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name piper \
  --image <marketplace-image-urn> \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_rsa.pub \
  --public-ip-sku Standard

Then open port 443 to the clients that need it:

az vm open-port --resource-group <your-rg> --name piper --port 443

Step 3 - Confirm the services are running

SSH to the VM and confirm both units are active. Note that Piper itself listens only on 127.0.0.1:5000 - nginx on :443 is the only network-facing surface.

systemctl is-active piper nginx
ss -tln | grep -E ':5000|:443'

Both units report active, and the listener on port 5000 is bound to 127.0.0.1, not to 0.0.0.0.

Terminal showing piper.service and nginx.service both active, with the Piper server bound only to 127.0.0.1 port 5000 while nginx listens on port 443

Step 4 - Retrieve the per-VM credential

A unique password is generated on the first boot of every VM and written to a root-only file. There is no default or shared login.

sudo cat /root/piper-credentials.txt

The file lists the URL, the voice, the username admin and the generated password, together with ready-to-run examples. Confirm the file is readable only by root:

ls -l /root/piper-credentials.txt

Terminal showing the credentials file owned by root with mode 600, its contents with the password masked, the single preinstalled voice model, and the recorded licence position

Step 5 - Open the browser interface

Browse to https://<vm-public-ip>/. The certificate is self-signed and generated uniquely for this VM, so your browser will warn once; accept it, or copy /etc/nginx/tls/piper.crt to your client's trust store. Sign in with admin and the password from Step 4.

The Piper browser interface after signing in, with an empty text box, the Speak button, and a table showing the loaded en_US-ljspeech-medium voice

Type a sentence into the text box.

The Piper browser interface with the sentence "Piper turns text into natural sounding speech, entirely on this machine." entered in the text box, ready to synthesize

Select Speak. Piper synthesizes the sentence and an audio player appears with the generated speech, which begins playing. The interface also reports how long synthesis took.

The Piper browser interface after selecting Speak, showing the audio player playing the generated speech at two seconds of a four second clip, and the message that synthesis completed in 0.295 seconds

The open /health endpoint reports the loaded voice and the server's record of the most recent utterance, which is a quick way to confirm the engine is working without sending any text.

The /health endpoint returning JSON that names the loaded en_US-ljspeech-medium voice and records the synthesis time of the most recent utterance

Step 6 - Synthesize speech from the HTTP API

Send a POST to /synthesize with a JSON body containing the text. The response body is a 16-bit PCM WAV file, mono, 22050 Hz. Run this on the VM itself:

PIPER_PASSWORD=$(sudo grep '^PIPER_PASSWORD=' /root/piper-credentials.txt | cut -d= -f2-)
curl -sS --cacert /etc/nginx/tls/piper.crt -u admin:"$PIPER_PASSWORD" \
  -X POST https://127.0.0.1/synthesize \
  -H 'Content-Type: application/json' \
  -d '{"text":"The quick brown fox jumps over the lazy dog."}' \
  -o /tmp/speech.wav
file /tmp/speech.wav

file reports RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 22050 Hz.

From a client machine, use the VM's public address and the same credential:

curl -u admin:<password> --cacert piper.crt \
  -X POST https://<vm-public-ip>/synthesize \
  -H 'Content-Type: application/json' \
  -d '{"text":"Hello from Piper."}' \
  --output speech.wav

You can also control the speaking rate with length_scale - values above 1.0 are slower, below 1.0 are faster:

PIPER_PASSWORD=$(sudo grep '^PIPER_PASSWORD=' /root/piper-credentials.txt | cut -d= -f2-)
curl -sS --cacert /etc/nginx/tls/piper.crt -u admin:"$PIPER_PASSWORD" \
  -X POST https://127.0.0.1/synthesize \
  -H 'Content-Type: application/json' \
  -d '{"text":"This sentence is spoken more slowly.","length_scale":1.3}' \
  -o /tmp/slow.wav
ls -l /tmp/slow.wav

Step 7 - Verify the audio is real

A zero-length or completely silent WAV is still a structurally valid WAV, so checking the HTTP status code alone does not prove speech was produced. The image ships a checker that inspects the waveform itself - its duration, RMS and peak amplitude:

/usr/local/lib/cloudimg/piper-audio-check.py /tmp/speech.wav \
  --min-duration 1 --min-rms 300 --min-peak 2000

It prints the measured frames, duration, RMS and peak, and exits non-zero if the audio is silent or implausibly short. You can confirm the check is meaningful rather than always passing:

/usr/local/lib/cloudimg/piper-audio-check.py --self-test

The self-test generates digital silence and a real waveform, and passes only if the checker rejects the first and accepts the second.

Terminal showing an authenticated synthesis request returning a valid WAV file, the audio checker reporting a duration of 3.065 seconds with RMS 3871 and peak 32767, and the checker self-test confirming it rejects silence and accepts real audio

Step 8 - Confirm the security posture

The image is closed by default. Anonymous requests, wrong credentials and the voice-download endpoint are all refused, while the health probe stays open for load balancers:

PIPER_PASSWORD=$(sudo grep '^PIPER_PASSWORD=' /root/piper-credentials.txt | cut -d= -f2-)
echo "anonymous  : $(curl -s -o /dev/null -w '%{http_code}' --cacert /etc/nginx/tls/piper.crt -X POST https://127.0.0.1/synthesize -H 'Content-Type: application/json' -d '{"text":"hi"}')"
echo "wrong pass : $(curl -s -o /dev/null -w '%{http_code}' --cacert /etc/nginx/tls/piper.crt -u admin:wrong -X POST https://127.0.0.1/synthesize -H 'Content-Type: application/json' -d '{"text":"hi"}')"
echo "download   : $(curl -s -o /dev/null -w '%{http_code}' --cacert /etc/nginx/tls/piper.crt -u admin:"$PIPER_PASSWORD" -X POST https://127.0.0.1/download -H 'Content-Type: application/json' -d '{"voice":"en_US-lessac-medium"}')"
echo "health     : $(curl -s -o /dev/null -w '%{http_code}' --cacert /etc/nginx/tls/piper.crt https://127.0.0.1/healthz)"

Anonymous synthesis and a wrong password both return 401, the download endpoint returns 403, and the health probe returns 200.

Terminal showing anonymous synthesis refused with HTTP 401, a wrong password refused with 401, the voice download endpoint disabled with 403, and the unauthenticated health probe returning 200

POST /download is deliberately disabled. Upstream it makes the server fetch voice files from the internet on request, which is both a server-side request forgery surface and a way for unvetted voice models to arrive on your disk.

Licence position

Two separate licences apply, and they are recorded on the VM at /usr/share/doc/cloudimg-piper/PIPER-LICENSE-NOTICE.txt:

cat /usr/share/doc/cloudimg-piper/PIPER-LICENSE-NOTICE.txt

The engine. Piper 1.5.0 is licensed GPL-3.0-or-later. Development moved from the original rhasspy/piper repository, which was archived in October 2025, to OHF-Voice/piper1-gpl; the current release is GPL, not MIT. cloudimg ships it unmodified and adds no code into the Piper process - the authentication layer is a separate nginx process reached over loopback - and the on-VM notice includes a written offer for the complete corresponding source.

The voice. This image ships exactly one voice, en_US-ljspeech-medium. It is built from the LJ Speech corpus, which its author states is in the public domain, and its model card records that it was trained from scratch rather than fine-tuned from another model.

That second point matters more than it looks. Most Piper voices are fine-tuned from a small number of base voices, and two of the most common bases carry restrictions: lessac is trained on a corpus licensed for research only, whose terms explicitly exclude commercial speech-synthesis products, and ryan is CC BY-NC-SA 4.0, which is non-commercial. A fine-tuned voice's own model card describes only the fine-tuning data, so a voice can declare a permissive licence while inheriting a restricted base - several voices declare CC0 or Apache-2.0 and are fine-tuned from lessac. The Hugging Face repository tag and voices.json are not reliable here either: the repository carries a single blanket mit tag that the individual model cards contradict, and voices.json has no licence field at all.

Adding another voice

You can install additional voices, but you are responsible for their licensing. Before using any voice commercially, read its MODEL_CARD and follow its "Finetuned from" line back to the root of the training lineage. A permissive licence line on the voice itself is not sufficient.

Voices whose full chain is public domain or permissive include en_US-ljspeech, en_GB-cori, en_US-kristin and en_US-norman. To install one, place both files in the voices directory and point the service at it:

sudo curl -fL -o /var/lib/piper/voices/en_GB-cori-medium.onnx \
  https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_GB/cori/medium/en_GB-cori-medium.onnx
sudo curl -fL -o /var/lib/piper/voices/en_GB-cori-medium.onnx.json \
  https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_GB/cori/medium/en_GB-cori-medium.onnx.json
sudo chown piper:piper /var/lib/piper/voices/en_GB-cori-medium.onnx*
sudo sed -i 's/^VOICE=.*/VOICE=en_GB-cori-medium/' /etc/piper/piper.env
sudo systemctl restart piper.service

Maintenance

Piper and nginx are ordinary systemd units:

systemctl status piper.service --no-pager | head -5

Logs go to the journal:

journalctl -u piper.service --no-pager | tail -5

The image keeps unattended security upgrades enabled for the operating system. To change the served voice, edit VOICE in /etc/piper/piper.env and restart piper.service. To rotate the API password, regenerate the htpasswd entry:

sudo htpasswd -B /etc/nginx/.piper.htpasswd admin
sudo systemctl reload nginx

For a production deployment, replace the self-signed certificate in /etc/nginx/tls/ with one issued for your own hostname and reload nginx.

Support

cloudimg provides 24/7 technical support for this product by email and live chat, covering deployment, the HTTP API, voice selection and voice licensing, TLS and the per-VM credential, and scaling. Email support@cloudimg.co.uk.