What LibreTime runs for a station
LibreTime keeps the schedule so nobody sits at a playout PC. The scheduling guide's workflow is upload, create a show on the calendar, drag tracks into it. Shows repeat weekly or monthly and can be linked so one playlist serves every airing; an autoloading playlist fills a show an hour before air.
| Account type | What it can do, on top of the type above it |
|---|---|
| Guest | View the calendar and the playout log, listen inside the interface |
| DJ | Upload to their own library and schedule it into shows assigned to them |
| Program Manager | Create and assign shows, manage others' libraries, see listener statistics, export the playout log |
| Admin | Manage every account, track types and system settings |
That log export feeds royalty reporting, per the users guide (who gets paid). A station that only needs a stream, a hosted Auto DJ and DJ logins should look at our internet radio plans.
Requirements and ports
| Item | LibreTime figure | Notes |
|---|---|---|
| Processor | 1 GHz | Images are amd64 only, so no ARM line |
| Memory | 1 GB required, 2 GB recommended | Postgres, RabbitMQ, Liquidsoap, PHP |
| Disk | Not stated | Our rule of thumb is twice the library |
| 8000/tcp | Icecast listeners | Published by Docker |
| 8001, 8002/tcp | Live inputs, main and show | Published by Docker |
The machine
Our dedicated streaming servers are physical machines with root and an unshared, unmetered port (1 Gbps on 10 of our 18 lines, 500 Mbps on the rest). Take a blade or rack server; those, and among cartridges only the m510 and m710x, carry an out-of-band console (iLO, iDRAC or a Supermicro BMC). Ready servers go live about 30 minutes after the order is approved; more bays or a chosen RAID level come from the custom build page in 4 to 24 hours.
At the default 256 kbps Ogg, 80% of 1 Gbps carries about 3,100 listeners by our arithmetic, but Icecast as shipped admits 100 (the clients default in Icecast 2.4.4's sample configuration). The backup block raises it through ICECAST_MAX_CLIENTS, which the image's README lists.
Installing release 4.5.0 with Docker Compose
You need Ubuntu 24.04, Docker Engine 28 or newer from Docker's packages, DNS pointing here and a root shell (sudo -i). Firewall first, with apt-get update && apt-get install -y ufw && sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw && ufw allow OpenSSH && ufw --force enable && ufw allow 8000,8001,8002/tcp (OpenSSH opens only port 22; allow your own SSH port first if it differs). Docker-published ports skip ufw on IPv4; the rule covers docker-proxy on [::].
Set H to the hostname and TZN to the station's IANA zone, then paste. It refuses Docker below 28, an unchanged H, or an existing /opt/libretime/.env, so a second paste cannot replace the passwords the database was created with.
H=radio.example.org; TZN=Europe/London; V=4.5.0
v=$(docker version -f '{{.Server.Version}}') && [ "${v%%.*}" -ge 28 ] \
&& [ "$H" != radio.example.org ] && [ ! -e /opt/libretime/.env ] \
&& type envsubst >/dev/null && install -d -m 750 /opt/libretime \
&& cd /opt/libretime && U=https://raw.githubusercontent.com/libretime/libretime/$V \
&& wget -qO docker-compose.yml "$U/docker-compose.yml" \
&& wget -qO config.template.yml "$U/docker/config.template.yml" \
&& sed -i 's/- 8080:8080/- 127.0.0.1:8080:8080/' docker-compose.yml \
&& grep -q 127.0.0.1:8080:8080 docker-compose.yml \
&& install -m 600 /dev/null .env.new && { echo LIBRETIME_VERSION=$V
for k in POSTGRES_PASSWORD RABBITMQ_DEFAULT_PASS ICECAST_SOURCE_PASSWORD \
ICECAST_ADMIN_PASSWORD ICECAST_RELAY_PASSWORD; do echo "$k=$(openssl rand -hex 16)"; done
} > .env.new && ! grep -q '=$' .env.new && mv .env.new .env \
&& install -m 640 -g 1000 /dev/null config.yml \
&& bash -a -c 'source .env; envsubst < config.template.yml > config.yml' \
&& sed -i -e "s|^ public_url:\$| public_url: https://$H|" \
-e "s|^ timezone: UTC\$| timezone: $TZN|" \
-e "s|^ api_key:\$| api_key: $(openssl rand -hex 32)|" \
-e "s|^ secret_key:\$| secret_key: $(openssl rand -hex 32)|" config.yml \
&& [ "$(grep -Ec '^ ((api|secret)_key: [0-9a-f]{64}|public_url: https://.+)$' config.yml)" = 3 ] \
&& docker compose run --rm api libretime-api migrate && docker compose up -d \
&& echo started || echo "STOP - see below"
- A STOP is a guard, missing
envsubst(apt install gettext-base), a failed download or edit, or the migration or start; readdocker compose -f /opt/libretime/docker-compose.yml logs, and if Postgres was slow, rerun the two compose commands. - If a first paste stopped after writing
.env, the guard will refuse a second one; start over (nothing uploaded yet) withF=/opt/libretime/docker-compose.yml; { [ ! -f $F ] || docker compose -f $F down -v; } && rm -r /opt/libretime. The-vwipes the database and storage volumes. - Our changes to the official Docker guide, in full. The web port is bound to 127.0.0.1;
.envis written once at mode 600 and checked for empty values;config.ymlat mode 640, group 1000;public_url,api_key,secret_keyandtimezoneset bysed(the template marks the first three required); files in/opt/libretime. LibreTime's own images run as UID and GID 1000, hence the group;/opt/libretimeis mode 750 and owned by root, so no host login account can reach the file. Two later edits are ours as well, theICECAST_MAX_CLIENTSline in the compose file and a start-at-boot entry in/etc/cron.d, since the compose file sets no restart policy.
A mapping without an address publishes on 0.0.0.0 and [::]; 127.0.0.1 keeps the web UI local, enforced from Docker Engine 28 (moby issue 45610).
First sign-in over SSH, then HTTPS
- After a few seconds,
curl -fsS -o /dev/null http://127.0.0.1:8080/ && echo web-ok || echo "STOP - read the compose logs"on the server should print web-ok. - On your workstation,
ssh -L 18080:127.0.0.1:8080 -N you@SERVER, then open http://localhost:18080.CORSHelper.phpaccepts localhost origins, so this works whilepublic_urlnames your domain. - Log in with the documented default,
admin/admin, and under Settings > Manage Users add yourself as type Admin with a long password. - Log back in as yourself and delete
adminwith the x on its row; the users guide advises against leaving it on an internet-facing server. Fill in Settings > General.
The site file below is the reverse-proxy guide's, with three changes of ours, namely listen [::]:80; added (as in the installer guide), proxy_pass pointed at 127.0.0.1 (localhost may resolve to ::1, where nothing listens), and our file name. Certbot prompts for an email and the terms. The last line adds the start-at-boot cron entry; its log is overwritten on every boot.
H=radio.example.org
[ "$H" != radio.example.org ] && apt-get update && apt-get install -y nginx certbot python3-certbot-nginx \
&& ufw allow 80,443/tcp && echo "packages ok" || echo "STOP - set H, then check apt"
tee /etc/nginx/sites-available/libretime.conf >/dev/null <<'EOF'
server {
listen 80;
listen [::]:80;
server_name HOSTNAME;
client_max_body_size 512M;
client_body_timeout 300s;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_pass http://127.0.0.1:8080/;
}
}
EOF
f=/etc/nginx/sites-available/libretime.conf
[ "$H" != radio.example.org ] && sed -i "s/HOSTNAME/$H/" $f && rm -f /etc/nginx/sites-enabled/default \
&& ln -sf $f /etc/nginx/sites-enabled/ && nginx -t && systemctl reload nginx \
&& certbot --nginx -d "$H" && echo "HTTPS is live" \
|| echo "STOP - nginx test, reload or certificate did not succeed; read the output above"
printf '@reboot root sleep 30; docker compose --file /opt/libretime/docker-compose.yml up -d > /var/log/libretime-start.log 2>&1\n' \
> /etc/cron.d/libretime-start
Live shows and the listener stream
In the live broadcast guide, BUTT connects to port 8002, mount /show, with the DJ's own login; 8001 is the main input. Both harbors are unencrypted by default. Listeners use http://H:8000/main; Icecast cannot be proxied (reverse-proxy guide) and browsers block http audio on https pages, so a secure-site player needs an Icecast TLS socket, not covered here. Our hosted plans print an https listen link.
Listener limit, backups, restore and updates
The block opens with the listener limit (set L; the line is added once). Recreating Icecast drops listeners for a few seconds until they press play again, so run it, and the same lines after an update, at a quiet hour. The backup guide lists configuration, database and file storage. This script keeps the station on air, dumps Postgres from the running container, archives the storage volume and /opt/libretime, checks each part, and prunes sets past 14 days only after a good one exists. A file uploaded mid-run may land in one part only. Copy /var/backups/libretime to another machine yourself.
L=3000; f=/opt/libretime/docker-compose.yml
grep -q ICECAST_MAX_CLIENTS $f || sed -i "/ICECAST_RELAY_PASSWORD:/a\ ICECAST_MAX_CLIENTS: \"$L\"" $f
grep -Eq 'ICECAST_MAX_CLIENTS: "[0-9]+"' $f && docker compose -f $f up -d icecast \
&& echo limit-ok || echo "STOP - limit not set"
install -d -m 700 /var/backups/libretime
tee /usr/local/sbin/libretime-backup.sh >/dev/null <<'EOF'
set -euo pipefail
umask 077
c=/opt/libretime/docker-compose.yml
rm -rf /var/backups/libretime/lt-*.part
b=/var/backups/libretime/lt-$(date +%F-%H%M%S)
mkdir "$b.part"
docker compose -f "$c" exec -T postgres pg_dump -U libretime --no-owner \
--no-privileges libretime | gzip > "$b.part/db.sql.gz"
zcat "$b.part/db.sql.gz" | tail -n 5 | grep -q 'dump complete'
s=$(docker volume inspect -f '{{.Mountpoint}}' libretime_libretime_storage)
tar --numeric-owner -czf "$b.part/storage.tar.gz" -C "$s" . || [ $? -eq 1 ]
tar --numeric-owner -czf "$b.part/config.tar.gz" -C /opt libretime
tar -tzf "$b.part/config.tar.gz" libretime/.env libretime/config.yml >/dev/null
tar -tzf "$b.part/storage.tar.gz" >/dev/null
mv "$b.part" "$b"
[ "${NOPRUNE:-0}" = 1 ] || find /var/backups/libretime -maxdepth 1 \
-name 'lt-*' -mtime +14 -exec rm -r {} +
EOF
printf '20 4 * * * root umask 077; bash /usr/local/sbin/libretime-backup.sh > /var/backups/libretime/last-run.log 2>&1\n' \
> /etc/cron.d/libretime-backup
bash /usr/local/sbin/libretime-backup.sh && ls -l /var/backups/libretime
To restore on the same machine, work as root through these steps.
- Define the variables and check the set and the volume with `c=/opt/libretime/docker-compose.yml; B=/var/backups/libretime/lt-STAMP; A="playout liquidsoap analyzer worker api legacy nginx"; s=$(docker volume inspect libretime_libretime_storage | grep -o '"Mountpoint": "[^"]*' | cut -d'"' -f4); test -d "$s" && gzip -t $B/db.sql.gz && tar -tzf $B/config.tar.gz libretime/.env >/dev/null && tar -tzf $B/storage.tar.gz >/dev/null && echo ok || echo "STOP - set or volume missing, nothing changed"`.
- Snapshot the present state without pruning,
NOPRUNE=1 bash /usr/local/sbin/libretime-backup.sh && ls -d /var/backups/libretime/lt-* | tail -n 1 || echo "STOP - no rollback set, do not continue"; the folder it prints is your rollback set. - Reload the database by running `docker compose -f $c stop $A && docker compose -f $c exec -T postgres dropdb -U libretime libretime && docker compose -f $c exec -T postgres createdb -U libretime -O libretime libretime && (set -o pipefail; gunzip -c $B/db.sql.gz | docker compose -f $c exec -T postgres psql -q -U libretime -v ON_ERROR_STOP=1 libretime) && echo db-ok || echo "STOP - the database may be half back; run steps 3 and 4 with B set to the rollback set"`.
- Put the files back and start with `[ -d "$s" ] && find "$s" -mindepth 1 -delete && tar --numeric-owner -xzf $B/storage.tar.gz -C "$s" && chown -R 1000:1000 "$s" && docker compose -f $c run --rm api libretime-api migrate && docker compose -f $c up -d && echo restored || echo "STOP - the audio may be half back; run steps 3 and 4 with B set to the rollback set, and read the compose logs if the files are back but the stack is down"`.
- Rollback is steps 1, 3 and 4 with
Bon the rollback set; nothing else is set aside.
On a new machine, after Docker and the firewall line, set c and B and run `[ ! -e /opt/libretime ] && tar --numeric-owner -xzf $B/config.tar.gz -C /opt && docker compose -f $c up -d --no-recreate postgres && docker compose -f $c create --no-recreate && echo config-ok || echo "STOP - /opt/libretime exists, or the unpack or start did not succeed". Once docker compose -f $c exec postgres pg_isready -U libretime` says accepting connections, run steps 1, 3 and 4, then the proxy and backup blocks.
To update, read every release note up to the target (some change config.yml), run the backup, and set N to the new tag from the releases page. Prepare with `cd /opt/libretime; if [ -z "$N" ]; then echo "STOP - set N first, nothing changed"; elif grep -qx "LIBRETIME_VERSION=$N" .env; then echo "STOP - already prepared for $N; nothing changed, do not roll back"; else cp -p .env .env.old && cp -p config.yml config.yml.old && cp -p docker-compose.yml docker-compose.yml.old && wget -qO docker-compose.yml.new "https://raw.githubusercontent.com/libretime/libretime/$N/docker-compose.yml" && sed -i 's/- 8080:8080/- 127.0.0.1:8080:8080/' docker-compose.yml.new && grep -q 127.0.0.1:8080:8080 docker-compose.yml.new && mv docker-compose.yml.new docker-compose.yml && sed -i "s/^LIBRETIME_VERSION=.*/LIBRETIME_VERSION=$N/" .env && echo prepared || echo "STOP - not prepared; run the rollback line"; fi. Run the listener-limit lines from the backup block again, since the download replaced the compose file. Then, with c and A as in restore step 1, if docker compose -f $c pull; then docker compose -f $c stop $A && docker compose -f $c run --rm api libretime-api migrate && docker compose -f $c up -d && echo update-ok || echo "STOP - run the rollback line, then restore steps 1, 3 and 4 from the pre-update set; the restore starts the station"; else echo "STOP - images not pulled, nothing changed on air; run the rollback line"; fi`. Pulling first limits the silence to the migration.
The rollback line is `cd /opt/libretime && test -f .env.old && test -f config.yml.old && grep -q 127.0.0.1:8080:8080 docker-compose.yml.old && cp -p .env.old .env && cp -p config.yml.old config.yml && cp -p docker-compose.yml.old docker-compose.yml && echo rolled-back || echo "STOP - a set-aside copy is missing; if prepare stopped before copying, nothing needs rolling back". After a good update, delete the three .old` files.
Licence terms
LibreTime's LICENSE file is the GNU Affero General Public License, version 3. Our general reading, which is not legal advice, is that running it for your own station owes nobody anything. Distribute copies, modified or verbatim, and the source goes with them; run a modified build that people use over the network, and they are owed its source too.
Questions
Does LibreTime run on Ubuntu 24.04?
Through Docker, yes; the 4.5.0 install script accepts only Ubuntu 20.04 and Debian 11.
Can DJs broadcast from home?
Yes. Any Icecast-capable encoder, BUTT or Mixxx for example, connects to port 8002, mount /show, with the DJ's own login. Without harbor TLS that login is unencrypted, so it should not be reused elsewhere.
Which ports does a LibreTime station open?
8000 for listeners, 8001 and 8002 for live DJs, and 80 and 443 for the web interface through nginx.