Wave 1 indri→ringtail migration: paperless, teslamate, mealie (#363)

Migrate paperless, teslamate, and mealie off the OOM-saturated minikube-indri node onto ringtail k3s, shedding ~1.1 GiB of resident load. Second chain in the indri-k8s decommission after immich.

**Containers ported to Nix (default.nix), build-verified on ringtail:**
- paperless → wraps nixpkgs paperless-ngx 2.20.15 (pinned unstable); runs as web/worker/beat/consumer
- mealie → wraps nixpkgs mealie 3.16.0 (forward 4-minor bump, breaking-change reviewed); single gunicorn, SQLite
- teslamate → from-scratch beamPackages mixRelease (not in nixpkgs); erlang_27+elixir_1_18, npm assets, ex_cldr locales pre-fetched

**Data:** cold downtime-tolerant cutover. paperless+teslamate postgres dump/restore from quiesced source into a new ringtail blumeops-pg CNPG cluster; mealie SQLite PVC copied. Source DBs untouched until verified (rollback = repoint).

**Also:** ringtail blumeops-pg cluster + ExternalSecrets scaffold; fixes pre-existing shower version-check drift.

Runbook: docs/how-to/ringtail/migrate-wave1-ringtail.md. Deploy-from-branch + cutover happens before merge; container images rebuilt from main after merge.
Reviewed-on: #363
This commit is contained in:
Erich Blume 2026-06-03 10:34:00 -07:00
commit fcac8e5a72
45 changed files with 1422 additions and 445 deletions

View file

@ -1,145 +0,0 @@
# Mealie — self-hosted recipe manager
# Built from source via forge mirror of mealie-recipes/mealie
# Based on upstream docker/Dockerfile (multi-stage: Node frontend + Python backend)
ARG CONTAINER_APP_VERSION=v3.12.0
###############################################
# Frontend Build
###############################################
FROM node:24-slim AS frontend-builder
ARG CONTAINER_APP_VERSION
RUN apt-get update && apt-get install --no-install-recommends -y git ca-certificates && rm -rf /var/lib/apt/lists/*
RUN git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \
https://forge.ops.eblu.me/mirrors/mealie.git /src
WORKDIR /src/frontend
RUN yarn install \
--prefer-offline \
--frozen-lockfile \
--non-interactive \
--production=false \
--network-timeout 1000000
RUN yarn generate
###############################################
# Python Base
###############################################
FROM python:3.12-slim AS python-base
ENV MEALIE_HOME="/app"
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
VENV_PATH="/opt/mealie"
ENV PATH="$VENV_PATH/bin:$PATH"
RUN useradd -u 911 -U -d $MEALIE_HOME -s /bin/bash abc \
&& usermod -G users abc \
&& mkdir $MEALIE_HOME
###############################################
# Backend Package Build
###############################################
FROM python-base AS backend-builder
ARG CONTAINER_APP_VERSION
RUN apt-get update \
&& apt-get install --no-install-recommends -y curl git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN pip install uv
RUN git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \
https://forge.ops.eblu.me/mirrors/mealie.git /src
WORKDIR /src
COPY --from=frontend-builder /src/frontend/dist ./mealie/frontend
RUN uv build --out-dir dist
RUN uv export --no-editable --no-emit-project --extra pgsql --format requirements-txt --output-file dist/requirements.txt \
&& MEALIE_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") \
&& echo "mealie[pgsql]==${MEALIE_VERSION} \\" >> dist/requirements.txt \
&& pip hash dist/mealie-${MEALIE_VERSION}-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt \
&& echo " \\" >> dist/requirements.txt \
&& pip hash dist/mealie-${MEALIE_VERSION}.tar.gz | tail -n1 >> dist/requirements.txt
###############################################
# Python Venv Build
###############################################
FROM python-base AS venv-builder
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
build-essential \
libpq-dev \
libwebp-dev \
ffmpeg \
libsasl2-dev libldap2-dev libssl-dev \
gnupg gnupg2 gnupg1 \
&& rm -rf /var/lib/apt/lists/*
RUN python3 -m venv --upgrade-deps $VENV_PATH
COPY --from=backend-builder /src/dist /dist
RUN . $VENV_PATH/bin/activate \
&& pip install --require-hashes -r /dist/requirements.txt --find-links /dist
###############################################
# Production Image
###############################################
FROM python-base AS production
ENV PRODUCTION=true
ENV TESTING=false
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
curl \
ffmpeg \
gosu \
iproute2 \
libldap-common \
libldap2 \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /run/secrets
COPY --from=venv-builder $VENV_PATH $VENV_PATH
ENV NLTK_DATA="/nltk_data/"
RUN mkdir -p $NLTK_DATA
RUN python -m nltk.downloader -d $NLTK_DATA averaged_perceptron_tagger_eng
VOLUME ["$MEALIE_HOME/data/"]
ENV APP_PORT=9000
EXPOSE ${APP_PORT}
COPY --from=backend-builder /src/docker/healthcheck.sh $MEALIE_HOME/healthcheck.sh
RUN chmod +x $MEALIE_HOME/healthcheck.sh
HEALTHCHECK CMD $MEALIE_HOME/healthcheck.sh
ENV HOST=0.0.0.0
COPY --from=backend-builder /src/docker/entry.sh $MEALIE_HOME/run.sh
RUN chmod +x $MEALIE_HOME/run.sh
ARG CONTAINER_APP_VERSION
LABEL org.opencontainers.image.title="Mealie"
LABEL org.opencontainers.image.description="Self-hosted recipe manager"
LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}"
LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops"
LABEL org.opencontainers.image.vendor="blumeops"
ENTRYPOINT ["/app/run.sh"]

View file

@ -0,0 +1,65 @@
# Nix-built Mealie for ringtail (amd64).
#
# Replaces the from-source Dockerfile build (Node frontend + Python venv)
# with nixpkgs' mealie, which ships a single `mealie` gunicorn entrypoint
# serving the prebuilt frontend + backend — so this is a clean single-
# process wrap (unlike paperless, which is multi-process).
#
# Mealie stores its DB as SQLite under DATA_DIR (the mealie-data PVC at
# /app/data); there is no postgres. The run wrapper mirrors the nixpkgs
# mealie NixOS module: run `libexec/init_db` (Alembic migrations) first,
# then exec gunicorn.
#
# Self-pins nixos-unstable: stable nixpkgs lags at 3.9.2, unstable carries
# 3.16.0. This is a forward 4-minor bump from the v3.12.0 Dockerfile build
# (the deferred upgrade) — mealie auto-migrates the SQLite DB forward on
# startup via init_db; the source PVC is retained for rollback. The version
# assertion makes nix-build fail if a pin bump changes the version.
let
nixpkgs = fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/331800de5053fcebacf6813adb5db9c9dca22a0c.tar.gz";
sha256 = "1p54fm6dkbq62kpi55cr4wyx7b1nsajpsnjgs64cmp073fwi15f7";
};
pkgs = import nixpkgs { system = "x86_64-linux"; };
version = "3.16.0";
app = pkgs.mealie;
# Mirror the NixOS module's mealie service: init_db (Alembic) then
# gunicorn bound to the app port. DATA_DIR/env come from the image +
# k8s manifest.
mealie-run = pkgs.writeShellScriptBin "mealie-run" ''
set -e
${app}/libexec/init_db
exec ${pkgs.lib.getExe app} -b 0.0.0.0:9000
'';
in
assert app.version == version;
pkgs.dockerTools.buildLayeredImage {
name = "blumeops/mealie";
contents = [
app
mealie-run
pkgs.bashInteractive
pkgs.coreutils
pkgs.cacert
pkgs.tzdata
];
config = {
Cmd = [ "${mealie-run}/bin/mealie-run" ];
Env = [
"DATA_DIR=/app/data"
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
"PYTHONUNBUFFERED=1"
"PRODUCTION=true"
];
ExposedPorts = {
"9000/tcp" = { };
};
};
}

View file

@ -1,156 +0,0 @@
# syntax=docker/dockerfile:1
# Paperless-ngx — self-hosted document management
# Built from source via forge mirror of paperless-ngx/paperless-ngx
# Closely follows upstream Dockerfile structure with git clone instead of COPY
ARG CONTAINER_APP_VERSION=v2.20.13
###############################################
# Stage 1: Clone source (reused by later stages)
###############################################
FROM docker.io/library/alpine:3.22 AS source
ARG CONTAINER_APP_VERSION
RUN apk add --no-cache git
RUN git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \
https://forge.ops.eblu.me/mirrors/paperless-ngx.git /src
###############################################
# Stage 2: Compile frontend
###############################################
FROM --platform=$BUILDPLATFORM docker.io/node:20-trixie-slim AS compile-frontend
COPY --from=source /src/src-ui /src/src-ui
WORKDIR /src/src-ui
RUN set -eux \
&& npm update -g pnpm \
&& npm install -g corepack@latest \
&& corepack enable \
&& pnpm install
RUN set -eux \
&& ./node_modules/.bin/ng build --configuration production
###############################################
# Stage 3: s6-overlay base
###############################################
FROM ghcr.io/astral-sh/uv:0.9.15-python3.12-trixie-slim AS s6-overlay-base
WORKDIR /usr/src/s6
ENV S6_BEHAVIOUR_IF_STAGE2_FAILS=2 \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
S6_VERBOSITY=1 \
PATH=/command:$PATH
ARG TARGETARCH
ARG TARGETVARIANT
ARG S6_OVERLAY_VERSION=3.2.1.0
RUN set -eux \
&& apt-get update \
&& apt-get install --yes --quiet --no-install-recommends curl xz-utils \
&& S6_ARCH="" \
&& if [ "${TARGETARCH}${TARGETVARIANT}" = "amd64" ]; then S6_ARCH="x86_64"; \
elif [ "${TARGETARCH}${TARGETVARIANT}" = "arm64" ]; then S6_ARCH="aarch64"; fi \
&& if [ -z "${S6_ARCH}" ]; then echo "Error: Cannot determine arch"; exit 1; fi \
&& curl --fail --silent --show-error --location --remote-name-all --parallel \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz.sha256" \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz.sha256" \
&& sha256sum --check ./*.sha256 \
&& tar --directory / -Jxpf s6-overlay-noarch.tar.xz \
&& tar --directory / -Jxpf s6-overlay-${S6_ARCH}.tar.xz \
&& rm ./*.tar.xz ./*.sha256 \
&& apt-get --yes purge curl xz-utils \
&& apt-get --yes autoremove --purge \
&& rm -rf /var/lib/apt/lists/*
# Copy rootfs (s6 service definitions, init scripts)
COPY --from=source /src/docker/rootfs /
###############################################
# Stage 4: Main application
###############################################
FROM s6-overlay-base AS main-app
ARG CONTAINER_APP_VERSION
ARG DEBIAN_FRONTEND=noninteractive
ARG TARGETARCH
ARG JBIG2ENC_VERSION=0.30
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONWARNINGS="ignore:::django.http.response:517" \
PNGX_CONTAINERIZED=1 \
UV_LINK_MODE=copy \
UV_CACHE_DIR=/cache/uv/
# Runtime packages
RUN set -eux \
&& apt-get update \
&& apt-get install --yes --quiet --no-install-recommends \
curl gosu tzdata fonts-liberation gettext ghostscript gnupg \
icc-profiles-free imagemagick postgresql-client \
tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu tesseract-ocr-fra \
tesseract-ocr-ita tesseract-ocr-spa unpaper pngquant jbig2dec \
libxml2 libxslt1.1 qpdf file libmagic1 media-types zlib1g \
libzbar0 poppler-utils \
&& curl --fail --silent --show-error --location --remote-name-all \
"https://github.com/paperless-ngx/builder/releases/download/jbig2enc-trixie-v${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb" \
&& dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
&& cp /etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml \
&& rm --force *.deb \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/paperless/src/
# Python dependencies
COPY --from=source /src/pyproject.toml /src/uv.lock /usr/src/paperless/src/
RUN --mount=type=cache,target=${UV_CACHE_DIR},id=python-cache \
set -eux \
&& apt-get update \
&& apt-get install --yes --quiet --no-install-recommends \
build-essential default-libmysqlclient-dev pkg-config \
&& uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt \
&& uv pip install --system --no-python-downloads --python-preference system --requirements requirements.txt \
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" punkt_tab \
&& apt-get --yes purge build-essential default-libmysqlclient-dev pkg-config \
&& apt-get --yes autoremove --purge \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Copy backend source
COPY --from=source /src/src ./
# Copy compiled frontend
COPY --from=compile-frontend /src/src/documents/static/frontend/ ./documents/static/frontend/
# Create user and finalize
RUN set -eux \
&& addgroup --gid 1000 paperless \
&& useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \
&& mkdir -p /usr/src/paperless/data /usr/src/paperless/media \
/usr/src/paperless/consume /usr/src/paperless/export \
&& chown -R paperless:paperless /usr/src/paperless \
&& s6-setuidgid paperless python3 manage.py collectstatic --clear --no-input --link \
&& s6-setuidgid paperless python3 manage.py compilemessages
VOLUME ["/usr/src/paperless/data", "/usr/src/paperless/media", \
"/usr/src/paperless/consume", "/usr/src/paperless/export"]
ENTRYPOINT ["/init"]
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --retries=5 \
CMD [ "curl", "-fs", "-S", "-L", "--max-time", "2", "http://localhost:8000" ]
LABEL org.opencontainers.image.title="Paperless-ngx"
LABEL org.opencontainers.image.description="Self-hosted document management system"
LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}"
LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops"
LABEL org.opencontainers.image.vendor="blumeops"

View file

@ -0,0 +1,77 @@
# Nix-built Paperless-ngx for ringtail (amd64).
#
# Replaces the from-source Dockerfile build (s6-overlay) with nixpkgs'
# paperless-ngx, which already bundles the full OCR/imaging closure
# (tesseract, ghostscript, imagemagick, qpdf, poppler, jbig2enc) and the
# NLTK data via wrappers — so the image stays lean.
#
# Unlike the upstream s6 image, this image does NOT run all processes
# itself. Paperless is multi-process; on ringtail it runs as four
# containers sharing this one image, each with a different command:
# web -> paperless-web (granian, the wrapper below)
# worker -> celery --app paperless worker
# beat -> celery --app paperless beat
# consumer -> paperless-ngx document_consumer
# plus a redis/valkey sidecar. The PYTHONPATH/granian invocation mirrors
# the nixpkgs paperless NixOS module's paperless-web service exactly.
#
# Self-pins nixos-unstable: stable nixpkgs lags at 2.19.6, while unstable
# carries 2.20.15 — a same-minor forward patch bump from the previous
# Dockerfile build (v2.20.13). The version assertion makes nix-build fail
# if a pin bump changes the version, forcing an explicit acknowledgment
# here and in service-versions.yaml (enforced by container-version-check).
let
nixpkgs = fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/331800de5053fcebacf6813adb5db9c9dca22a0c.tar.gz";
sha256 = "1p54fm6dkbq62kpi55cr4wyx7b1nsajpsnjgs64cmp073fwi15f7";
};
pkgs = import nixpkgs { system = "x86_64-linux"; };
version = "2.20.15";
app = pkgs.paperless-ngx;
# Mirror the NixOS module's paperless-web service: granian serving the
# ASGI app with the package's propagated deps + src on PYTHONPATH.
pythonPath =
"${app.python.pkgs.makePythonPath app.propagatedBuildInputs}:${app}/lib/paperless-ngx/src";
paperless-web = pkgs.writeShellScriptBin "paperless-web" ''
export PYTHONPATH="${pythonPath}"
export PAPERLESS_NLTK_DIR="${app.nltkDataDir}"
exec ${app.python.pkgs.granian}/bin/granian \
--interface asginl --ws \
--host 0.0.0.0 --port 8000 \
"paperless.asgi:application"
'';
in
assert app.version == version;
pkgs.dockerTools.buildLayeredImage {
name = "blumeops/paperless";
contents = [
app
paperless-web
pkgs.bashInteractive
pkgs.coreutils
pkgs.cacert
pkgs.tzdata
];
config = {
# Default command is the web server; worker/beat/consumer containers
# override `command` in their k8s manifests.
Cmd = [ "${paperless-web}/bin/paperless-web" ];
Env = [
"PAPERLESS_NLTK_DIR=${app.nltkDataDir}"
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
"PYTHONUNBUFFERED=1"
"PNGX_CONTAINERIZED=1"
];
ExposedPorts = {
"8000/tcp" = { };
};
};
}

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,122 @@
# 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;
# Keep the build-generated Erlang cookie in the release. mixRelease
# strips it by default (expecting RELEASE_COOKIE at runtime), but the
# start script reads releases/COOKIE. teslamate is single-node (no
# distributed Erlang exposed), so a baked-in cookie is fine.
removeCookie = false;
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 "$@"