Blog

Einrichten eines Servers für die cloudbasierte Webentwicklung (3/4)

Veröffentlicht vor einem Monat

authelia
code-server
docker
grafana
traefik
Illustration zum Blogeintrag

Überblick

Einleitung

Im letzten Teil haben wir die die Infrastruktur unseres Servers definiert, die wir für den sicheren Betrieb von code-server benötigen.

Nun kommen wir im dritten Teil endlich zum Herzstück des gesamten Setups: code-server.

Für code-server wie auch für VSCode gibt es sehr viele Erweiterungen und Themes, so dass wahrscheinich jeder Entwickler sein eigenes ganz individuelles Setup hat, mit dem er sich am wohlsten fühlt. Ich zeige hier, wie ich mein Setup eingerichtet habe.

"Friendly Interactive SHell"

Für die Kommandozeile nutze ich die "Friendly Interactive SHell" (fish), die es erlaubt, einige Anpassungen vorzunehmen und zusätzlich bereits eingegebene Kommandos beim nächsten Mal automatisch vervollständigen kann. Im Prinzip also "Code Completion" für die Kommandozeile.

Die fish- Shell werden wir später direkt in das Docker- Image installieren. Zusätzlich werden wir das Theme "emoji-powerline" nutzen. Dafür müssen die beiden Dateien fish_prompt.fish und fish_right_prompt.fish von der Github- Seite heruntergeladen und im lokalen Ordner ./code-server/config abgelegt werden. Aus diesem Ordner werden die Dateien dann später während des Build- Prozesses in das Image kopiert.

Fonts

Seit Längerem nutze ich als Schriftart in VSCode bzw. dann auch in code-server sehr gerne Cascadia Code, ein Monospace Font, der auch Code Ligatures unterstützt. Das sind spezielle Symbole, die anstatt von mehreren zusammenhängenden Zeichen angezeigt werden und den Code ein bisschen lesbarer machen. So wird zum Beispiel aus => ein richtiger Pfeil (⇒).

Und um unserem Motto treu zu bleiben, die Installation auf dem Server so einfach wie möglich zu gestalten, werden wir auch die Dateien für die Schriftart bereits in ein eigenes Docker- Image integrieren.

Dazu wird im Repository ein neuer Ordner ./code-server/fonts angelegt, in dem wir die woff2- Dateien kopieren, die von der Cascadia Github Seite heruntergeladen wurden.

Einstellungen für code-server

Des Weiteren wollen wir natürlich auch die Einstellungen in code-server selbst im Github- Repository speichern und wie alle Konfigurationen bisher auch mit dem eigenen code-server- Image bereitstellen.

Wir erstellen also im Ordner ./code-server/config die Datei settings.json, in die wir die Einstellungen aus VSCode komplett übernehmen können. Hier mal exemplarisch meine Einstellungen:

// ./code-server/config/settings.json

{
  "workbench.colorTheme": "Cobalt2",
  "todo-tree.tree.showScanModeButton": false,
  "todo-tree.general.tags": ["BUG", "HACK", "FIXME", "TODO", "XXX", "[ ]", "[x]"],
  "todo-tree.regex.regex": "(//|#|<!--|;|/\\*|^|^\\s*(-|\\d+.))\\s*($TAGS)",
  "workbench.iconTheme": "material-icon-theme",
  "editor.fontFamily": "'Cascadia Code', Consolas, 'Courier New', monospace",
  "editor.fontWeight": "600",
  "editor.fontLigatures": true,
  "terminal.integrated.fontWeight": 600,
  "editor.tabSize": 2,
  "explorer.openEditors.visible": 0,
  "html.format.wrapLineLength": 100,
  "editor.minimap.enabled": false,
  "editor.suggestSelection": "first",
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.detectIndentation": true,
  "editor.rulers": [100],
  "editor.snippetSuggestions": "top",
  "editor.wordBasedSuggestions": false,
  "editor.suggest.localityBonus": true,
  "editor.acceptSuggestionOnCommitCharacter": false,
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.suggestSelection": "recentlyUsed",
    "editor.suggest.showKeywords": false
  },
  "editor.renderWhitespace": "boundary",
  "files.exclude": {
    "USE_GITIGNORE": true
  },
  "files.defaultLanguage": "{activeEditorLanguage}",
  "javascript.validate.enable": false,
  "search.exclude": {
    "**/node_modules": true,
    "**/bower_components": true,
    "**/coverage": true,
    "**/dist": true,
    "**/build": true,
    "**/.build": true,
    "**/.gh-pages": true
  },
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": false
  },
  "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
  "eslint.options": {
    "overrideConfig": {
      "env": {
        "browser": true,
        "es6": true
      },
      "parserOptions": {
        "ecmaVersion": 2019,
        "sourceType": "module",
        "ecmaFeatures": {
          "jsx": true
        }
      },
      "rules": {
        "no-debugger": "off"
      }
    }
  },
  "breadcrumbs.enabled": true,
  "grunt.autoDetect": "off",
  "gulp.autoDetect": "off",
  "npm.runSilent": true,
  "explorer.confirmDragAndDrop": false,
  "editor.formatOnPaste": false,
  "editor.cursorSmoothCaretAnimation": true,
  "editor.smoothScrolling": true,
  "php.suggest.basic": false,
  "workbench.tree.indent": 16,
  "editor.lineHeight": 20,
  "editor.formatOnSave": true,
  "prettier.documentSelectors": ["*.md"],
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "files.autoSave": "off",
  "conventionalCommits.showNewVersionNotes": false,
  "terminal.integrated.defaultProfile.linux": "fish",
  "git.confirmSync": false,
  "files.associations": {
    "*.md": "mdx"
  }
}

