teslamate: port container from Dagger to Nix (default.nix)

teslamate is not in nixpkgs, so this is a from-scratch beamPackages
mixRelease: an Elixir/Phoenix release with npm-built assets. Replaces
container.py (+ entrypoint.sh, now inlined as the image Entrypoint).

Pins erlang_27 + elixir_1_18 from the shared nixos-unstable rev (teslamate
needs elixir ~> 1.17; stays off the default OTP 28). Source from the forge
mirror, pinned by the v3.0.0 tag commit. Assets build in-release via npm ci
(esbuild + sass are devDeps; esbuild platform binary is optional) + the
custom node scripts/build.js, then mix phx.digest. ex_cldr locale data is
pre-fetched and pointed at via LOCALES to avoid compile-time GitHub
downloads the build sandbox blocks. Version unchanged (v3.0.0). Build
verified on ringtail (exit 0, ~134 MB image).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-06-03 09:09:01 -07:00
commit 39686c8a2e
4 changed files with 125 additions and 128 deletions

View file

@ -1,104 +0,0 @@
"""TeslaMate — Tesla data logger.
Two-stage build: Elixir+Node (builder), Debian slim (runtime).
Source cloned from forge mirror.
"""
import dagger
from dagger import dag
from blumeops.containers import clone_from_forge, oci_labels
VERSION = "v3.0.0"
async def build(src: dagger.Directory) -> dagger.Container:
source = clone_from_forge("teslamate", VERSION)
# Stage 1: Build Elixir release with Node.js assets
builder = (
dag.container()
.from_("elixir:1.19.5-otp-26")
.with_exec(
[
"bash",
"-c",
"apt-get update"
" && apt-get install -y ca-certificates curl gnupg git zstd brotli"
" && mkdir -p /etc/apt/keyrings"
" && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key"
" | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg"
' && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg]'
' https://deb.nodesource.com/node_22.x nodistro main"'
" > /etc/apt/sources.list.d/nodesource.list"
" && apt-get update"
" && apt-get install -y nodejs"
" && apt-get clean"
" && rm -rf /var/lib/apt/lists/*",
]
)
.with_exec(["mix", "local.rebar", "--force"])
.with_exec(["mix", "local.hex", "--force"])
.with_directory("/opt/app", source)
.with_workdir("/opt/app")
.with_env_variable("MIX_ENV", "prod")
.with_exec(["mix", "deps.get", "--only", "prod"])
.with_exec(["mix", "deps.compile"])
.with_exec(
[
"npm",
"ci",
"--prefix",
"./assets",
"--progress=false",
"--no-audit",
"--loglevel=error",
]
)
.with_exec(["mix", "assets.deploy"])
.with_exec(["mix", "compile"])
.with_exec(
["bash", "-c", "SKIP_LOCALE_DOWNLOAD=true mix release --path /opt/built"]
)
)
# Stage 2: Debian slim runtime
entrypoint = src.file("containers/teslamate/entrypoint.sh")
runtime = (
dag.container()
.from_("debian:trixie-slim")
.with_exec(
[
"bash",
"-c",
"apt-get update && apt-get install -y --no-install-recommends"
" libodbc2 libsctp1 libssl3t64 libstdc++6"
" netcat-openbsd tini tzdata"
" && apt-get clean"
" && rm -rf /var/lib/apt/lists/*"
" && groupadd --gid 10001 --system nonroot"
" && useradd --uid 10000 --system --gid nonroot"
" --home-dir /home/nonroot --shell /sbin/nologin nonroot",
]
)
)
runtime = oci_labels(
runtime,
title="TeslaMate",
description="Tesla data logger and visualization",
version=VERSION,
)
return (
runtime.with_env_variable("LANG", "C.UTF-8")
.with_env_variable("SRTM_CACHE", "/opt/app/.srtm_cache")
.with_env_variable("HOME", "/opt/app")
.with_workdir("/opt/app")
.with_directory("/opt/app", builder.directory("/opt/built"), owner="nonroot")
.with_exec(["mkdir", "-p", "/opt/app/.srtm_cache"])
.with_file("/entrypoint.sh", entrypoint, permissions=0o555, owner="nonroot")
.with_user("nonroot")
.with_exposed_port(4000)
.with_entrypoint(["tini", "--", "/bin/dash", "/entrypoint.sh"])
.with_default_args(args=["bin/teslamate", "start"])
)

View file

