Nuxt + Strapi: Hosting und Deployment Pipeline

Ich habe mich damit beschäftigt, wie ich mein Nuxt und Strapi Konstrukt mithilfe von GitHub Actions auf meinen Server deployed bekomme.

Nuxt + Strapi: Hosting und Deployment Pipeline
Photo by 夜 咔罗 / Unsplash

Zuletzt habe ich beschrieben, wie ich ein Projekt mit Nuxt im Frontend und Strapi im Backend gestartet habe.

Die lokale Entwicklung war cool und einfach: npm run start bei Strapi, npm run dev bei Nuxt und alles war up and running. Als ich mit dem ersten Wurf fertig war, stellte ich mir dann natürlich die Frage, wie das Ganze jetzt a) auf dem Server läuft und b) nicht mehr im Development-Modus läuft.

Probieren geht über Studieren. Und im besten Fall entsteht daraus ein Blogpost 😅

Was ich noch nicht erwähnt habe, ist, dass ich alle meine Projekte in privaten GitHub-Repositories speichere. Bike-Note ist also eins davon und hat für das Deployment den neuen Ordner .github/workflows spendiert bekommen.

Nachdem ich mir ein paar Gists angeschaut habe, habe ich mir folgende main.yml zusammengebaut:

name: Deploy
on:
  push:
    branches:
      - main
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: rsync deployments
        uses: burnett01/rsync-deployments@6.0.0
        with:
          switches: -hvrPt
          path: ./
          remote_path: ${{ secrets.SSH_PATH }}
          remote_host: ${{ secrets.SSH_HOST }}
          remote_port: ${{ secrets.SSH_PORT }}
          remote_user: ${{ secrets.SSH_USER }}
          remote_key: ${{ secrets.SSH_KEY }}

Von oben nach unten kurz erklärt:
Als Trigger habe ich einen Push in den main Branch gewählt, da ich nur alleine an dem Projekt arbeite und noch lange nicht so weit bin, dass sich Release-Versionen lohnen würden. Ein Blick in die Doku zeigt, dass es wirklich unzählige Varianten gibt, um einen Workflow anzustoßen.

Mein deploy Job läuft, wie mein eigener Server, auf einem Ubuntu-Server. Geht sicher auch mit einem kleineren Image, aber gerade wenn man hier ein bisschen herumprobieren will, macht es wahrscheinlich weniger Spaß, wenn der Job immer failed, weil man irgendwelche Packages nachinstallieren muss.

Der erste Schritt beim Deployment ist logischerweise das Auschecken des Repositorys. Auch hier gibt es mehr als eine Handvoll Optionen, die ich aber alle auf Default belassen habe.

Der zweite Schritt war das Kopieren auf meinen Server. Auch hier habe ich vorerst mal auf eine Near Zero Downtime¹ Logik verzichtet und kopiere es immer in denselben Ordner. Man kann hier auf scp oder auf rsync setzen; für beide Varianten gibt es Actions aus der Community. Da ich immer in den gleichen Ordner schreibe, dürfte rsync ein kleines bisschen schneller sein.

Als Parameter, welcher bei der Action als switches angegeben werden, habe ich folgende gewählt:

  • [h]uman-readable damit die Größen in kB und MB angezeigt werden
  • [v]erbose zum debuggen
  • [r]ecursively damit auch alle Ordnerinhalte kopiert werden
  • [P]rogress damit man was zu sehen hat
  • [t]ime damit die Erstellzeit der Quelldatei beibehalten wird

Und den Rest habe ich in Secrets ausgelagert, die man unter Settings > Secrets and variables > Actions konfigurieren kann. Hier bleibt es einem selbst überlassen, mit welchem User man deployt, was natürlich auch bei mir ein Betriebsgeheimnis bleibt.

¹ Bei einer Near Zero Downtime Logik geht der Webserver auf einen Symlink, welcher auf den letzten Release zeigt. z.B.:

./releases/7
./releases/8
./releases/9 --> letzter Release

./current/ --> Symlink auf ./releases/9

Beim Deploy würde in diesem Beispiel ./releases/10 erstellt werden und erst wenn alle Dateien kopiert sind, ändert sich der Symlink. Dadurch gibt es praktisch keine Downtime.

Nachdem die Dateien am gewünschten Ort auf dem Server lagen, durfte ich mich um das Docker-Hosting kümmern. Damit bin ich noch nicht hundertprozentig zufrieden, aber hier im Blog geht es ja auch um den Fortschritt und nicht um durchgeplante Tutorials.

Vereinfacht ohne Network und traefik-Labels sieht meine docker-compose.yml folgendermaßen aus:

version: "3"
services:
  api:
    build: api
    restart: unless-stopped
    environment:
      STRAPI_ADMIN_BACKEND_URL: ${STRAPI_APP_URL}
      APP_KEYS: ${STRAPI_APP_KEY}
      API_TOKEN_SALT: ${STRAPI_API_TOKEN_SALT}
      ADMIN_JWT_SECRET: ${STRAPI_ADMIN_JWT_SECRET}
      TRANSFER_TOKEN_SALT: ${STRAPI_TRANSFER_TOKEN_SALT}
      JWT_SECRET: ${STRAPI_JWT_SECRET}
    volumes:
      - ./data:/app/.tmp
  client:
    build: client
    restart: unless-stopped

Sowohl Frontend als auch Backend werden also mit einem Dockerfile gebuilded, was in Zukunft noch angepasst werden muss. Hierbei gibt es die Option, das bereits in der GitHub Action builded oder eben das Docker Build über die GitHub Action ausgeführt werden muss.

Der Grund, warum ich das nicht auf die Schnelle gemacht habe, war neben dem Zeitdruck, es an einem Wochenende fertig zu machen, dass ich pnpm verwenden wollte. Ja, manchmal setze ich mir Dinge in den Kopf, die mir dann die Welt erschweren.

Ich bin auch nicht super tief im npm-Game, aber für alle, die noch weniger tief drin sind als ich:
- npm ist absolut basic; hiermit funktioniert alles
- yarn ist schneller und deswegen cooler
- bun ist der heiße Scheiß, weil es superschnell ist, ich aber die Kehrseite noch nicht kenne
- pnpm ist natürlich auch schneller, glänzt aber durch das Lockfile, welches in YAML geschrieben ist und keine Merge-Konflikte erzeugt

Ich hatte durch pnpm also ein wunderschönes Lockfile, aber leider Permission-Probleme und da es Sonntag war und ich es Live bekommen wollte, entschied ich mich für Dockerfiles.

Für die Strapi API:

FROM node:20-slim
RUN corepack enable
ENV NODE_ENV=production

COPY . /app
WORKDIR /app

RUN pnpm install
RUN pnpm build

EXPOSE 1337
CMD ["pnpm", "start"]

und für das Nuxt Frontend:

FROM node:20-slim
RUN corepack enable

COPY . /app
WORKDIR /app

RUN pnpm install
RUN pnpm build

EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

Ich musste zwar den Strapi-Container vier oder fünf Mal starten, weil hier und da Secrets, Tokens und Keys fehlten, aber nachdem alles eingerichtet war, konnte ich das Frontend ohne Probleme aufrufen.

Alles in allem also ein erfolgreiches Wochenende, bis auf ein oder zwei Todos, die noch zu erledigen sind.Insgesamt also ein erfolgreiches Wochenende außer dass ein oder zwei Todos noch offen sind.