Ich werde jetzt nicht ins Detail gehen, was die einzelnen Einstellungen jeweils bewirken. Dazu sind es einfach zu viele und jeder hat sich seinen VSCode ja auch ganz individuell angepasst.

Dockerfile

Nach diesen ganzen Vorarbeiten können wir nun als Nächstes das Dockerfile erstellen, das die einzelnen Schritte für den Build unseres eigenen code-server- Images definiert.

Als Basis verwende ich ein Image für code-server von linuxserver.io. Jedoch wird nicht die Neueste Version verwendet, da die Entwickler nach der Version 4.0.2 etwas geändert haben, so dass die Schriftarten an einem anderen Ort abgelegt werden müssen. Und ich hatte bisher nicht die Muße, mir das im Detail anzuschauen.

Das Dockerfile erstellen wir also im Ordner ./code-server und fügen den folgenden Inhalt ein:

# ./code-server/Dockerfile

# base image
FROM linuxserver/code-server:v4.0.2-ls111

# install updates
RUN apt-get update --quiet && apt-get upgrade --yes --quiet

# install nodejs, npm amd yarn
RUN set -ex; \
  curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add; \
  echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list; \
  apt-get update --quiet && apt-get install --yes --quiet yarn; \
  node --version; \
  npm --version; \
  yarn --version

# install pnpm
RUN npm install -g pnpm

# add ssh key to access github
RUN mkdir -p /config/.ssh
RUN --mount=type=secret,id=CODE_SERVER_PRIVATE_SSH_KEY_GITHUB cat /run/secrets/CODE_SERVER_PRIVATE_SSH_KEY_GITHUB > /config/.ssh/id_rsa
RUN chmod 600 /config/.ssh/id_rsa

# install fish shell and customize the theme
RUN apt-get install --yes --quiet fish

# apply customizations to fish theme
COPY ./config/fish_prompt.fish /usr/share/fish/functions/
COPY ./config/fish_right_prompt.fish /usr/share/fish/functions/

# install extensions (look for other extensions on https://open-vsx.org/)
RUN set -ex; \
  code-server  --extensions-dir=/config/extensions \
    --install-extension redwan-hossain.auto-rename-tag-clone \
    --install-extension mgmcdermott.vscode-language-babel \
    --install-extension wesbos.theme-cobalt2 \
    --install-extension vivaxy.vscode-conventional-commits \
    --install-extension hediet.vscode-drawio \
    --install-extension dsznajder.es7-react-js-snippets \
    --install-extension dbaeumer.vscode-eslint \
    --install-extension mhutchie.git-graph \
    --install-extension heybourn.headwind \
    --install-extension bierner.markdown-preview-github-styles \
    --install-extension PKief.material-icon-theme \
    --install-extension anwar.papyrus-pdf \
    --install-extension esbenp.prettier-vscode \
    --install-extension humao.rest-client \
    --install-extension bradlc.vscode-tailwindcss \
    --install-extension Gruntfuggly.todo-tree \
    --install-extension redhat.vscode-yaml \
    --install-extension silvenon.mdx \
    --install-extension prisma.prisma

