From c8b655f17730a9fffb6820263d40ead5c813e7a5 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 25 Jan 2026 21:35:57 -0800 Subject: [PATCH] Build local containers for k8s services (#61) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Move devpi Dockerfile from argocd/manifests to containers/devpi/ - Add containers for: transmission, teslamate, miniflux, kiwix-serve, kubectl - Update all k8s deployments to use local images (registry.ops.eblu.me/blumeops/*) - All containers use v1.0.0 tag for initial release ## Containers Added | Container | Source | Notes | |-----------|--------|-------| | devpi | python:3.12-slim | Existing, moved to containers/ | | kubectl | alpine + download | For zim-watcher CronJob | | miniflux | Go build from source | v2.2.16 | | kiwix-serve | Download pre-built binary | v3.8.1 | | transmission | alpine + apk install | Simpler than linuxserver image | | teslamate | Elixir build from source | v2.2.0 | ## Deployment and Testing - [ ] Build and tag devpi-v1.0.0 - [ ] Build and tag kubectl-v1.0.0 - [ ] Build and tag miniflux-v1.0.0 - [ ] Build and tag kiwix-serve-v1.0.0 - [ ] Build and tag transmission-v1.0.0 - [ ] Build and tag teslamate-v1.0.0 - [ ] Sync ArgoCD apps and verify services 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/61 --- argocd/manifests/devpi/statefulset.yaml | 3 +- .../manifests/kiwix/cronjob-zim-watcher.yaml | 2 +- argocd/manifests/kiwix/deployment.yaml | 7 +- argocd/manifests/miniflux/deployment.yaml | 2 +- argocd/manifests/teslamate/deployment.yaml | 2 +- argocd/manifests/torrent/deployment.yaml | 2 +- .../manifests => containers}/devpi/Dockerfile | 0 .../manifests => containers}/devpi/start.sh | 0 containers/kiwix-serve/Dockerfile | 45 +++++++++++ containers/kubectl/Dockerfile | 36 +++++++++ containers/miniflux/Dockerfile | 31 ++++++++ containers/teslamate/Dockerfile | 78 +++++++++++++++++++ containers/teslamate/entrypoint.sh | 23 ++++++ containers/transmission/Dockerfile | 27 +++++++ containers/transmission/start.sh | 57 ++++++++++++++ 15 files changed, 306 insertions(+), 9 deletions(-) rename {argocd/manifests => containers}/devpi/Dockerfile (100%) rename {argocd/manifests => containers}/devpi/start.sh (100%) create mode 100644 containers/kiwix-serve/Dockerfile create mode 100644 containers/kubectl/Dockerfile create mode 100644 containers/miniflux/Dockerfile create mode 100644 containers/teslamate/Dockerfile create mode 100644 containers/teslamate/entrypoint.sh create mode 100644 containers/transmission/Dockerfile create mode 100644 containers/transmission/start.sh diff --git a/argocd/manifests/devpi/statefulset.yaml b/argocd/manifests/devpi/statefulset.yaml index 77a4c56..14cdf7e 100644 --- a/argocd/manifests/devpi/statefulset.yaml +++ b/argocd/manifests/devpi/statefulset.yaml @@ -18,8 +18,7 @@ spec: fsGroup: 1000 containers: - name: devpi - # TODO: Tag builds with semantic versions (e.g., v1.0.0) for reproducibility - image: registry.ops.eblu.me/blumeops/devpi:latest + image: registry.ops.eblu.me/blumeops/devpi:v1.0.0 env: - name: DEVPI_ROOT_PASSWORD valueFrom: diff --git a/argocd/manifests/kiwix/cronjob-zim-watcher.yaml b/argocd/manifests/kiwix/cronjob-zim-watcher.yaml index 491736f..3532676 100644 --- a/argocd/manifests/kiwix/cronjob-zim-watcher.yaml +++ b/argocd/manifests/kiwix/cronjob-zim-watcher.yaml @@ -14,7 +14,7 @@ spec: serviceAccountName: zim-watcher containers: - name: watcher - image: bitnami/kubectl:1.34.1 + image: registry.ops.eblu.me/blumeops/kubectl:v1.0.0 command: ["/bin/bash", "-c"] args: - | diff --git a/argocd/manifests/kiwix/deployment.yaml b/argocd/manifests/kiwix/deployment.yaml index ec141dc..bf45625 100644 --- a/argocd/manifests/kiwix/deployment.yaml +++ b/argocd/manifests/kiwix/deployment.yaml @@ -20,9 +20,10 @@ spec: containers: # Main kiwix-serve container - name: kiwix-serve - image: ghcr.io/kiwix/kiwix-serve:3.8.1 - command: ["/bin/sh", "-c"] + image: registry.ops.eblu.me/blumeops/kiwix-serve:v1.0.0 args: + - "/bin/sh" + - "-c" - "kiwix-serve --port=80 /data/complete/*.zim" ports: - containerPort: 80 @@ -52,7 +53,7 @@ spec: # Sidecar: Syncs declarative ZIM torrents to transmission - name: torrent-sync - image: lscr.io/linuxserver/transmission:4.0.6 # Has transmission-remote CLI + image: registry.ops.eblu.me/blumeops/transmission:v1.0.1 command: ["/bin/bash", "-c"] args: - | diff --git a/argocd/manifests/miniflux/deployment.yaml b/argocd/manifests/miniflux/deployment.yaml index ab573c9..f5324ac 100644 --- a/argocd/manifests/miniflux/deployment.yaml +++ b/argocd/manifests/miniflux/deployment.yaml @@ -15,7 +15,7 @@ spec: spec: containers: - name: miniflux - image: ghcr.io/miniflux/miniflux:2.2.16 + image: registry.ops.eblu.me/blumeops/miniflux:v1.0.0 ports: - containerPort: 8080 env: diff --git a/argocd/manifests/teslamate/deployment.yaml b/argocd/manifests/teslamate/deployment.yaml index 684b632..116541d 100644 --- a/argocd/manifests/teslamate/deployment.yaml +++ b/argocd/manifests/teslamate/deployment.yaml @@ -15,7 +15,7 @@ spec: spec: containers: - name: teslamate - image: teslamate/teslamate:2.2.0 + image: registry.ops.eblu.me/blumeops/teslamate:v1.0.1 ports: - containerPort: 4000 env: diff --git a/argocd/manifests/torrent/deployment.yaml b/argocd/manifests/torrent/deployment.yaml index 8f331bb..5eafce8 100644 --- a/argocd/manifests/torrent/deployment.yaml +++ b/argocd/manifests/torrent/deployment.yaml @@ -16,7 +16,7 @@ spec: spec: containers: - name: transmission - image: lscr.io/linuxserver/transmission:4.0.6 + image: registry.ops.eblu.me/blumeops/transmission:v1.0.1 env: - name: PUID value: "1000" diff --git a/argocd/manifests/devpi/Dockerfile b/containers/devpi/Dockerfile similarity index 100% rename from argocd/manifests/devpi/Dockerfile rename to containers/devpi/Dockerfile diff --git a/argocd/manifests/devpi/start.sh b/containers/devpi/start.sh similarity index 100% rename from argocd/manifests/devpi/start.sh rename to containers/devpi/start.sh diff --git a/containers/kiwix-serve/Dockerfile b/containers/kiwix-serve/Dockerfile new file mode 100644 index 0000000..37255a4 --- /dev/null +++ b/containers/kiwix-serve/Dockerfile @@ -0,0 +1,45 @@ +# kiwix-serve container +# Downloads pre-built binary from kiwix mirror + +FROM alpine:3.21 + +ARG TARGETPLATFORM +ARG KIWIX_VERSION=3.8.1 + +RUN set -e && \ + apk --no-cache add dumb-init curl && \ + # Detect architecture - use TARGETPLATFORM if set, otherwise detect from uname + if [ -n "$TARGETPLATFORM" ]; then \ + echo "TARGETPLATFORM: $TARGETPLATFORM"; \ + case "$TARGETPLATFORM" in \ + linux/arm64*) ARCH="aarch64" ;; \ + linux/amd64*) ARCH="x86_64" ;; \ + *) ARCH="" ;; \ + esac; \ + else \ + echo "TARGETPLATFORM not set, detecting from uname..."; \ + UNAME_ARCH=$(uname -m); \ + echo "uname -m: $UNAME_ARCH"; \ + case "$UNAME_ARCH" in \ + aarch64|arm64) ARCH="aarch64" ;; \ + x86_64) ARCH="x86_64" ;; \ + *) ARCH="" ;; \ + esac; \ + fi && \ + if [ -z "$ARCH" ]; then \ + echo "ERROR: Unsupported architecture"; \ + exit 1; \ + fi && \ + url="http://mirror.download.kiwix.org/release/kiwix-tools/kiwix-tools_linux-$ARCH-$KIWIX_VERSION.tar.gz" && \ + echo "URL: $url" && \ + curl -k -L $url | tar -xz -C /usr/local/bin/ --strip-components 1 && \ + apk del curl + +EXPOSE 80 + +# Run as non-root +RUN adduser -D -u 1000 kiwix +USER kiwix + +ENTRYPOINT ["/usr/bin/dumb-init", "--"] +CMD ["/bin/sh", "-c", "echo 'Use: kiwix-serve [options] ' && kiwix-serve --help"] diff --git a/containers/kubectl/Dockerfile b/containers/kubectl/Dockerfile new file mode 100644 index 0000000..31a2536 --- /dev/null +++ b/containers/kubectl/Dockerfile @@ -0,0 +1,36 @@ +# Minimal kubectl container +# Multi-arch build: downloads correct binary for target platform + +FROM alpine:3.21 AS downloader + +ARG TARGETARCH +ARG KUBECTL_VERSION=v1.34.1 + +RUN apk add --no-cache curl && \ + # Detect architecture - use TARGETARCH if set, otherwise detect from uname + if [ -n "$TARGETARCH" ]; then \ + ARCH="$TARGETARCH"; \ + else \ + UNAME_ARCH=$(uname -m); \ + case "$UNAME_ARCH" in \ + aarch64|arm64) ARCH="arm64" ;; \ + x86_64) ARCH="amd64" ;; \ + *) echo "Unsupported architecture: $UNAME_ARCH"; exit 1 ;; \ + esac; \ + fi && \ + echo "Downloading kubectl for $ARCH..." && \ + curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl" && \ + chmod +x kubectl + +FROM alpine:3.21 + +COPY --from=downloader /kubectl /usr/local/bin/kubectl + +# Add ca-certificates for HTTPS connections and bash for scripts +RUN apk add --no-cache ca-certificates bash + +# Run as non-root +RUN adduser -D -u 1000 kubectl +USER kubectl + +ENTRYPOINT ["kubectl"] diff --git a/containers/miniflux/Dockerfile b/containers/miniflux/Dockerfile new file mode 100644 index 0000000..eecee4d --- /dev/null +++ b/containers/miniflux/Dockerfile @@ -0,0 +1,31 @@ +# Miniflux RSS feed reader +# Based on upstream packaging/docker/alpine/Dockerfile + +ARG MINIFLUX_VERSION=2.2.16 + +FROM golang:alpine3.21 AS build + +ARG MINIFLUX_VERSION +RUN apk add --no-cache build-base git make + +# Clone specific version +RUN git clone --depth 1 --branch ${MINIFLUX_VERSION} \ + https://github.com/miniflux/v2.git /go/src/app + +WORKDIR /go/src/app +RUN make miniflux + +FROM alpine:3.21 + +LABEL org.opencontainers.image.title=Miniflux +LABEL org.opencontainers.image.description="Miniflux is a minimalist and opinionated feed reader" +LABEL org.opencontainers.image.source=https://github.com/miniflux/v2 + +EXPOSE 8080 +ENV LISTEN_ADDR=0.0.0.0:8080 + +RUN apk --no-cache add ca-certificates tzdata +COPY --from=build /go/src/app/miniflux /usr/bin/miniflux + +USER 65534 +CMD ["/usr/bin/miniflux"] diff --git a/containers/teslamate/Dockerfile b/containers/teslamate/Dockerfile new file mode 100644 index 0000000..e152831 --- /dev/null +++ b/containers/teslamate/Dockerfile @@ -0,0 +1,78 @@ +# TeslaMate - Tesla data logger +# Based on upstream Dockerfile + +ARG TESLAMATE_VERSION=v2.2.0 + +FROM elixir:1.18-otp-26 AS builder + +ARG TESLAMATE_VERSION + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +RUN apt-get update \ + && apt-get install -y ca-certificates curl gnupg git \ + && mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ + | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && NODE_MAJOR=22 \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" \ + | tee /etc/apt/sources.list.d/nodesource.list \ + && apt-get update \ + && apt-get install nodejs -y \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +RUN mix local.rebar --force && \ + mix local.hex --force + +# Clone specific version +RUN git clone --depth 1 --branch ${TESLAMATE_VERSION} \ + https://github.com/teslamate-org/teslamate.git /opt/app + +ENV MIX_ENV=prod +WORKDIR /opt/app + +RUN mix deps.get --only $MIX_ENV +RUN mix deps.compile + +RUN npm ci --prefix ./assets --progress=false --no-audit --loglevel=error +RUN mix assets.deploy + +RUN mix compile +RUN SKIP_LOCALE_DOWNLOAD=true mix release --path /opt/built + +# Runtime image +FROM debian:bookworm-slim AS app + +ENV LANG=C.UTF-8 \ + SRTM_CACHE=/opt/app/.srtm_cache \ + HOME=/opt/app + +WORKDIR $HOME + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libodbc2 \ + libsctp1 \ + libssl3 \ + 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 \ + && chown -R nonroot:nonroot . + +COPY entrypoint.sh / +COPY --from=builder /opt/built . +RUN chmod 555 /entrypoint.sh && \ + chown -R nonroot:nonroot . && \ + mkdir $SRTM_CACHE + +USER nonroot:nonroot + +EXPOSE 4000 + +ENTRYPOINT ["tini", "--", "/bin/dash", "/entrypoint.sh"] +CMD ["bin/teslamate", "start"] diff --git a/containers/teslamate/entrypoint.sh b/containers/teslamate/entrypoint.sh new file mode 100644 index 0000000..f66117e --- /dev/null +++ b/containers/teslamate/entrypoint.sh @@ -0,0 +1,23 @@ +#!/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 "$@" diff --git a/containers/transmission/Dockerfile b/containers/transmission/Dockerfile new file mode 100644 index 0000000..b17a59e --- /dev/null +++ b/containers/transmission/Dockerfile @@ -0,0 +1,27 @@ +# Transmission BitTorrent daemon +# Simpler alternative to linuxserver image + +FROM alpine:3.21 + +ARG TRANSMISSION_VERSION=4.0.6-r0 + +RUN apk add --no-cache \ + transmission-daemon=${TRANSMISSION_VERSION} \ + transmission-cli=${TRANSMISSION_VERSION} \ + transmission-remote=${TRANSMISSION_VERSION} \ + bash \ + curl \ + tzdata \ + su-exec + +# Create directories (user is created dynamically by start.sh based on PUID/PGID) +RUN mkdir -p /config /downloads/complete /downloads/incomplete + +COPY start.sh /start.sh +RUN chmod +x /start.sh + +EXPOSE 9091 51413/tcp 51413/udp + +VOLUME ["/config", "/downloads"] + +ENTRYPOINT ["/start.sh"] diff --git a/containers/transmission/start.sh b/containers/transmission/start.sh new file mode 100644 index 0000000..05d0bf9 --- /dev/null +++ b/containers/transmission/start.sh @@ -0,0 +1,57 @@ +#!/bin/bash +set -e + +# Handle PUID/PGID like linuxserver images +PUID=${PUID:-1000} +PGID=${PGID:-1000} + +# Create or update transmission group/user with requested UID/GID +# The transmission package may have created a user with different IDs +echo "Setting up transmission user with UID=$PUID GID=$PGID" + +# Remove existing user/group if they exist (ignore errors) +deluser transmission 2>/dev/null || true +delgroup transmission 2>/dev/null || true + +# Create fresh user/group with requested IDs +addgroup -g "$PGID" transmission +adduser -D -u "$PUID" -G transmission transmission + +# Ensure directories exist with correct ownership +mkdir -p /config /downloads/complete /downloads/incomplete +# Only chown /config (emptyDir) - /downloads is NFS and may not allow chown +chown -R transmission:transmission /config 2>/dev/null || true +chown transmission:transmission /downloads /downloads/complete /downloads/incomplete 2>/dev/null || true + +# Create default config if it doesn't exist +CONFIG_FILE="/config/settings.json" +if [ ! -f "$CONFIG_FILE" ]; then + echo "Creating default configuration..." + cat > "$CONFIG_FILE" << 'EOF' +{ + "download-dir": "/downloads/complete", + "incomplete-dir": "/downloads/incomplete", + "incomplete-dir-enabled": true, + "rpc-enabled": true, + "rpc-bind-address": "0.0.0.0", + "rpc-port": 9091, + "rpc-whitelist-enabled": false, + "rpc-host-whitelist-enabled": false, + "peer-port": 51413, + "watch-dir-enabled": false, + "umask": 2 +} +EOF + chown transmission:transmission "$CONFIG_FILE" +fi + +# Set timezone +if [ -n "$TZ" ]; then + ln -sf "/usr/share/zoneinfo/$TZ" /etc/localtime +fi + +echo "Starting transmission-daemon..." +exec su-exec transmission transmission-daemon \ + --foreground \ + --config-dir /config \ + --log-level=info