HydraMancer

Deploy a service

Ship a service to Hydra

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.

Or: git push to deploy (the Linux pipeline). For an OCI container service (a 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.

Containerize it to listen on :8080, plain HTTP

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.

Push the image to the scale registry

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 .

Launch it as a scale on a Hydra node

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

Give it a domain

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.

Point DNS at the district

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.

That's it. 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.

Rules that will save you an afternoon

Every one of these was learned the hard way. They are not optional.

Serve plain HTTP with a --dev / --listen flag; never bind 443.

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.

Build multi-arch. arm64 is not optional.

The fleet is arm64. An amd64-only image cannot be scheduled — the launch fails outright, not slowly. Every image must contain linux/arm64.

Honour 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.

Never declare a 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.

State and secrets live on the host, never in the image.

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.

Set 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.