# install font (https://github.com/coder/code-server/issues/1374, https://github.com/tonsky/FiraCode)
COPY ./fonts /usr/local/share/.config/yarn/global/node_modules/code-server/vendor/modules/code-oss-dev/out/vs/code/browser/workbench/fonts
RUN sed -i "s|<head>|\
  <style> \n\
    @font-face { \n\
      font-family: 'Cascadia  Code'; \n\
      src: url('static/out/vs/code/browser/workbench/fonts/CascadiaCodePL-Light.woff2') format('woff2'); \n\
      font-weight: 300; \n\
      font-style: normal; \n\
    } \n\
    @font-face { \n\
      font-family: 'Cascadia  Code'; \n\
      src: url('static/out/vs/code/browser/workbench/fonts/CascadiaCodePL-LightItalic.woff2') format('woff2'); \n\
      font-weight: 300; \n\
      font-style: italic; \n\
    } \n\
    @font-face { \n\
      font-family: 'Cascadia Code'; \n\
      src: url('static/out/vs/code/browser/workbench/fonts/CascadiaCodePL-Regular.woff2') format('woff2'); \n\
      font-weight: 400; \n\
      font-style: normal; \n\
    } \n\
    @font-face { \n\
      font-family: 'Cascadia Code'; \n\
      src: url('static/out/vs/code/browser/workbench/fonts/CascadiaCodePL-Italic.woff2') format('woff2'); \n\
      font-weight: 400; \n\
      font-style: italic; \n\
    } \n\
    @font-face { \n\
      font-family: 'Cascadia Code'; \n\
      src: url('static/out/vs/code/browser/workbench/fonts/CascadiaCodePL-SemiBold.woff2') format('woff2'); \n\
      font-weight: 600; \n\
      font-style: normal; \n\
    } \n\
    @font-face { \n\
      font-family: 'Cascadia Code'; \n\
      src: url('static/out/vs/code/browser/workbench/fonts/CascadiaCodePL-SemiBoldItalic.woff2') format('woff2'); \n\
      font-weight: 600; \n\
      font-style: italic; \n\
    } \n\
    @font-face { \n\
      font-family: 'Cascadia Code'; \n\
      src: url('static/out/vs/code/browser/workbench/fonts/woff2/CascadiaCode-Bold.woff2') format('woff2'); \n\
      font-weight: 700; \n\
      font-style: normal; \n\
    } \n\
    @font-face { \n\
      font-family: 'Cascadia Code'; \n\
      src: url('static/out/vs/code/browser/workbench/fonts/woff2/CascadiaCode-BoldItalic.woff2') format('woff2'); \n\
      font-weight: 700; \n\
      font-style: italic; \n\
    } \n\
  \n\</style><head>|g" /usr/local/share/.config/yarn/global/node_modules/code-server/vendor/modules/code-oss-dev/out/vs/code/browser/workbench/workbench.html

# copy settings file
RUN mkdir -p /config/data/User
COPY ./config/settings.json /config/data/User/settings.json

# set git config
RUN set -ex; \
  git config --global user.name "<GITHUB_USERNAME>"; \
  git config --global user.email "<ME@EMAIL.COM>"

Die Schritte sind im Einzelnen:

  • Aktualisieren des zugrundeliegenden Betriebssystems (Ubuntu)
  • Installation des für die Webentwicklung unerlässlichen NodeJS sowie der Paketmanager npm, yarn und pnpm.
  • Außerdem fügen wir einen privaten SSH- Key hinzu, für den wir den öffentlichen Schlüssel bei Github hinterlegen, so dass wir vom code-server auf den eigenen Github- Account zugreifen können. Dazu muss dieses Schlüsselpaar natürlich erst erzeugt werden. Für den privaten Schlüssel erstellen wir in den Einstellungen auf Github eine neue Umgebungsvariable mit dem Namen CODE_SERVER_PRIVATE_SSH_KEY_GITHUB.
  • Installation der fish- Shell und kopieren des Themes
  • Installation der VSCode- Erweiterungen. Achtung: Für code-server können nicht die Erweiterungen aus dem VSCode- Marketplace verwendet werden. Auf der Seite https://open-vsx.org/ kann aber nach kompatiblen Erweiterungen gesucht werden, die in den meisten Fällen gleich heißen wie die offiziellen VSCode- Erweiterungen.
  • Kopieren der Font- Dateien und die Nutzung dieser Schriftarten definieren
  • Kopieren der Datei mit den VSCode- Einstellungen
  • Nutzernamen und E-Mail- Adresse für git und Github setzen

Github- Action

Nun sind alle Voraussetzungen geschaffen, um unser eigenes Image von code-server zu erstellen. Dazu definieren wir wieder eine Github Action, die das Image automatisch erneut erstellt, sobald Änderungen an den Konfigurationsdateien per Commit auf Github gepusht werden.

# ./.github/workflows/docker-build-code-server.yml

name: build custom image for code-server

on:
  push:
    branches:
      - 'master'
    paths:
      - 'code-server/**'

