If your service is a container that speaks HTTP, it can run on Hydra with
automatic HTTPS on a public *.experiencenet.com domain — no
server to rent, no certificates to manage, no reverse proxy to configure.
Five steps. The whole thing takes about fifteen minutes the first time.
Dockerfile
that serves HTTP - not a binary, AppImage or desktop app) you do not have
to build or launch anything by hand. Push a v* tag to your
watched repo and hydragitwatcher → hydralinuxpipeline builds your
Dockerfile natively on an
arm64 builder, pushes the image to the scale registry, and deploys the scale
over the exec transport — the same result as the five steps below, on every tag.
See Linux pipeline getting started in the onboarding docs
(docs/onboarding/linux-pipeline-getting-started.md).
It is operator-onboarded today; ask to have your repo watched.
Hydra terminates TLS for you at the edge, so your service must serve plain HTTP — do not manage certificates yourself. Build a multi-arch image: the fleet runs on arm64, and a single-arch amd64 image cannot be scheduled at all.
FROM golang:1.25-alpine AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/app
FROM alpine:3.21
RUN apk add --no-cache ca-certificates
COPY --from=build /app /usr/local/bin/app
ENV HYDRA_AUTO_UPDATE=off
EXPOSE 8080
CMD ["app", "serve", "--listen", ":8080"]
See the rules below the steps — a couple of them will bite you if you skip them.
Build for both architectures and push to
scaleregistry.experiencenet.com. In CI this is a
docker buildx build --platform linux/amd64,linux/arm64 --push.
docker buildx build \
--platform linux/amd64,linux/arm64 --push \
-t scaleregistry.experiencenet.com/myapp:v1.0.0 .
A "scale" is your image running as a container on a Hydra node. Attach any persistent state as a disk device on the path your app writes to — never bake data or secrets into the image.
incus launch scaleregistry.experiencenet.com/myapp:v1.0.0 myapp
incus config device add myapp state disk \
source=/srv/scales/myapp path=/root/.myapp shift=true
Label the scale with the domain it should answer on and the container port it listens on. That's all the router needs.
incus config set myapp user.hydra.domain=myapp.experiencenet.com
incus config set myapp user.hydra.port=8080
If your service answers health checks somewhere other than /
(e.g. it's an API with no landing page), also set
user.hydra.health_path=/api/v1/health — otherwise the router
marks it unhealthy and serves nothing.
Create an A record for myapp.experiencenet.com pointing at the
district ingress. Set the TTL to 60 before you change anything, so
cutovers are fast and cheap to reverse.
Within about 30 seconds the router discovers the scale, requests a Let's Encrypt certificate, and starts serving.
https://myapp.experiencenet.com is live with a valid
certificate, health-checked and load-balanced at the edge. Moving it to another
node later changes nothing a user sees — the domain follows the service.
Every one of these was learned the hard way. They are not optional.
TLS terminates at the edge. If your binary's default serve
tries to bind 80/443 and run its own autocert, it will fail inside a
container. Give it an explicit plain-HTTP listen mode.
The fleet is arm64. An amd64-only image cannot be scheduled — the launch
fails outright, not slowly. Every image must contain linux/arm64.
HYDRA_AUTO_UPDATE=off in code, not just in the Dockerfile.
Setting the env var does nothing on its own. Your serve command must
read it and skip the built-in self-updater — otherwise it rewrites its own
binary inside an ephemeral container every few hours and tries to restart a
systemd unit that does not exist.
VOLUME in the Dockerfile.
The OCI runtime cannot satisfy an anonymous volume and the container fails to
start with Failed to mount "none". Attach persistence as an Incus
disk device (step 3) instead.
Attach them as a disk device. Anyone who can pull your image tag can read anything baked into it. This also means a container rebuild or node move loses nothing — the data was never inside.
user.hydra.health_path if you don't serve /.
The router health-checks / by default. An API-only service that
404s on / is marked down and serves 503 — while looking perfectly
healthy to you on its own port.