English

Jak wdrażam tę stronę

jchs.software działa na maszynach w moim homelabie. DevOps to moje hobby, a własna strona to bezpieczne miejsce, żeby je ćwiczyć.

Poniżej droga, którą commit przechodzi od git push do produkcji.

W skrócie

  1. Robię push na main.
  2. Zadanie bierze runner GitHub Actions na mojej własnej maszynie.
  3. Runner instaluje zależności, puszcza linter i testy, buduje stronę i sprawdza budżety JavaScriptu.
  4. Podman buduje obraz kontenera i wysyła go do prywatnego rejestru.
  5. Runner mówi serwerowi z aplikacją, który commit ma działać. Resztę robi serwer.

Pull requesty też przechodzą kroki od 2 do 4, więc każdy PR ma swój obraz. Do kroku 5 dochodzi tylko push na main.

Build

Strona to aplikacja Next.js budowana z output: "standalone". Wynikiem jest server.js i tylko te paczki, których naprawdę używa, więc obraz jest mały.

Cały Containerfile:

FROM node:24-alpine
WORKDIR /app

COPY .next/standalone ./
COPY public ./public
COPY .next/static ./public/_next/static

ENV HOSTNAME=0.0.0.0
ENV PORT=3000
EXPOSE 3000
CMD ["node", "server.js"]

Build odbywa się na runnerze. Obraz tylko kopiuje jego wynik. Oba używają tej samej głównej wersji Node, więc testuję dokładnie to, co potem działa.

Sprawdzenia przed obrazem

Cztery kroki mogą zatrzymać przebieg, zanim powstanie jakikolwiek obraz:

- name: Lint
  run: npm run lint

- name: Test
  run: npm test

- name: Build Next.js (standalone)
  run: npm run build

- name: Check bundle budgets
  run: npm run check:bundle

Ostatni to mały skrypt. Sumuje rozmiar gzip JavaScriptu, który strona ładuje na starcie, i kończy się błędem, gdy suma przekracza budżet. Biurko 3D ładuje się później i ma własny budżet.

Przeniesienie tego sprawdzenia do CI czegoś mnie nauczyło. Ten sam build, bajt w bajt, na Node 24 ważył o 206 bajtów więcej niż na Node 22, bo każda z tych wersji ma inną wersję zlib. U siebie miałem 44 bajty zapasu. Na Node 24, czyli na wersji z CI, byłem 162 bajty ponad limitem.

Limit zostawiłem i wyciąłem kod. Przełącznik języka używał next/link, ale przejście z / na /pl i tak przeładowuje całą stronę, więc zwykły <a> robi to samo przy 3 KB mniej JavaScriptu.

Tagi obrazu

Każdy obraz dostaje więcej niż jeden tag:

Tag Kiedy Po co
sha- + 7 znaków zawsze nigdy się nie przesuwa, więc mogę wrócić do każdego builda
pr- + numer pull requesty najnowszy build danego PR-a
main i latest push na main to, co działa na produkcji
vX.Y.Z tagi gita wydania

Wdrożenie

Runner i aplikacja stoją na różnych maszynach. Runner łączy się po SSH kluczem, który służy tylko do wdrożeń. Na serwerze ten klucz jest przypięty do jednego skryptu, więc nie da się nim otworzyć powłoki.

CI wysyła jedną rzecz, SHA commita:

- name: Deploy
  run: ssh -i "$HOME/.ssh/$SSH_KEY" -o BatchMode=yes "$REMOTE_USER@$REMOTE_HOST" "$GITHUB_SHA"

Dalej działa skrypt na serwerze. Na koniec sprawdza, czy uruchomiona wersja to dokładnie ten commit.

Wdrożenia z main idą jedno po drugim i nigdy nie są przerywane w połowie. Buildy PR-ów działają odwrotnie: nowy push anuluje poprzedni build.

Co stoi przed aplikacją

Żądania idą przez Cloudflare do gatewaya w mojej sieci. Nginx na gatewayu jest reverse proxy i przekazuje je do maszyny wirtualnej, na której działa kontener.

I to wszystko

Około stu linii YAML-a, krótki Containerfile i jeden skrypt na serwerze.

Wszystkie wpisy