feat/docker-deploy #105

Merged
sickday merged 5 commits from feat/docker-deploy into main 2026-08-02 18:54:38 +00:00
Owner
No description provided.
`initial/2` built its trailing padding with a local call inside a bitstring
size, `<<body::bitstring, 0::size(padding(body))>>`. Elixir 1.20 accepts an
arbitrary expression there; 1.18 restricts `size/1` to guard-safe ones and
rejects it, reporting an undefined `padding/1` and an unused `padding/1` in
the same breath.

mix.exs has declared `~> 1.18` throughout, so the tree has not built at its
own stated minimum. Nothing local could have caught it: every machine here
runs 1.20, and the first one to compile at 1.18 was a release builder.

Hoisting the call to a variable is behaviour-identical and compiles on both.
The other two `::size(...)` sites take arithmetic over variables, which is
guard-safe and needed no change.
A two-stage image — a Debian bookworm builder for the four C dependencies
(bcrypt, exqlite, crc, bzip2) and a hardened distroless runtime carrying
ERTS, the release and libbz2 and nothing else. 104 MB, no Mix, no Elixir,
no toolchain, uid 65532. The cache is never in it: operators supply their
own and it is bind-mounted read-only, as the BYO-cache rule requires.

The larger half of this is a config restructure, and it was not optional.
`config.exs` read the environment with `System.get_env/2`, and that file is
evaluated when the tree is BUILT — inside the builder container, for a
release — so every value would have been frozen into the image and no
`docker run -e` could have moved it. `config/runtime.exs` is now the one
file that touches the environment, evaluated at boot under `mix run`,
`mix test` and the release alike. Two things stay behind on purpose:
`config_env()`, and the database adapter, which `use Ecto.Repo` bakes into
generated code and which is therefore a build ARG.

That move repaired three faults that had nothing to do with containers:

  * the SQLite default expanded to `Path.expand("../priv/repository/…")`,
    which under a release is the BUILDER's source tree — a path absent from
    the image;
  * `pool_size` was `System.schedulers_online()` evaluated at build, baking
    the build machine's core count into the deployment;
  * a variable set to the empty string was not the same as an unset one,
    and Compose substitutes an unset `${VAR:-}` to `""` rather than
    omitting the key — so an optional `REVENANT_ADMIN_PORT` would have
    reached `String.to_integer/1` and taken the boot down. Blank now counts
    as absent everywhere.

`.env` is read by the server itself as well as by Compose, so a bare-metal
run behaves like a container. The real environment always wins over the
file.

Eight `config.exs` blocks setting keys nothing in this project reads are
gone — those variables are read by the converter in its own project. Their
prose is kept as a comment block beside the tables they produce.

`elixirc_paths/1` no longer compiles `lib/mix` outside dev and test. A Mix
task has no business in a release: `:mix` is not an application it carries,
and the bench task calls `:eprof`, which `profiling_apps/1` keeps out of
:prod deliberately and which warned on every release build.

No migration step is sequenced anywhere, because `Database.Migrator` brings
the schema current at boot. The panel is off unless `REVENANT_ADMIN_PORT`
names a port, binds 0.0.0.0 inside the container because Docker forwards a
published port to eth0 rather than loopback, and is published to the HOST
loopback only — an SSH tunnel is how it is reached.

The forge builds and pushes the image when main advances. What a green run
there proves is narrower than it looks and the workflow says so: the image
is never started, because without a cache the application does not boot and
no workflow file may name a place to get one.

Verified by building and running it: the world comes up against the real
rev-149 cache — collision decoded, 17,906 NPCs, listener on 43594 — the
panel answers 401 without a credential, 401 with a wrong one and 200 with
the right one, the schema is created in the mounted volume, and SIGTERM
unwinds the tree in 1.3s.

One trap is recorded in the compose file because it cost a boot: a bind
mount copies a symlink AS a symlink and resolves it inside the container,
so a linked cache directory arrives dangling and the server reports "No
index file(s) present" having found the directory perfectly well.
The panel used to open no socket unless REVENANT_ADMIN_PORT named one, which
meant the ordinary case — an operator who wants to see their world — required
knowing a variable existed before there was anywhere to read about it.

It now serves on 4001 bound to the loopback address unless told otherwise.
What makes that safe is not the port but the two gates already around it, and
neither of them was weakened: nothing off the machine can reach 127.0.0.1,
and `Contract.AdminAuth` fails CLOSED, so an operator who has set no
credentials is serving 503 rather than serving the world. Reaching it from
elsewhere still takes two deliberate acts, a bind address and a credential,
and neither has a default that opens anything.

The off switch is a word — `off`, `none`, `false` or `disabled`, matched
case-insensitively — because every number is a valid port and `0` already
means "ask the operating system for a free one", which is what the suite
uses.

`enabled?` stays `false` in config.exs. That is the value in force before
anything has decided, so a configuration reaching the supervisor without
runtime.exs having run still opens nothing; failing closed is this
subsystem's whole posture and its default should not be the exception.

:test is unaffected — runtime.exs has always exempted it, and test.exs pins
port 0 so the suite never fights a dev server for a real one.
feat: update example with admin port
All checks were successful
ci / gates (pull_request) Successful in 47s
ce333f4285
Merge branch 'main' into feat/docker-deploy
All checks were successful
ci / gates (pull_request) Successful in 47s
ci / gates (push) Successful in 47s
build / image (push) Successful in 1m33s
edbc97253b
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Revenant/Server!105
No description provided.