Compare commits

...

1 Commits

Author SHA1 Message Date
Jorijn Schrijvershof
5be6a506fb chore: rebuild docker pipeline 2026-01-13 23:19:32 +01:00
12 changed files with 564 additions and 406 deletions

View File

@@ -1,42 +1,51 @@
# Git
# VCS
.git
.gitignore
.github
# Python
__pycache__
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
.venv
venv
ENV
env
.venv/
venv/
ENV/
env/
.mypy_cache/
.pytest_cache/
.ruff_cache/
# IDE
.idea
.vscode
# Editor/IDE
.idea/
.vscode/
*.swp
*.swo
*~
# Project directories (generated/runtime)
data/
out/
# Configuration (use environment variables in Docker)
# Local env/config
.env
.env.*
.envrc
.direnv
.direnv/
meshcore.conf
# Documentation
# Generated/runtime data
.data/
data/
out/
.buildx-cache/
# Docs/tests (not needed in image)
docs/
tests/
*.md
!README.md
!LICENSE
# Development files
.codex/
*.log
# macOS
# Misc
.DS_Store
Thumbs.db
*.log
.codex/

View File

@@ -1,276 +0,0 @@
# Build and publish Docker images to GitHub Container Registry
#
# Triggers:
# - On release: Build with version tags (X.Y.Z, X.Y, latest)
# - On schedule: Rebuild all tags with fresh base image (OS patches)
# - Manual: For testing, optional push
#
# Security:
# - All actions pinned by SHA
# - Vulnerability scanning with Trivy
# - SBOM and provenance attestation
name: Docker Build and Publish
on:
release:
types: [published]
schedule:
# Daily at 4 AM UTC - rebuild with fresh base image
- cron: "0 4 * * *"
pull_request:
paths:
- Dockerfile
- .dockerignore
- docker/**
- pyproject.toml
- uv.lock
- src/**
- scripts/**
- .github/workflows/docker-publish.yml
workflow_dispatch:
inputs:
push:
description: "Push image to registry"
required: false
default: false
type: boolean
permissions:
contents: read
packages: write
id-token: write
attestations: write
artifact-metadata: write
concurrency:
group: docker-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
# For nightly builds, get the latest release version
- name: Get latest release version
id: get-version
if: github.event_name == 'schedule'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get latest release tag
LATEST_TAG=$(gh release view --json tagName -q '.tagName' 2>/dev/null || echo "")
if [ -z "$LATEST_TAG" ]; then
echo "No releases found, skipping nightly build"
echo "skip=true" >> $GITHUB_OUTPUT
exit 0
fi
# Strip 'v' prefix if present
VERSION=$(echo "$LATEST_TAG" | sed 's/^v//')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "skip=false" >> $GITHUB_OUTPUT
- name: Skip if no releases
if: github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true'
run: |
echo "No releases found, skipping nightly build"
exit 0
- name: Set up QEMU
if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')"
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')"
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to Container Registry
if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')"
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Generate tags based on event type
- name: Extract metadata (release)
id: meta-release
if: github.event_name == 'release'
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
# X.Y.Z
type=semver,pattern={{version}}
# X.Y
type=semver,pattern={{major}}.{{minor}}
# latest
type=raw,value=latest
- name: Extract metadata (nightly)
id: meta-nightly
if: github.event_name == 'schedule' && steps.get-version.outputs.skip != 'true'
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
# Rebuild version tags with OS patches
type=raw,value=${{ steps.get-version.outputs.version }}
# Nightly tags
type=raw,value=nightly
type=raw,value=nightly-{{date 'YYYYMMDD'}}
# Also update latest with security patches
type=raw,value=latest
- name: Extract metadata (manual)
id: meta-manual
if: github.event_name == 'workflow_dispatch'
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=sha-
# Build image (release - with cache)
- name: Build and push (release)
id: build-release
if: github.event_name == 'release'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.meta-release.outputs.tags }}
labels: ${{ steps.meta-release.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
# Build image (nightly - no cache, fresh base)
- name: Build and push (nightly)
id: build-nightly
if: github.event_name == 'schedule' && steps.get-version.outputs.skip != 'true'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.meta-nightly.outputs.tags }}
labels: ${{ steps.meta-nightly.outputs.labels }}
pull: true
no-cache: true
provenance: true
sbom: true
# Build image (manual)
- name: Build and push (manual)
id: build-manual
if: github.event_name == 'workflow_dispatch'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: ${{ inputs.push }}
tags: ${{ steps.meta-manual.outputs.tags }}
labels: ${{ steps.meta-manual.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
# Determine image tag for scanning and testing
- name: Determine image tag
id: image-tag
if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')"
run: |
if [ "${{ github.event_name }}" = "release" ]; then
# Strip 'v' prefix to match semver tag format from metadata-action
echo "tag=$(echo '${{ github.event.release.tag_name }}' | sed 's/^v//')" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" = "schedule" ]; then
echo "tag=nightly" >> $GITHUB_OUTPUT
else
echo "tag=sha-${{ github.sha }}" >> $GITHUB_OUTPUT
fi
# Vulnerability scanning
- name: Run Trivy vulnerability scanner
if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')"
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.image-tag.outputs.tag }}
format: "sarif"
output: "trivy-results.sarif"
severity: "CRITICAL,HIGH"
continue-on-error: true
- name: Upload Trivy scan results
if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')"
uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with:
sarif_file: "trivy-results.sarif"
continue-on-error: true
# Smoke test - verify image runs correctly
# Skip for manual runs when push is disabled (image not available to pull)
- name: Smoke test
if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true') && !(github.event_name == 'workflow_dispatch' && inputs.push == false)"
run: |
IMAGE_TAG="${{ steps.image-tag.outputs.tag }}"
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG}
# Test that Python and key modules are available
docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG} \
python -c "from meshmon.db import init_db; from meshmon.env import get_config; print('Smoke test passed')"
# Attestation (releases only)
- name: Generate attestation
if: github.event_name == 'release'
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build-release.outputs.digest }}
push-to-registry: true
build-pr:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build image (PR)
id: build-pr
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/amd64
load: true
push: false
tags: meshcore-stats:pr-${{ github.event.pull_request.number }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Smoke test (PR)
run: |
docker run --rm meshcore-stats:pr-${{ github.event.pull_request.number }} \
python -c "from meshmon.db import init_db; from meshmon.env import get_config; print('Smoke test passed')"

97
.github/workflows/nightly.yml vendored Normal file
View File

@@ -0,0 +1,97 @@
name: Nightly Container
on:
schedule:
- cron: '0 3 * * *'
workflow_dispatch:
permissions:
contents: read
packages: write
id-token: write
concurrency:
group: nightly
cancel-in-progress: true
jobs:
nightly:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
IMAGE_NAME: ghcr.io/jorijn/meshcore-stats
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Build metadata
id: vars
run: |
echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
echo "version=nightly" >> "$GITHUB_OUTPUT"
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to GHCR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push nightly image
uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0
env:
BUILDKIT_PROGRESS: plain
CACHE_FROM: type=gha
CACHE_TO: type=gha,mode=max
PLATFORMS: linux/amd64,linux/arm64
VERSION: ${{ steps.vars.outputs.version }}
VCS_REF: ${{ github.sha }}
BUILD_DATE: ${{ steps.vars.outputs.build_date }}
with:
files: docker-bake.hcl
targets: nightly
provenance: true
sbom: true
- name: Verify multi-arch manifest
run: |
docker buildx imagetools inspect "${IMAGE_NAME}:nightly" | tee /tmp/imagetools.txt
grep -q "linux/amd64" /tmp/imagetools.txt
grep -q "linux/arm64" /tmp/imagetools.txt
- name: Resolve manifest digest
id: digest
run: |
DIGEST=$(docker buildx imagetools inspect "${IMAGE_NAME}:nightly" --format '{{.Digest}}')
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
- name: Trivy image scan (report-only)
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
with:
image-ref: ${{ env.IMAGE_NAME }}:nightly
format: sarif
output: trivy-nightly.sarif
exit-code: '0'
vuln-type: os,library
ignore-unfixed: true
- name: Upload Trivy report
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: trivy-nightly
path: trivy-nightly.sarif
if-no-files-found: warn
retention-days: 14
- name: Install cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Cosign sign image (keyless)
run: |
cosign sign --yes "${IMAGE_NAME}@${{ steps.digest.outputs.digest }}"

137
.github/workflows/pr.yml vendored Normal file
View File

@@ -0,0 +1,137 @@
name: PR Container Checks
on:
pull_request:
paths:
- Dockerfile
- .dockerignore
- docker-bake.hcl
- docker/**
- .github/workflows/**
- pyproject.toml
- uv.lock
- src/**
- scripts/**
concurrency:
group: pr-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Lint workflows (actionlint)
uses: rhysd/actionlint@0933c147c9d6587653d45fdcb4c497c57a65f9af # v1.7.10
- name: Lint Dockerfile (hadolint)
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
with:
dockerfile: Dockerfile
build:
runs-on: ubuntu-latest
timeout-minutes: 45
needs: [lint]
env:
IMAGE_NAME: ghcr.io/jorijn/meshcore-stats
PLATFORMS: linux/amd64,linux/arm64
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Build metadata
id: vars
run: |
echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
echo "version=pr-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build multi-arch (amd64+arm64)
uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0
env:
BUILDKIT_PROGRESS: plain
CACHE_FROM: type=gha
CACHE_TO: type=gha,mode=max
PLATFORMS: ${{ env.PLATFORMS }}
VERSION: ${{ steps.vars.outputs.version }}
VCS_REF: ${{ github.sha }}
BUILD_DATE: ${{ steps.vars.outputs.build_date }}
with:
files: docker-bake.hcl
targets: pr-multiarch
- name: Build native image (load)
uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0
env:
BUILDKIT_PROGRESS: plain
CACHE_FROM: type=gha
CACHE_TO: type=gha,mode=max
LOCAL_PLATFORM: linux/amd64
VERSION: ${{ steps.vars.outputs.version }}
VCS_REF: ${{ github.sha }}
BUILD_DATE: ${{ steps.vars.outputs.build_date }}
with:
files: docker-bake.hcl
targets: pr-native
scan:
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [lint]
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build metadata
id: scan-vars
run: |
echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
echo "version=pr-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
- name: Build native image for scan (load)
uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0
env:
BUILDKIT_PROGRESS: plain
CACHE_FROM: type=gha
CACHE_TO: type=gha,mode=max
LOCAL_PLATFORM: linux/amd64
VERSION: ${{ steps.scan-vars.outputs.version }}
VCS_REF: ${{ github.sha }}
BUILD_DATE: ${{ steps.scan-vars.outputs.build_date }}
with:
files: docker-bake.hcl
targets: pr-native
- name: Trivy image scan (report-only)
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
with:
image-ref: meshcore-stats:pr
format: sarif
output: trivy-report.sarif
exit-code: '0'
vuln-type: os,library
ignore-unfixed: true
- name: Upload Trivy report
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: trivy-report-pr
path: trivy-report.sarif
if-no-files-found: warn
retention-days: 7

104
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,104 @@
name: Release Container
on:
push:
tags:
- 'v*.*.*'
permissions:
contents: read
packages: write
id-token: write
jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
IMAGE_NAME: ghcr.io/jorijn/meshcore-stats
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Validate tag and set metadata
id: vars
shell: bash
run: |
if [[ ! "$GITHUB_REF_NAME" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "Tag must match vMAJOR.MINOR.PATCH (e.g., v1.2.3)." >&2
exit 1
fi
MAJOR="${BASH_REMATCH[1]}"
MINOR="${BASH_REMATCH[2]}"
PATCH="${BASH_REMATCH[3]}"
VERSION="${MAJOR}.${MINOR}.${PATCH}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "release_tags=${IMAGE_NAME}:${MAJOR},${IMAGE_NAME}:${MAJOR}.${MINOR},${IMAGE_NAME}:${VERSION},${IMAGE_NAME}:latest" >> "$GITHUB_OUTPUT"
echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to GHCR
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push release image
uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0
env:
BUILDKIT_PROGRESS: plain
CACHE_FROM: type=gha
CACHE_TO: type=gha,mode=max
PLATFORMS: linux/amd64,linux/arm64
VERSION: ${{ steps.vars.outputs.version }}
VCS_REF: ${{ github.sha }}
BUILD_DATE: ${{ steps.vars.outputs.build_date }}
RELEASE_TAGS: ${{ steps.vars.outputs.release_tags }}
with:
files: docker-bake.hcl
targets: release
provenance: true
sbom: true
- name: Verify multi-arch manifest
run: |
docker buildx imagetools inspect "${IMAGE_NAME}:${{ steps.vars.outputs.version }}" | tee /tmp/imagetools.txt
grep -q "linux/amd64" /tmp/imagetools.txt
grep -q "linux/arm64" /tmp/imagetools.txt
- name: Resolve manifest digest
id: digest
run: |
DIGEST=$(docker buildx imagetools inspect "${IMAGE_NAME}:${{ steps.vars.outputs.version }}" --format '{{.Digest}}')
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
- name: Trivy image scan (report-only)
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
with:
image-ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.version }}
format: sarif
output: trivy-release.sarif
exit-code: '0'
vuln-type: os,library
ignore-unfixed: true
- name: Upload Trivy report
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: trivy-release
path: trivy-release.sarif
if-no-files-found: warn
retention-days: 14
- name: Install cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Cosign sign image (keyless)
run: |
cosign sign --yes "${IMAGE_NAME}@${{ steps.digest.outputs.digest }}"

3
.gitignore vendored
View File

@@ -25,6 +25,7 @@ meshcore.conf
# Docker local overrides
docker-compose.override.yml
.buildx-cache/
# Data directories (keep structure, ignore content)
data/snapshots/companion/**/*.json
@@ -48,4 +49,4 @@ out/*
.DS_Store
Thumbs.db
.direnv
.direnv

View File

@@ -359,16 +359,11 @@ Jobs configured in `docker/ofelia.ini`:
### GitHub Actions Workflow
`.github/workflows/docker-publish.yml` builds and publishes Docker images for `linux/amd64`, `linux/arm64`, and `linux/arm/v7`:
Container workflows (amd64 + arm64 only):
| Trigger | Tags Created |
|---------|--------------|
| Release | `X.Y.Z`, `X.Y`, `latest` |
| Nightly (4 AM UTC) | Rebuilds all version tags + `nightly`, `nightly-YYYYMMDD` |
| Manual | `sha-xxxxxx` |
| Pull request | Builds image (linux/amd64) without pushing and runs a smoke test |
**Nightly rebuilds** ensure version tags always include the latest OS security patches. This is a common pattern used by official Docker images (nginx, postgres, node). Users needing reproducibility should pin by SHA digest or use dated nightly tags.
- `pr.yml`: pull_request lint (actionlint/hadolint), container-relevant path filter, multi-arch build (no push), native image build for report-only Trivy scan.
- `release.yml`: tag push `vMAJOR.MINOR.PATCH` → push `MAJOR`, `MAJOR.MINOR`, `MAJOR.MINOR.PATCH`, `latest` with SBOM + provenance attestations, Trivy report artifact, cosign keyless signing, and manifest platform verification.
- `nightly.yml`: scheduled + workflow_dispatch → push `nightly` with SBOM + provenance attestations, Trivy report artifact, cosign keyless signing, and manifest platform verification.
GitHub Actions use version tags in workflows, and Renovate is configured in `renovate.json` to pin action digests, maintain lockfiles, and auto-merge patch + digest updates once required checks pass (with automatic rebases when behind `main`).

View File

@@ -1,155 +1,100 @@
# =============================================================================
# Stage 0: Ofelia binary
# =============================================================================
FROM golang:1.25-bookworm@sha256:2c7c65601b020ee79db4c1a32ebee0bf3d6b298969ec683e24fcbea29305f10e AS ofelia-builder
# syntax=docker/dockerfile:1.7
# Ofelia version (built from source for multi-arch support)
FROM ghcr.io/astral-sh/uv:0.9.24@sha256:816fdce3387ed2142e37d2e56e1b1b97ccc1ea87731ba199dc8a25c04e4997c5 AS uv
FROM golang:1.25-bookworm@sha256:2c7c65601b020ee79db4c1a32ebee0bf3d6b298969ec683e24fcbea29305f10e AS ofelia-builder
ARG OFELIA_VERSION=0.3.12
ARG TARGETARCH
ARG TARGETVARIANT
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
ca-certificates \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src/ofelia
RUN git clone --depth 1 --branch "v${OFELIA_VERSION}" https://github.com/mcuadros/ofelia.git /src/ofelia
RUN git clone --depth 1 --branch "v${OFELIA_VERSION}" https://github.com/mcuadros/ofelia.git .
RUN set -ex; \
if [ "$TARGETARCH" = "amd64" ]; then \
GOARCH="amd64"; \
elif [ "$TARGETARCH" = "arm64" ]; then \
GOARCH="arm64"; \
elif [ "$TARGETARCH" = "arm" ] && [ "$TARGETVARIANT" = "v7" ]; then \
GOARCH="arm"; \
GOARM="7"; \
else \
echo "Unsupported architecture: $TARGETARCH${TARGETVARIANT:+/$TARGETVARIANT}" && exit 1; \
fi; \
if [ -n "${GOARM:-}" ]; then \
export GOARM; \
fi; \
CGO_ENABLED=0 GOOS=linux GOARCH="$GOARCH" go build -o /usr/local/bin/ofelia .
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
set -eux; \
case "$TARGETARCH" in \
amd64|arm64) GOARCH="$TARGETARCH" ;; \
*) echo "Unsupported architecture: $TARGETARCH" >&2; exit 1 ;; \
esac; \
CGO_ENABLED=0 GOOS=linux GOARCH="$GOARCH" \
go build -trimpath -ldflags "-s -w" -o /usr/local/bin/ofelia .
# =============================================================================
# Stage 1: Build dependencies
# =============================================================================
FROM python:3.14-slim-bookworm@sha256:3be2c910db2dacfb3e576f94c7ffc07c10b115cbcd3de99d49bfb0b4ccfd75e7 AS builder
# uv version and checksums (verified from GitHub releases)
ARG UV_VERSION=0.9.24
ARG UV_SHA256_AMD64=fb13ad85106da6b21dd16613afca910994446fe94a78ee0b5bed9c75cd066078
ARG UV_SHA256_ARM64=9b291a1a4f2fefc430e4fc49c00cb93eb448d41c5c79edf45211ceffedde3334
ARG UV_SHA256_ARMV7=8d05b55fe2108ecab3995c2b656679a72c543fd9dc72eeb3a525106a709cfdcb
ARG TARGETARCH
ARG TARGETVARIANT
# Install build dependencies for Python packages
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libfreetype6-dev \
libpng-dev \
curl \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
# Download and verify uv binary in builder stage
RUN set -ex; \
if [ "$TARGETARCH" = "amd64" ]; then \
UV_ARCH="x86_64"; \
UV_SHA256="$UV_SHA256_AMD64"; \
elif [ "$TARGETARCH" = "arm64" ]; then \
UV_ARCH="aarch64"; \
UV_SHA256="$UV_SHA256_ARM64"; \
elif [ "$TARGETARCH" = "arm" ] && [ "$TARGETVARIANT" = "v7" ]; then \
UV_ARCH="armv7"; \
UV_SHA256="$UV_SHA256_ARMV7"; \
else \
echo "Unsupported architecture: $TARGETARCH${TARGETVARIANT:+/$TARGETVARIANT}" && exit 1; \
fi; \
curl -fsSL "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_ARCH}-unknown-linux-gnu.tar.gz" \
-o /tmp/uv.tar.gz \
&& echo "${UV_SHA256} /tmp/uv.tar.gz" | sha256sum -c - \
&& tar -xzf /tmp/uv.tar.gz -C /usr/local/bin --strip-components=1 --wildcards "*/uv" \
&& rm /tmp/uv.tar.gz \
&& chmod +x /usr/local/bin/uv
COPY --from=uv /uv /uvx /usr/local/bin/
# Create virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH" \
UV_PROJECT_ENVIRONMENT=/opt/venv
# Install Python dependencies
COPY pyproject.toml uv.lock ./
RUN pip install --no-cache-dir --upgrade pip && \
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev --no-install-project
# =============================================================================
# Stage 2: Runtime
# =============================================================================
FROM python:3.14-slim-bookworm@sha256:3be2c910db2dacfb3e576f94c7ffc07c10b115cbcd3de99d49bfb0b4ccfd75e7
# OCI Labels
LABEL org.opencontainers.image.source="https://github.com/jorijn/meshcore-stats"
LABEL org.opencontainers.image.description="MeshCore Stats - LoRa mesh network monitoring"
LABEL org.opencontainers.image.licenses="MIT"
ARG BUILD_DATE=1970-01-01T00:00:00Z
ARG VCS_REF=unknown
ARG VERSION=dev
LABEL org.opencontainers.image.source="https://github.com/jorijn/meshcore-stats" \
org.opencontainers.image.description="MeshCore Stats - LoRa mesh network monitoring" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.revision=$VCS_REF \
org.opencontainers.image.version=$VERSION
# Install runtime dependencies
# - tini: init system for proper signal handling
# - libfreetype6, libpng16-16: matplotlib runtime libraries
# - fontconfig, fonts-dejavu-core: fonts for chart text rendering
RUN apt-get update && apt-get install -y --no-install-recommends \
tini \
libfreetype6 \
libpng16-16 \
ca-certificates \
fontconfig \
fonts-dejavu-core \
libfreetype6 \
libpng16-16 \
tini \
&& rm -rf /var/lib/apt/lists/* \
# Build font cache for matplotlib
&& fc-cache -f \
# Remove setuid/setgid binaries for security
&& find / -perm /6000 -type f -exec chmod a-s {} \; 2>/dev/null || true
&& find /usr /bin /sbin /usr/local /usr/lib -perm /6000 -type f -exec chmod a-s {} + 2>/dev/null || true
# Create non-root user with dialout group for serial access
RUN groupadd -g 1000 meshmon \
&& useradd -u 1000 -g meshmon -G dialout -s /sbin/nologin meshmon \
&& mkdir -p /data/state /out /tmp/matplotlib \
&& chown -R meshmon:meshmon /data /out /tmp/matplotlib
RUN set -eux; \
if ! getent group dialout >/dev/null; then groupadd -r dialout; fi; \
groupadd -g 1000 meshmon; \
useradd -u 1000 -g 1000 -G dialout -m -s /usr/sbin/nologin meshmon; \
mkdir -p /app /data/state /out /tmp/matplotlib; \
chown -R meshmon:meshmon /app /data /out /tmp/matplotlib
# Copy Ofelia binary from builder (keeps curl out of runtime image)
COPY --from=ofelia-builder /usr/local/bin/ofelia /usr/local/bin/ofelia
# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
# Copy application code
COPY --chown=meshmon:meshmon src/ /app/src/
COPY --chown=meshmon:meshmon scripts/ /app/scripts/
COPY --chown=meshmon:meshmon docker/ofelia.ini /app/ofelia.ini
# Environment configuration
# - PATH: Include venv so Ofelia can run Python
# - PYTHONPATH: Allow imports from src/meshmon
# - PYTHONUNBUFFERED: Ensure logs are output immediately
# - PYTHONDONTWRITEBYTECODE: Don't create .pyc files
# - MPLCONFIGDIR: Matplotlib font cache directory
ENV PATH="/opt/venv/bin:$PATH" \
PYTHONPATH=/app/src \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
MPLCONFIGDIR=/tmp/matplotlib
MPLCONFIGDIR=/tmp/matplotlib \
XDG_CACHE_HOME=/tmp \
HOME=/tmp
WORKDIR /app
# Run as non-root user
USER meshmon
USER 1000:1000
# Use tini as init system for proper signal handling
ENTRYPOINT ["/usr/bin/tini", "--"]
# Run Ofelia scheduler
CMD ["ofelia", "daemon", "--config=/app/ofelia.ini"]
# Health check - verify database is accessible
HEALTHCHECK --interval=5m --timeout=30s --start-period=60s --retries=3 \
CMD python -c "import sqlite3; sqlite3.connect('/data/state/metrics.db').execute('SELECT 1')" || exit 1

28
Makefile Normal file
View File

@@ -0,0 +1,28 @@
SHELL := /bin/sh
IMAGE_NAME ?= ghcr.io/jorijn/meshcore-stats
PLATFORMS ?= linux/amd64,linux/arm64
LOCAL_PLATFORM ?= $(shell (docker info -f '{{.OSType}}/{{.Architecture}}' 2>/dev/null || echo linux/amd64) | tr -d '[:space:]')
BUILD_DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
VCS_REF ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
VERSION ?= dev
CACHE_FROM ?= type=local,src=.buildx-cache
CACHE_TO ?= type=local,dest=.buildx-cache,mode=max
.PHONY: docker-build docker-buildx-all
docker-build:
LOCAL_PLATFORM=$(LOCAL_PLATFORM) docker buildx bake pr-native \
--set *.args.BUILD_DATE=$(BUILD_DATE) \
--set *.args.VCS_REF=$(VCS_REF) \
--set *.args.VERSION=$(VERSION) \
--set *.cache-from=$(CACHE_FROM) \
--set *.cache-to=$(CACHE_TO)
docker-buildx-all:
PLATFORMS=$(PLATFORMS) docker buildx bake pr-multiarch \
--set *.args.BUILD_DATE=$(BUILD_DATE) \
--set *.args.VCS_REF=$(VCS_REF) \
--set *.args.VERSION=$(VERSION) \
--set *.cache-from=$(CACHE_FROM) \
--set *.cache-to=$(CACHE_TO)

View File

@@ -56,7 +56,7 @@ docker compose logs meshcore-stats | head -20
- Remote repeater node reachable via LoRa from the companion
**Resource requirements:** ~100MB memory, ~100MB disk per year of data.
**Container architectures:** `linux/amd64`, `linux/arm64`, and `linux/arm/v7` (32-bit).
**Container architectures:** `linux/amd64`, `linux/arm64`.
## Installation
@@ -157,6 +157,46 @@ cp data/state/metrics.db data/state/metrics.db.backup
> **Note**: `docker compose down` preserves your data. Use `docker compose down -v` only if you want to delete everything.
## Container Images & CI
### Tags and Platforms
- **Release tags** (on `vMAJOR.MINOR.PATCH`): `MAJOR`, `MAJOR.MINOR`, `MAJOR.MINOR.PATCH`, `latest`
- **Nightly tag**: `nightly`
- **Platforms**: `linux/amd64`, `linux/arm64`
### Security Artifacts
- **SBOM and provenance** are attached to the pushed images in GHCR (view in the image details/attestations UI).
- **Vulnerability reports** are uploaded as GitHub Actions artifacts for each release/nightly build.
Example cosign verification (release tag):
```bash
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp 'https://github.com/jorijn/meshcore-stats/.github/workflows/release.yml@refs/tags/v1.2.3' \
ghcr.io/jorijn/meshcore-stats:1.2.3
```
### Local Workflow Testing
Prerequisites:
- Docker Desktop (or Docker Engine) with Buildx enabled
- QEMU/binfmt installed for multi-arch builds
Commands:
```bash
# Native-arch build (loads into local Docker)
make docker-build
# Multi-arch build validation (amd64 + arm64, no push)
make docker-buildx-all
```
Use `make docker-buildx-all` for full multi-arch validation.
### Manual Installation (Alternative)
For environments where Docker is not available.

72
docker-bake.hcl Normal file
View File

@@ -0,0 +1,72 @@
variable "IMAGE_NAME" {
default = "ghcr.io/jorijn/meshcore-stats"
}
variable "PLATFORMS" {
default = "linux/amd64,linux/arm64"
}
variable "LOCAL_PLATFORM" {
default = "linux/amd64"
}
variable "CACHE_FROM" {
default = "type=local,src=.buildx-cache"
}
variable "CACHE_TO" {
default = "type=local,dest=.buildx-cache,mode=max"
}
variable "VERSION" {
default = "dev"
}
variable "VCS_REF" {
default = "unknown"
}
variable "BUILD_DATE" {
default = "1970-01-01T00:00:00Z"
}
variable "RELEASE_TAGS" {
default = ""
}
target "base" {
context = "."
dockerfile = "Dockerfile"
platforms = split(",", PLATFORMS)
args = {
VERSION = VERSION
VCS_REF = VCS_REF
BUILD_DATE = BUILD_DATE
}
cache-from = [CACHE_FROM]
cache-to = [CACHE_TO]
}
target "pr-multiarch" {
inherits = ["base"]
output = ["type=cacheonly"]
}
target "pr-native" {
inherits = ["base"]
platforms = [LOCAL_PLATFORM]
tags = ["meshcore-stats:pr"]
output = ["type=docker"]
}
target "release" {
inherits = ["base"]
tags = RELEASE_TAGS != "" ? split(",", RELEASE_TAGS) : []
push = true
}
target "nightly" {
inherits = ["base"]
tags = ["${IMAGE_NAME}:nightly"]
push = true
}

View File

@@ -17,6 +17,12 @@
],
"pinDigests": true
},
{
"matchManagers": [
"dockerfile"
],
"pinDigests": true
},
{
"matchManagers": [
"docker-compose"