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:
parent
40bd929820
commit
fcac8e5a72
45 changed files with 1422 additions and 445 deletions
|
|
@ -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"]
|
||||
65
containers/mealie/default.nix
Normal file
65
containers/mealie/default.nix
Normal 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" = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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"
|
||||
77
containers/paperless/default.nix
Normal file
77
containers/paperless/default.nix
Normal 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" = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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"])
|
||||
)
|
||||
122
containers/teslamate/default.nix
Normal file
122
containers/teslamate/default.nix
Normal 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" = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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 "$@"
|
||||
Loading…
Add table
Add a link
Reference in a new issue