diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml new file mode 100644 index 0000000..c7295a2 --- /dev/null +++ b/.gitea/workflows/docker.yml @@ -0,0 +1,76 @@ +name: Docker + +on: + push: + branches: [master, main] + tags: ["v*"] + pull_request: + branches: [master, main] + +env: + REGISTRY: git.govekar.net + IMAGE: lovrog05/localmodelroutex + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker meta (standard) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha-,format=short + + - name: Docker meta (bundled) + id: meta-bundled + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE }} + tags: | + type=ref,event=branch,suffix=-bundled + type=semver,pattern={{version}},suffix=-bundled + type=semver,pattern={{major}}.{{minor}},suffix=-bundled + type=sha,prefix=sha-,format=short,suffix=-bundled + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Build and push — standard + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=standard + cache-to: type=gha,mode=max,scope=standard + platforms: linux/amd64,linux/arm64 + + - name: Build and push — bundled (postgres included) + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.bundled + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta-bundled.outputs.tags }} + labels: ${{ steps.meta-bundled.outputs.labels }} + cache-from: type=gha,scope=bundled + cache-to: type=gha,mode=max,scope=bundled + platforms: linux/amd64,linux/arm64 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..57846b0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# ── Build stage ────────────────────────────────────────────────────────────── +FROM node:22-alpine AS ui +WORKDIR /src/dashboard +COPY backend/dashboard/package*.json ./ +RUN npm ci +COPY backend/dashboard/ ./ +RUN npm run build + +FROM golang:1.24-alpine AS go-build +WORKDIR /src +COPY backend/go.mod backend/go.sum ./ +RUN go mod download +COPY backend/ . +COPY --from=ui /src/dashboard/../internal/web/dist ./internal/web/dist +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /lmr ./cmd/server + +# ── Runtime stage ───────────────────────────────────────────────────────────── +FROM gcr.io/distroless/static-debian12:nonroot +COPY --from=go-build /lmr /lmr +EXPOSE 8080 +ENTRYPOINT ["/lmr"] diff --git a/Dockerfile.bundled b/Dockerfile.bundled new file mode 100644 index 0000000..e522451 --- /dev/null +++ b/Dockerfile.bundled @@ -0,0 +1,36 @@ +# ── Build stage ────────────────────────────────────────────────────────────── +FROM node:22-alpine AS ui +WORKDIR /src/dashboard +COPY backend/dashboard/package*.json ./ +RUN npm ci +COPY backend/dashboard/ ./ +RUN npm run build + +FROM golang:1.24-alpine AS go-build +WORKDIR /src +COPY backend/go.mod backend/go.sum ./ +RUN go mod download +COPY backend/ . +COPY --from=ui /src/dashboard/../internal/web/dist ./internal/web/dist +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /lmr ./cmd/server + +# ── Runtime stage (Postgres bundled) ───────────────────────────────────────── +FROM alpine:3.21 + +RUN apk add --no-cache postgresql16 postgresql16-client su-exec bash + +COPY --from=go-build /lmr /usr/local/bin/lmr +COPY deploy/entrypoint-bundled.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENV PGDATA=/var/lib/postgresql/data +ENV PGUSER=lmr +ENV PGPASSWORD=lmr +ENV PGDATABASE=lmr +# The gateway reads GATEWAY_DB_URL; set a default that points to the local PG. +ENV GATEWAY_DB_URL="postgres://lmr:lmr@localhost:5432/lmr?sslmode=disable" + +VOLUME ["/var/lib/postgresql/data"] +EXPOSE 8080 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/deploy/entrypoint-bundled.sh b/deploy/entrypoint-bundled.sh new file mode 100644 index 0000000..3eaa792 --- /dev/null +++ b/deploy/entrypoint-bundled.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +PGDATA="${PGDATA:-/var/lib/postgresql/data}" +PGUSER="${PGUSER:-lmr}" +PGPASSWORD="${PGPASSWORD:-lmr}" +PGDATABASE="${PGDATABASE:-lmr}" + +# ── Initialise cluster if needed ───────────────────────────────────────────── +if [ ! -f "$PGDATA/PG_VERSION" ]; then + echo "→ initialising postgres cluster" + install -d -m 0700 -o postgres "$PGDATA" + su-exec postgres initdb \ + --username="$PGUSER" \ + --pwfile=<(echo "$PGPASSWORD") \ + --auth=md5 \ + -D "$PGDATA" \ + --no-instructions +fi + +# ── Start postgres in the background ───────────────────────────────────────── +echo "→ starting postgres" +su-exec postgres postgres -D "$PGDATA" \ + -c listen_addresses=localhost \ + -c log_min_messages=WARNING & +PG_PID=$! + +# ── Wait for postgres to accept connections ─────────────────────────────────── +echo "→ waiting for postgres" +until su-exec postgres pg_isready -U "$PGUSER" -d "$PGDATABASE" -q 2>/dev/null; do + sleep 0.5 +done + +# ── Ensure the application database exists ─────────────────────────────────── +su-exec postgres psql -U "$PGUSER" -tc \ + "SELECT 1 FROM pg_database WHERE datname = '$PGDATABASE'" \ + | grep -q 1 || su-exec postgres createdb -U "$PGUSER" "$PGDATABASE" + +# ── Start the gateway (foreground, takes over the process) ─────────────────── +echo "→ starting lmr" +# Forward signals to both processes on exit. +trap 'kill $PG_PID 2>/dev/null; wait $PG_PID' TERM INT + +exec /usr/local/bin/lmr