jobs:
  build-code-server:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2

      - name: Set up Docker BuildX
        uses: docker/setup-buildx-action@v1

      - name: login to GitHub Container Registry
        uses: docker/login-action@v1
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and Push the custom Code-Server Image
        uses: docker/build-push-action@v2
        with:
          context: ./code-server
          push: true
          tags: ghcr.io/<GITHUB_USERNAME>/code-server:latest
          secrets: |
            "CODE_SERVER_PRIVATE_SSH_KEY_GITHUB=${{ secrets.CODE_SERVER_PRIVATE_SSH_KEY_GITHUB }}"

Das Schema dieser Github Action ähnelt natürlich wieder den Actions aus dem zweiten Teil, die das Image für Authelia oder Traefik erstellen.

Eine Besonderheit ist jedoch, dass wir mit der Umgebungsvariable CODE_SERVER_PRIVATE_SSH_KEY_GITHUB einen privaten SSH- Key, im Docker-Image ablegen, um später auf Github zugreifen zu können. Der dazugehörigen Public Key wird in den Einstellungen von Github gespeichert. Eine Anleitung zum Erstellen von SSH- Keys kann hier gefunden werden.

docker-compose.yml

Als nächstes müssen wir unsere docker-compose.yml erweitern, um den Docker- Container für code-server starten zu können:

# ./docker-compose.yml

volumes:
  # ...
  code-server-data:
    driver: local

services:
  # ...
  code-server:
    image: ghcr.io/<GITHUB_USERNAME>/code-server:latest
    container_name: code-server
    hostname: code-server
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Berlin
      - SUDO_PASSWORD=123
    volumes:
      - code-server-data:/config
    networks:
      - proxy
    expose:
      - 8443
      - 3000 # nextjs & react development server
      - 4200 # nx development server
      - 4400 # nx storybook
    restart: unless-stopped
    labels:
      - 'traefik.enable=true'
      - 'traefik.docker.network=proxy'
      - 'com.centurylinklabs.watchtower.enable=true'

      # code-server app
      - 'traefik.http.routers.code-server.rule=Host(`vscode.server.<MY-DOMAIN>`)'
      - 'traefik.http.routers.code-server.entrypoints=https'
      - 'traefik.http.routers.code-server.tls=true'
      - 'traefik.http.routers.code-server.tls.certresolver=letsencrypt'
      - 'traefik.http.routers.code-server.middlewares=authelia@docker'
      - 'traefik.http.routers.code-server.service=code-server-service'
      - 'traefik.http.services.code-server-service.loadbalancer.server.port=8443'

      # port 3000
      - 'traefik.http.routers.code-server-port-3000.rule=Host(`port-3000.server.<MY-DOMAIN>`)'
      - 'traefik.http.routers.code-server-port-3000.entrypoints=https'
      - 'traefik.http.routers.code-server-port-3000.tls=true'
      - 'traefik.http.routers.code-server-port-3000.tls.certresolver=letsencrypt'
      - 'traefik.http.routers.code-server-port-3000.middlewares=authelia@docker'
      - 'traefik.http.routers.code-server-port-3000.service=code-server-port-3000-service'
      - 'traefik.http.services.code-server-port-3000-service.loadbalancer.server.port=3000'

      # port 4200
      - 'traefik.http.routers.code-server-port-4200.rule=Host(`port-4200.server.<MY-DOMAIN>`)'
      - 'traefik.http.routers.code-server-port-4200.entrypoints=https'
      - 'traefik.http.routers.code-server-port-4200.tls=true'
      - 'traefik.http.routers.code-server-port-4200.tls.certresolver=letsencrypt'
      - 'traefik.http.routers.code-server-port-4200.middlewares=authelia@docker'
      - 'traefik.http.routers.code-server-port-4200.service=code-server-port-4200-service'
      - 'traefik.http.services.code-server-port-4200-service.loadbalancer.server.port=4200'

      # port 4400
      - 'traefik.http.routers.code-server-port-4400.rule=Host(`port-4400.server.<MY-DOMAIN>`)'
      - 'traefik.http.routers.code-server-port-4400.entrypoints=https'
      - 'traefik.http.routers.code-server-port-4400.tls=true'
      - 'traefik.http.routers.code-server-port-4400.tls.certresolver=letsencrypt'
      - 'traefik.http.routers.code-server-port-4400.middlewares=authelia@docker'
      - 'traefik.http.routers.code-server-port-4400.service=code-server-port-4400-service'
      - 'traefik.http.services.code-server-port-4400-service.loadbalancer.server.port=4400'

