Getting started
This page walks through going from nothing to a running service, using
hll the whole way.
Installing hllc
Every merge to main cuts a tagged release with a prebuilt Linux x86-64
hllc binary attached—no Rust toolchain required:
curl -Lo hllc https://github.com/travisboettcher/hl-lang/releases/latest/download/hllc-linux-x86_64
chmod +x hllc
./hllc --version
Put hllc somewhere on your PATH, for example ~/.local/bin, so the
rest of this page can just call hllc directly. Pin to a specific release
tag instead of latest for anything you intend to keep reproducible, such
as CI or a deploy script.
Linux x86-64 is the only platform this project tests or supports—see the main repository’s README. If you’re on a different platform, building from source—also covered in the README—may work, but it’s untested.
Your first service
Create a file called jellyfin.hll:
service jellyfin {
image "jellyfin/jellyfin:latest"
expose 8096
volume "/mnt/media" -> "/data"
env PUID = "1000"
restart unless-stopped
}
This declares one service named jellyfin, running the
jellyfin/jellyfin:latest image, reachable on container port 8096,
with a bind mount, one environment variable, and a restart policy of
unless-stopped.
Reaching it from a hostname is a separate matter, and deliberately not part of the language—see Routing once the basics here make sense.
Compile it:
hllc build jellyfin.hll
With no --out, build prints the generated Compose YAML straight to
your terminal—a Compose services: block for jellyfin, with image,
expose, volumes, environment, restart and networks. Skim
it—the mapping from the .hll fields you wrote to the YAML fields it
produces should be fairly direct.
The first two lines are a # Generated by hllc comment. Compose ignores
it, but it marks the file as compiler output—and hllc refuses to
overwrite a file that doesn’t have it, so pointing build at a
directory can’t quietly eat a docker-compose.yml you wrote by hand
(see the command-line tool page).
To write the result to disk instead of printing it:
hllc build jellyfin.hll --out docker-compose.yml
docker compose -f docker-compose.yml up -d
That’s a complete, deployable service from six lines of hll.
Adding a second service
A single .hll file can declare more than one service, and a real
homelab usually wants several. Add a second service to the same file,
or start a new one—either works, since hllc build treats one input
file as one Compose document that may hold multiple services:
volume uptime-kuma-data {}
service jellyfin {
image "jellyfin/jellyfin:latest"
expose 8096
volume "/mnt/media" -> "/data"
env PUID = "1000"
restart unless-stopped
}
service uptime-kuma {
image "louislam/uptime-kuma:latest"
expose 3001
volume uptime-kuma-data -> "/app/data"
restart unless-stopped
}
uptime-kuma-data names a Docker-managed named volume rather than a
host path, so it needs the top-level volume uptime-kuma-data {}
declaration at the top of the file—the same way a networks [x] entry
needs a network x { ... }. The /mnt/media that jellyfin mounts
starts with a /, which makes it a bind mount, and bind mounts need no
declaration. See volume for the full
rule.
Removing repetition with a template
Both preceding services repeat restart unless-stopped, and a real
homelab tends to repeat far more than that across every service—the same
proxy network, the same routing labels, the same PUID/PGID pair.
That repetition is what template and with are for:
template baseline {
restart unless-stopped
}
volume uptime-kuma-data {}
service jellyfin {
with baseline
image "jellyfin/jellyfin:latest"
expose 8096
volume "/mnt/media" -> "/data"
env PUID = "1000"
}
service uptime-kuma {
with baseline
image "louislam/uptime-kuma:latest"
expose 3001
volume uptime-kuma-data -> "/app/data"
}
Both services now pick up restart unless-stopped from one place. A
template applies only where a with names it, so nothing happens behind
your back and a service can opt out by leaving the line off. Templates
& Composition covers parameters and the
merge rules in full. Imports covers sharing templates
like this across every .hll file in your homelab instead of just
within one file.
Where to go next
- Syntax Basics—how a statement, a body, and the
primary-value shorthand from the preceding example (
expose 8096instead of writing the fullexpose { port: 8096 }body) actually work. - Built-in Fields—every field
hllunderstands, what it accepts, and its defaults. - Routing—getting a hostname to reach one of these
services, with the Traefik templates
hllcships. - The
hllccommand-line tool—building a whole directory of services at once, not just one file.