@ -0,0 +1,116 @@
# Nix-built TeslaMate for ringtail (amd64).
#
# Replaces the Dagger container.py (Elixir+Node builder -> Debian slim).
# TeslaMate is NOT in nixpkgs, so this is a from-scratch beamPackages
# mixRelease: an Elixir/Phoenix release with npm-built assets.
#
# Pinned to the same nixos-unstable rev as paperless/mealie for a
# consistent toolchain. The BEAM combo is pinned to erlang_27 + elixir_1_18
# (teslamate requires elixir ~> 1.17; upstream's image uses OTP 26, so we
# stay off the default OTP 28 which elixir 1.18 does not target).
#
# Source comes from the forge mirror (supply-chain control), pinned by the
# v3.0.0 tag's commit so builtins.fetchGit needs no hash.
let
nixpkgs = fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/331800de5053fcebacf6813adb5db9c9dca22a0c.tar.gz";
sha256 = "1p54fm6dkbq62kpi55cr4wyx7b1nsajpsnjgs64cmp073fwi15f7";
};
pkgs = import nixpkgs { system = "x86_64-linux"; };
lib = pkgs.lib;
version = "3.0.0";
beamPackages = pkgs.beam.packages.erlang_27;
elixir = beamPackages.elixir_1_18;
src = builtins.fetchGit {
url = "https://forge.ops.eblu.me/mirrors/teslamate.git";
ref = "refs/tags/v${version}";
rev = "3281154d42330786a182c1bbe094ecda0b1c5578";
};
# ex_cldr downloads locale JSON from GitHub at compile time, which the
# build sandbox blocks. teslamate's cldr.ex reads the data dir from the
# LOCALES env var; point it at the pre-fetched elixir-cldr data so no
# download is attempted (with SKIP_LOCALE_DOWNLOAD=true disabling the
# forced refresh). CLDR data version matches the compile-time errors.
cldrData = pkgs.fetchFromGitHub {
owner = "elixir-cldr";
repo = "cldr";
rev = "v2.46.0";
sha256 = "1iwzk9dc754l72vpf8vsisdjncnjx26pz509552b6vnm49xbxyji";
};
teslamate = beamPackages.mixRelease {
pname = "teslamate";
inherit version src elixir;
mixFodDeps = beamPackages.fetchMixDeps {
pname = "mix-deps-teslamate";
inherit src version elixir;
hash = "sha256-DDrREiM1BIMgD2qFPTK8QyjOYlnfE3XlnaH/jk7G2go=";
};
# Frontend assets. esbuild + sass are devDeps and the esbuild platform
# binary is an optional dep, so npm ci must include both. We run npm ci
# here (not a separate derivation) because assets/package.json has
# file:../deps/phoenix references that only resolve once mixFodDeps has
# populated deps/. npmConfigHook wires up the offline cache from npmDeps;
# then `node scripts/build.js` (custom esbuild) + `mix phx.digest`.
nativeBuildInputs = [ pkgs.nodejs pkgs.npmHooks.npmConfigHook ];
npmDeps = pkgs.fetchNpmDeps {
name = "teslamate-npm-deps";
src = src + "/assets";
hash = "sha256-XyiaUkT/c4rZnNxmxhVLb+vEXnc64A1hjOrnR5fhaEk=";
};
npmRoot = "assets";
preBuild = ''
export SKIP_LOCALE_DOWNLOAD=true
export LOCALES=${cldrData}/priv/cldr
( cd assets && npm ci --include=dev --include=optional && node scripts/build.js )
mix phx.digest --no-deps-check
'';
};
in
pkgs.dockerTools.buildLayeredImage {
name = "blumeops/teslamate";
contents = [
teslamate
pkgs.bashInteractive
pkgs.coreutils
pkgs.dash
pkgs.netcat-openbsd
pkgs.cacert
pkgs.tzdata
];
config = {
# Mirror entrypoint.sh: wait for postgres, run migrations, then start.
Entrypoint = [
"${pkgs.dash}/bin/dash"
"-c"
''
: "''${DATABASE_HOST:=127.0.0.1}"
: "''${DATABASE_PORT:=5432}"
while ! ${pkgs.netcat-openbsd}/bin/nc -z "$DATABASE_HOST" "$DATABASE_PORT" 2>/dev/null; do
echo "waiting for postgres at $DATABASE_HOST:$DATABASE_PORT"; sleep 1
done
${teslamate}/bin/teslamate eval "TeslaMate.Release.migrate"
exec ${teslamate}/bin/teslamate start
''
];
Env = [
"HOME=/opt/app"
"SRTM_CACHE=/opt/app/.srtm_cache"
"LANG=C.UTF-8"
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
];
ExposedPorts = {
"4000/tcp" = { };
};
};
}

View file

@ -1,23 +0,0 @@
#!/usr/bin/env dash
set -e
: "${DATABASE_HOST:="127.0.0.1"}"
: "${DATABASE_PORT:=5432}"
: "${ULIMIT_MAX_NOFILE:=65536}"
# prevent memory bloat in some misconfigured versions of Docker/containerd
# where the nofiles limit is very large. 0 means don't set it.
if test "${ULIMIT_MAX_NOFILE}" != 0 && test "$(ulimit -n)" -gt "${ULIMIT_MAX_NOFILE}"; then
ulimit -n "${ULIMIT_MAX_NOFILE}"
fi
# wait until Postgres is ready
while ! nc -z "${DATABASE_HOST}" "${DATABASE_PORT}" 2>/dev/null; do
echo waiting for postgres at "${DATABASE_HOST}":"${DATABASE_PORT}"
sleep 1s
done
# apply migrations
bin/teslamate eval "TeslaMate.Release.migrate"
exec "$@"

View file

@ -222,9 +222,17 @@ services:
- name: teslamate
type: argocd
last-reviewed: 2026-04-14
last-reviewed: "2026-06-03"
current-version: "v3.0.0"
upstream-source: https://github.com/teslamate-org/teslamate/releases
notes: >-
Tesla data logger. Container ported from Dagger (container.py) to Nix
(containers/teslamate/default.nix) — a from-scratch beamPackages
mixRelease (Elixir/Phoenix release with npm-built assets), since
teslamate is not in nixpkgs. Pins erlang_27 + elixir_1_18 from the
shared nixos-unstable rev; assets via in-release npm ci + esbuild;
ex_cldr locale data pre-fetched (LOCALES env) to avoid sandbox
downloads. Version unchanged (v3.0.0). Build verified on ringtail.
- name: transmission
type: argocd