Auffallend ist hier, dass wir gleich vier Ports freigeben und über den Traefik Reverse Proxy aus dem Internet erreichbar machen. Auf Port 8443 läuft dabei code-server selbst, dieser Port ist also zwingend notwendig. Die anderen Ports werden zum Aufrufen des Entwicklungsservers für verschiedene Frameworks über den Browser benötigt und können individuell angepasst werden.

Wenn man zum Beispiel lokal eine App mit create-react-app initialisiert hat und diese dann mit npm run start startet, kann man diese App standardmäßig auf Port 3000 aufrufen. In unserem Fall würde man dann die Seite über die URL port-3000.server.<MY-DOMAIN> aufrufen können.

"File Watcher Limit"

Beim Start des Entwicklungsservers über zum Beispiel npm run start für React Apps wird für alle Dateien des Projekts ein "File Watcher" mit inotify registriert, so dass Änderungen an den Projektdateien erkannt werden können und die App erneut kompiliert werden kann.

Da aber für alle Dateien, die auf diese Weise überwacht werden, natürlich Resourcen verbraucht werden, ist die Anzahl an Dateien standardmäßig limitiert. Man kann dieses Limit mit dem folgenden Befehl abfragen:

sysctl fs.inotify.max_user_watches

Mit den vielen Node Modules, die für eine Web App genutzt werden, ist dieses Limit aber sehr schnell erreicht und es wird eine Fehlermeldung ausgegeben:

Watchpack Error (watcher): Error: ENOSPC: System limit for number of file watchers reached, watch '/some/path'

Um das Limit zu erhöhen (hier auf 524288) muss der folgende Befehl auf dem Host- System (also direkt auf dem Server) ausgeführt werden:

echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

Docker- Container starten und zum ersten Mal aufrufen

Jetzt haben wir alle Konfigurationen für den Betrieb von code-server und der Bereitstellung des Servers im Internet vorgenommen und können des Repository nach dem Erstellen eines Commit mit dem Befehl git push origin main auf Gihub hochladen.

Wenn wir anschließend auf der Github- Webseite für das Repository den Tab "Actions" aufrufen, sollten wir direkt sehen, dass drei Actions ausgeführt werden (je eine für das Erstellen unserer eigenen Images für Authelia, Traefik und code-server).

Wenn alle Actions erfolgreich fertiggestellt wurden, können wir uns per SSH auf dem Server anmelden und das Repository klonen. Danach wechseln wir in das Verzeichnis des Repositorys, in dem auch die `docker-compose.yml' liegt und können mit dem folgenden Befehl alle notwendigen Container herunterladen und starten:

docker-compose up -d

Beim ersten Start empfiehlt es sich eventuell das -d wegzulassen, so dass alle Meldungen der Container direkt im Terminal ausgegeben werden und eventuell auftretende Fehler leichter erkannt werden können. Für den späteren Betrieb ist dieses Flag für den "detached"- Modus jedoch notwendig, da sonst alle Container gestoppt werden, sobald wir uns vom Server abmelden wollen.

Nachdem alle Container gestartet sind, können wir code-server zum ersten Mal auf der URL vscode.server.<MY-DOMAIN> aufrufen.

Bevor wir jedoch auf den code-server weitergeleitet werden, müssen wir uns bei Authelia anmelden und die Zwei- Faktor- Authentifizierung einrichten. Dazu erhalten wir eine E-Mail mit einem Link, über den ein QR- Code für die Verknüpfung des Smartphones mit Authelia aufgerufen werden kann. Dazu muss einfach der QR- Code mit dem Smartphone und einer App wie zum Beispiel (Google Authenticator](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=de&gl=US) abgescannt werden. Anschließend wird auf dem Smartphone in regelmäßigen Abständen ein sechsstelliger Zahlencode generiert, der dann bei Authelia als zweiter Faktor für die Authentifizierung neben dem Passwort eingegeben werden muss.

Wenn das alles erledigt ist, sollten wir direkt zu code-server weitergeleitet werden.

Willkommensbildschirm von code-server

Theoretisch könnte man jetzt direkt mit dem Coden loslegen. Da aber code-server als "Progressive Web App" (PWA) entwickelt uwrde, kann man code-server in vielen Browsern als Desktopanwendung installieren. Danach lässt sich code-server fast wie eine lokal installierte Instanz von VSCode bedienen

Wie geht es weiter?

Prinzipiell ist der Server für die cloudbasiert Webentwicklung nun komplett und wir könnten mit dem Coden loslegen.

Im letzten Teil zeige ich aber noch wie man ein Monitoring für den Server einrichten kann, so dass man alle wichtigen Leistungsdaten (CPU- Auslastung, Speichernutzung, etc.) überwachen kann.

Teil 4: Server- Monitoring