feat: Raspberry Pi deployment — Dockerfile, CI/CD workflows, actuator
Some checks failed
CI / Build & test backend (push) Failing after 13m38s
Some checks failed
CI / Build & test backend (push) Failing after 13m38s
Backend: - backend/Dockerfile: multi-stage build (local + ci-image targets) - eclipse-temurin:21-jre-alpine runtime, non-root user caloriecounter - DOCKER_BUILDKIT=0 + native JAR build pattern for ARM64 Pi runner - pom.xml: add spring-boot-starter-actuator - application.yml: expose /actuator/health (liveness probe for Docker healthcheck) CI/CD: - .gitea/workflows/ci.yml: Maven test on pi-runner (push + PR) - .gitea/workflows/docker.yml: build + push calorie-counter-api:latest/:sha - Triggers after CI passes (workflow_run) to avoid OOM on single-capacity Pi - Docker login before Maven build (avoids daemon timeout after heavy CPU) - Deploys immediately via docker.sock to /home/andris/homelab/calorie-counter/ Mobile: - api.ts: update baseURL default to https://calories.amlab.dev (LAN fallback: http://10.18.1.135:8085 via WireGuard) Homelab changes pushed directly via Gitea MCP: - homelab/calorie-counter/docker-compose.yml (port 8085, 120s start_period) - homelab/calorie-counter/.env.example - homelab/.gitea/workflows/deploy-calorie-counter.yml (cron every 10 min) - homelab/backup/backup.sh (added calorie-counter pg_dump)
This commit is contained in:
42
.gitea/workflows/ci.yml
Normal file
42
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Generated by GitHub Copilot
|
||||||
|
#
|
||||||
|
# ci.yml — Build and test the calorie-counter backend
|
||||||
|
#
|
||||||
|
# Runs on every push to main and every PR targeting main.
|
||||||
|
# Produces a green/red signal that docker.yml waits for before building images.
|
||||||
|
#
|
||||||
|
# Runs on pi-runner (native ARM64) — no QEMU, no cross-compilation overhead.
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
# Cancel any in-progress CI run for the same ref so the single-capacity Pi
|
||||||
|
# runner is not blocked by a superseded run.
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Build & test backend
|
||||||
|
runs-on: pi-runner
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Java 21 LTS
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: '21'
|
||||||
|
cache: maven
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
working-directory: backend
|
||||||
|
run: mvn -q -ntp clean verify
|
||||||
168
.gitea/workflows/docker.yml
Normal file
168
.gitea/workflows/docker.yml
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# Generated by GitHub Copilot
|
||||||
|
#
|
||||||
|
# docker.yml — Build and push Docker image to Gitea Container Registry
|
||||||
|
#
|
||||||
|
# Triggers after CI passes on main (workflow_run) to prevent this heavyweight job
|
||||||
|
# from competing with the Maven+test job on the single-capacity Pi runner.
|
||||||
|
#
|
||||||
|
# Produces images tagged with the short SHA and 'latest'.
|
||||||
|
# Immediately deploys to the Pi production stack via docker.sock.
|
||||||
|
#
|
||||||
|
# Required Gitea repository secrets (Settings → Actions → Secrets):
|
||||||
|
# REGISTRY_USERNAME — Gitea username (andrisenins)
|
||||||
|
# REGISTRY_PASSWORD — Gitea access token (package:write + repo:write scope)
|
||||||
|
|
||||||
|
name: Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Only run after CI succeeds — prevents OOM kills on the Pi from parallel jobs.
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["CI"]
|
||||||
|
types: [completed]
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Cancel any in-progress docker build for the same branch so stale BuildKit
|
||||||
|
# containers from the previous run are cleaned up before the new build starts.
|
||||||
|
concurrency:
|
||||||
|
group: docker-${{ gitea.event.workflow_run.head_branch || gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.amlab.dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-api:
|
||||||
|
name: Build & push API image
|
||||||
|
runs-on: pi-runner
|
||||||
|
# For workflow_run events: only build if CI succeeded.
|
||||||
|
# For workflow_dispatch (manual): always run.
|
||||||
|
if: >
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
github.event.workflow_run.conclusion == 'success'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# In a workflow_run context, checkout the commit that triggered CI.
|
||||||
|
ref: ${{ gitea.event.workflow_run.head_sha || gitea.sha }}
|
||||||
|
|
||||||
|
- name: Log in to Gitea Container Registry
|
||||||
|
# Login BEFORE the Maven build while the Pi CPU is idle.
|
||||||
|
# When login is attempted after a long Maven build the host Docker daemon
|
||||||
|
# exhausts its 30s HTTP timeout on every attempt (confirmed in MYPHOTOS
|
||||||
|
# task logs — same issue applies here).
|
||||||
|
# NOTE: use 'if' not '&&' — act_runner uses 'bash -e', so 'cmd && break'
|
||||||
|
# exits the entire script on the first non-zero cmd result.
|
||||||
|
env:
|
||||||
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
run: |
|
||||||
|
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
if echo "$REGISTRY_PASSWORD" | docker login --username "$REGISTRY_USERNAME" --password-stdin "$REGISTRY"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "$i" -eq 10 ]; then
|
||||||
|
echo "ERROR: All 10 docker login attempts failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Login attempt $i failed, retrying in 30s..."
|
||||||
|
sleep 30
|
||||||
|
done
|
||||||
|
|
||||||
|
# Build JAR natively (inside the job container, outside BuildKit/QEMU).
|
||||||
|
# Without this, Maven runs under QEMU cross-emulation during the Docker build
|
||||||
|
# RUN step, causing TLS connections to Maven Central to break after ~20 min.
|
||||||
|
- name: Set up Java 21 LTS
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: '21'
|
||||||
|
cache: maven
|
||||||
|
|
||||||
|
- name: Build JAR (native — avoids QEMU Maven download)
|
||||||
|
working-directory: backend
|
||||||
|
run: mvn -q -ntp clean package -DskipTests
|
||||||
|
|
||||||
|
- name: Build and push API image
|
||||||
|
# Use plain docker build+push (no buildx, no JS action) to avoid
|
||||||
|
# docker/build-push-action@v6 crashing silently in act_runner v0.5.0.
|
||||||
|
# DOCKER_BUILDKIT=0 uses legacy builder — no gRPC DeadlineExceeded on RPi.
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: "0"
|
||||||
|
SHA: ${{ gitea.event.workflow_run.head_sha || gitea.sha }}
|
||||||
|
OWNER: ${{ gitea.repository_owner }}
|
||||||
|
run: |
|
||||||
|
SHORT_SHA="${SHA:0:7}"
|
||||||
|
IMAGE="${REGISTRY}/${OWNER}/calorie-counter-api"
|
||||||
|
docker build \
|
||||||
|
--target ci-image \
|
||||||
|
--build-arg JAR_FILE=target/calorie-counter-backend-1.0.0-SNAPSHOT.jar \
|
||||||
|
-t "${IMAGE}:${SHORT_SHA}" \
|
||||||
|
-t "${IMAGE}:latest" \
|
||||||
|
backend/
|
||||||
|
docker push "${IMAGE}:${SHORT_SHA}"
|
||||||
|
docker push "${IMAGE}:latest"
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy to Pi — pull & restart stack
|
||||||
|
runs-on: pi-runner
|
||||||
|
needs: [build-api]
|
||||||
|
if: >
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
github.event.workflow_run.conclusion == 'success'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Pull and restart production stack
|
||||||
|
# Run docker compose against the Pi HOST's homelab directory.
|
||||||
|
# Volume paths in a `docker run` issued via the mounted socket are
|
||||||
|
# resolved against the Docker DAEMON's filesystem (the Pi host), not
|
||||||
|
# the job container's filesystem — standard DinD-via-socket behaviour.
|
||||||
|
# The .env file in that directory supplies all production secrets so
|
||||||
|
# no secrets need to be passed into this job.
|
||||||
|
env:
|
||||||
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v /home/andris/homelab/calorie-counter:/project \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-e REGISTRY_PASSWORD \
|
||||||
|
-e REGISTRY_USERNAME \
|
||||||
|
-e "REGISTRY=${REGISTRY}" \
|
||||||
|
-w /project \
|
||||||
|
docker:27.5.1-cli \
|
||||||
|
sh -c '
|
||||||
|
echo "$REGISTRY_PASSWORD" | docker login \
|
||||||
|
--username "$REGISTRY_USERNAME" \
|
||||||
|
--password-stdin "$REGISTRY" &&
|
||||||
|
docker compose pull &&
|
||||||
|
docker compose up -d &&
|
||||||
|
echo "=== Stack status ===" &&
|
||||||
|
docker compose ps
|
||||||
|
'
|
||||||
|
|
||||||
|
# ── Build failure notification ───────────────────────────────────────────────
|
||||||
|
notify-failure:
|
||||||
|
name: Notify — build failure
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build-api, deploy]
|
||||||
|
# Only run when something failed; skip on success or cancellation
|
||||||
|
if: failure()
|
||||||
|
steps:
|
||||||
|
- name: Post commit comment on failure
|
||||||
|
env:
|
||||||
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
OWNER: ${{ gitea.repository_owner }}
|
||||||
|
SHA: ${{ gitea.event.workflow_run.head_sha || gitea.sha }}
|
||||||
|
RUN_URL: ${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id }}
|
||||||
|
run: |
|
||||||
|
BODY="❌ **Docker build failed** for \`${SHA:0:7}\`\n\nSee the failed run: ${RUN_URL}"
|
||||||
|
curl -sf -X POST \
|
||||||
|
"https://git.amlab.dev/api/v1/repos/${OWNER}/calorie-counter/commits/${SHA}/comments" \
|
||||||
|
-H "Authorization: token ${REGISTRY_PASSWORD}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"body\": \"${BODY}\"}" \
|
||||||
|
&& echo "Failure comment posted" \
|
||||||
|
|| echo "Warning: could not post comment (non-fatal)"
|
||||||
51
backend/Dockerfile
Normal file
51
backend/Dockerfile
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Generated by GitHub Copilot
|
||||||
|
#
|
||||||
|
# Build stage — Java 21 LTS + Maven (used only for local dev via --target local).
|
||||||
|
# The CI workflow builds the JAR natively on the Pi runner before calling docker build
|
||||||
|
# to avoid QEMU's unreliable TLS stack causing Maven Central downloads to time out.
|
||||||
|
FROM eclipse-temurin:21-jdk-alpine AS build
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
# Download Maven via curl — avoids apk's OpenJDK pulling in a conflicting JDK version.
|
||||||
|
ENV MAVEN_VERSION=3.9.9
|
||||||
|
RUN apk add --no-cache curl && \
|
||||||
|
curl -fsSL https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
|
||||||
|
| tar -xz -C /opt && \
|
||||||
|
ln -s /opt/apache-maven-${MAVEN_VERSION} /opt/maven
|
||||||
|
ENV PATH="/opt/maven/bin:${PATH}"
|
||||||
|
|
||||||
|
COPY pom.xml .
|
||||||
|
# Pre-fetch deps as a cached layer; rebuilds on source-only changes skip this step.
|
||||||
|
RUN mvn -q -ntp dependency:go-offline || true
|
||||||
|
|
||||||
|
COPY src src
|
||||||
|
RUN mvn -q -ntp clean package -DskipTests
|
||||||
|
|
||||||
|
# ── Runtime base — shared between local and ci-image targets ──────────────────
|
||||||
|
FROM eclipse-temurin:21-jre-alpine AS runtime-base
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Non-root user — principle of least privilege (REQ-SEC-001)
|
||||||
|
RUN addgroup -S caloriecounter && adduser -S caloriecounter -G caloriecounter
|
||||||
|
USER caloriecounter
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Container-aware heap sizing: use 75% of cgroup memory limit
|
||||||
|
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75"
|
||||||
|
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
|
||||||
|
|
||||||
|
# ── local target ──────────────────────────────────────────────────────────────
|
||||||
|
# Copies JAR from the Maven build stage above.
|
||||||
|
# Usage: docker build --target local -t calorie-counter-api .
|
||||||
|
FROM runtime-base AS local
|
||||||
|
COPY --from=build /workspace/target/*.jar app.jar
|
||||||
|
|
||||||
|
# ── ci-image target (DEFAULT) ─────────────────────────────────────────────────
|
||||||
|
# Copies a pre-built JAR from the Docker build context.
|
||||||
|
# The CI workflow builds the JAR natively (outside QEMU) before docker build,
|
||||||
|
# avoiding 20-min Maven downloads through QEMU's unreliable TLS stack.
|
||||||
|
# Usage: docker build --build-arg JAR_FILE=target/calorie-counter-backend-1.0.0-SNAPSHOT.jar .
|
||||||
|
FROM runtime-base AS ci-image
|
||||||
|
ARG JAR_FILE=target/*.jar
|
||||||
|
COPY ${JAR_FILE} app.jar
|
||||||
@@ -86,6 +86,12 @@
|
|||||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Actuator — liveness/readiness probes for Docker healthcheck -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Test -->
|
<!-- Test -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -36,6 +36,18 @@ openai:
|
|||||||
openfoodfacts:
|
openfoodfacts:
|
||||||
base-url: https://world.openfoodfacts.org
|
base-url: https://world.openfoodfacts.org
|
||||||
|
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
exposure:
|
||||||
|
include: health
|
||||||
|
endpoint:
|
||||||
|
health:
|
||||||
|
probes:
|
||||||
|
# Enable liveness + readiness probes — used by Docker HEALTHCHECK
|
||||||
|
enabled: true
|
||||||
|
show-details: never
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
root: WARN
|
root: WARN
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
const BASE_URL = process.env.API_BASE_URL ?? 'http://localhost:8080';
|
// Production: https://calories.amlab.dev (Nginx Proxy Manager → Pi:8085)
|
||||||
|
// Local dev: http://10.18.1.135:8085 (Pi LAN, via WireGuard if off-network)
|
||||||
|
// Override: set API_BASE_URL env var in your .env file
|
||||||
|
const BASE_URL = process.env.API_BASE_URL ?? 'https://calories.amlab.dev';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: BASE_URL,
|
baseURL: BASE_URL,
|
||||||
|
|||||||
Reference in New Issue
Block a user