Categories
Linux

Lokaler virtueller Server

## Motivation
Ich habe schon seit langer Zeit einen virtuellen Server gemietet, auf dem ich verschiedene Dienste, wie zB Webseite und Mail, laufen lasse. Die Anwendungen laufen direkt auf dem Server und ich will sie schon seit langem in Container stecken. Das Betriebssystem ist mit Ubuntu 18.04 LTS hoffnungslos veraltet, aktuell ist 24.04 LTS.
Ein Upgrade von Ubuntu 18 auf 24 ist mit Risiko und vermutlich hohem Aufwand verbunden, da erfahrungsgemäß irgendetwas irgendwie anders funktioniert und mühevoll angepasst werden muss. Grade bei einer Upgradekette 18->20->22->24 kann so einiges schief gehen.
Den Server miete ich seit ein paar Jahren und inzwischen gibt es für das gleiche Geld bessere (virtuelle) Hardware.
Aus diesen Gründen plane ich, einen neuen virtuellen Server zu mieten und auf diesen umzuziehen.

## Wie es auf dem Neuen laufen soll
Auf dem neuen Server soll Ubuntu 24.04 LTS laufen.
Auf dem Linux System soll Docker installiert werden.
Die Docker Container sollen mit Portainer verwaltet werden.
Die Webseite der Portainer Verwaltung und die weiteren Dienste sollen über einen Reverse Proxy, beispielsweise Traefik, erreichbar sein.
Der Reverse Proxy soll den Zugriff ausschließlich verschlüsselt über HTTPS erlauben und entsprechend konfiguriert sein.
Das HTTPS Zertifikat soll von [Let's Encrypt](https://letsencrypt.org/) kommen.

## lokaler virtueller Server
Zuerst möchte ich einen lokalen virtuellen Server einrichten um auf diesem die Migration vorbereiten zu können. Dazu werde ich [Virtual Box](https://www.virtualbox.org/) von Oracle verwenden.

graph TD G[Portainer Webinterface] --> F H[Website: Nginx] --> F I[Mailserver: Postfix/Dovecot] --> F F[Traefik] --> E E[Portainer] --> D D[Docker] --> C C[Ubuntu 24.04 LTS] --> B B[VirtualBox] --> A A[Host System: PC]

Vom Host System (also mein PC, auf dem Virtual Box läuft) aus soll der Zugriff auf die Webseite per Domain Name funktionieren, nicht nur über die IP. Idealerweise sollte die Verschlüsselung mit Let's Encrypt vorgenommen werden, um so realistisch wie möglich das spätere System vorzubauen.
Leider ist das nicht (mit vertretbarem Aufwand) möglich, daher werde ich alles ohne HTTPS aufsetzen.

### virtuellen Server aufsetzen
1. Oracle VirtualBox installieren
2. Neue virtuelle Maschine erzeugen
* 8 GB RAM
* 4 CPUs
* ISO: [Ubuntu 24.04.1 LTS](https://ubuntu.com/download/server)
* Unbeaufsichtigte Installation:
* Benutzername und Passwort
* Hostname: kaulbach
* Domain Name: local
3. In den Netzwerkadapter-Einstellungen der virtuellen Maschine auf "Bridged Adapter" (Brückenadapter) umstellen
4. nach der Installation einloggen und mittels ```ip a``` die IP-Adresse der VM identifizieren (192.168.178.47)
5. Auf dem Host System in der Datei ```C:\Windows\System32\drivers\etc\hosts``` hinzufügen:
192.168.178.47 kaulbach.local
192.168.178.47 traefik.kaulbach.local
192.168.178.47 portainer.kaulbach.local
6. Test auf dem Host System:
* ping 192.168.178.47
* ping kaulbach.local

### virtuellen Server einrichten
```bash
sudo apt update
sudo apt upgrade -y

# Spracheinstellungen ändern
sudo locale-gen de_DE.UTF-8
sudo update-locale LANG=de_DE.UTF-8

# Tastaturbelegung ändern
sudo dpkg-reconfigure keyboard-configuration

# Zeitzone festlegen
sudo timedatectl set-timezone Europe/Berlin

# SSH Server installieren
sudo apt install -y openssh-server
sudo systemctl start ssh
sudo systemctl enable ssh
```

## Installation von Docker, Traefik, Portainer

### 1. Docker installieren
1. **Paketliste aktualisieren**:
```bash
sudo apt update
sudo apt upgrade -y
```

2. **Abhängigkeiten installieren**:
```bash
sudo apt install -y ca-certificates curl gnupg
```

3. **Docker-Repository hinzufügen**:
```bash
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
```

4. **Docker installieren**:
```bash
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```

5. **Docker-Dienst aktivieren**:
```bash
sudo systemctl enable docker
sudo systemctl start docker
```

6. **Rechte für den aktuellen Benutzer konfigurieren**:
```bash
sudo usermod -aG docker $USER
newgrp docker
```

### 2. Docker Compose installieren
Docker Compose ist bei neueren Docker-Versionen bereits als Plugin enthalten. Es kann direkt über den Docker-CLI-Befehl `docker compose` genutzt werden.

**Installation testen**:
```bash
docker compose version
```

### 3.0 Traefik Netzwerk anlegen
Alle mit dem Traefik Dienst verbundenen Container sollen im selben Netzwerk liegen:
```bash
docker network create traefik-net
```

### 3. Traefik installieren
1. **Arbeitsverzeichnis erstellen**:
```bash
mkdir ~/traefik && cd ~/traefik
```

2. **`docker-compose.yml` für Traefik erstellen**:
```bash
vim docker-compose.yml
```
Inhalt:
```yaml
services:
traefik:
image: traefik:v3.2
container_name: traefik
restart: always
ports:
- "80:80"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./traefik.yml:/etc/traefik/traefik.yml:ro"
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.kaulbach.local`)"
- "traefik.http.routers.traefik.entrypoints=web"
- "traefik.http.services.traefik.loadbalancer.server.port=8080"

networks:
default:
name: traefik-net
external: true
```

3. **Traefik-Konfigurationsdatei erstellen**:
```bash
vim traefik.yml
```
Inhalt:
```yaml
entryPoints:
web:
address: ":80"

providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false

api:
dashboard: true
insecure: true
```

4. **Container starten**:
```bash
docker compose up -d
# Log Files checken
docker logs traefik
```

5. **Subdomain eintragen**
Auf dem Host System in der Datei ```C:\Windows\System32\drivers\etc\hosts``` den Eintrag für die Subdomain "traefik.kaulbach.local" hinzufügen:
192.168.178.47 traefik.kaulbach.local

6. **Dashboard aufrufen**:
Das Traefik-Dashboard ist unter `http://traefik.kaulbach.local` erreichbar.

### 4. Webserver installieren
1. **Arbeitsverzeichnis erstellen**:
```bash
mkdir ~/web && cd ~/web
```

2. **Beispielseite erstellen**:
```bash
mkdir ~/web/html
echo "<h1>Hello, World! (c) DerIngo</h1>" > ~/web/html/index.html
```

3. **`docker-compose.yml` für den Webserver erstellen**:
```bash
vim docker-compose.yml
```
Inhalt:
```yaml
services:
nginx:
image: nginx
container_name: nginx
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.nginx.rule=Host(`kaulbach.local`)"
- "traefik.http.routers.nginx.entrypoints=web"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./html:/usr/share/nginx/html:ro"

networks:
default:
name: traefik-net
external: true
```

4. **Traefik-Labels nutzen**:
- Die Labels im obigen Beispiel sorgen dafür, dass Traefik den Webserver unter `http://kaulbach.local` bereitstellt.

5. **Container starten**:
```bash
docker compose up -d
# Log Files checken
docker logs web
```

6. **Webseite aufrufen**:
Die Webseite ist unter `http://kaulbach.local` erreichbar.

### 5. Portainer installieren
1. **Arbeitsverzeichnis erstellen**:
```bash
mkdir ~/portainer && cd ~/portainer
```

2. **`docker-compose.yml` für Portainer erstellen**:
```bash
vim docker-compose.yml
```
Inhalt:
```yaml
services:
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
restart: always
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./data:/data"
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.rule=Host(`portainer.kaulbach.local`)"
- "traefik.http.routers.portainer.entrypoints=web"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"

networks:
default:
name: traefik-net
external: true
```

3. **Traefik-Labels nutzen**:
- Die Labels im obigen Beispiel sorgen dafür, dass Traefik das Portainer Dashboard unter `http://portainer.kaulbach.local` bereitstellt.

4. **Container starten**:
```bash
docker compose up -d
# Log Files checken
docker logs portainer
```

5. **Subdomain eintragen**
Auf dem Host System in der Datei ```C:\Windows\System32\drivers\etc\hosts``` den Eintrag für die Subdomain "portainer.kaulbach.local" hinzufügen:
192.168.178.47 portainer.kaulbach.local

6. **Dashboard aufrufen**:
Das Traefik-Dashboard ist unter `http://portainer.kaulbach.local` erreichbar.

7. **Portainer konfigurieren**
* Passwort für den User "admin" festlegen: "adminpassword".
* Environment "local" ist bereits angelegt

### 6. ein weiterer Webserver
1. **Webserver anlegen und starten**:
```bash
mkdir -p ~/web2/html
echo "<h1>Hello vom geheimnisvollen 2. Server</h1>" > ~/web2/html/index.html
touch ~/web2/docker-compose.yml # mit Inhalt s.u. befüllen
cd ~/web2
docker compose up -d
```

2. **`docker-compose.yml` für den Webserver erstellen**:
```yaml
services:
nginx:
image: nginx
container_name: nginx2
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.nginx2.rule=PathPrefix(`/derzweite`)"
- "traefik.http.routers.nginx2.entrypoints=web"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./html:/usr/share/nginx/html:ro"

networks:
default:
name: traefik-net
external: true
```

3. **`docker-compose.yml` des ersten Webservers anpassen**:
Der Router des ersten Webservers mit dem Host(`kaulbach.local`) wird auch auf /derzweite matchen.
Daher müssen wir die Regel für den Router des ersten Servers anpassen:
```- "traefik.http.routers.nginx.rule=PathPrefix(`/`) && !PathPrefix(`/derzweite`)"```

4. **Webseite aufrufen**:
Die Webseite ist unter `http://kaulbach.local/derzweite` erreichbar.

## Abschluß
Das Fundament ist gelegt, darauf aufbauend kann ich die Migration der einzelnen Anwendungen erarbeiten.
Der Code-Editor, bzw. die Anzeige des Codes, des "Markup Markdown"-Editors ist ziehmlich kaputt. Und das was man sieht, sieht hässlich aus. Ich habe auch schon ein Ticket erstellt, ob und wie man andere Code-Editoren wie CodeMirror Blocks einbinden kann.
Die Dateien habe ich in [GitHub](https://github.com/DerIngo/localvirtualserver) eingecheckt.

Categories
Development Linux

Bughunter

Am Wochenende habe ich zum ersten Mal einen Escape Room spielen dürfen und ich muss sagen, heute habe ich mich ähnlich gefühlt, denn das Bugfixing ähnelt auch einer kreativen rätselratenden Schnitzeljagd.

Der Bug

Die von mir entwickelte Anwendung ist mittlerweile im Pilot-Betrieb und ein Benutzer meldet, dass er einen Vorgang nicht beenden kann: Vermeindlich hochgeladene Bilder, die zwingend erforderlich sind, 'verschwinden' und so kann er nicht abschließend speichern.

Die Bilder werden dankenswerterweise mitgeliefert, so dass ich auch versuchen kann, das Problem nachzustellen.

Es gibt auch einen Hinweis, das das Ausführen einer bestimmten Funktion während des Vorgangs das Problem verursacht haben könnte. ERROR-Einträge in den Logfiles unterstützen diese These, aber letztendlich hatte es damit nichts zu tun. Es war nur eine falsche Fährte, eine Ablenkung, wie bei jedem guten Spiel 😉
Und die Erkenntnis, dass da noch irgendwo ein weiterer Bug schlummert, der von mir gejagt und entdeckt werden will.

Bug reproduzieren

Als erstes versuche ich, den Bug lokal nachstellen zu können, also auf meinem Entwickler-Laptop. Das funktioniert aber nicht, da alles fehlerfrei funktioniert, incl. Upload der beiden Bilder.

Hier wäre jetzt eine gute Gelegenheit, das Ticket mit "Works on my machine" zu schließen.

Works on my machine - Imgflip

Aber ich forsche weiter und versuche, das Problem auf dem Server nachzustellen. Und hier gelingt es: Die Bilder verschwinden auf magische Art und Weise. Und ohne Einträge in den Logs.

Analyse

Der Vorgang durchläuft einen Wizard, und nachdem man die Bilder hochgeladen hat, kann man einen Schritt weiter. Wenn man dann einen Schritt zurück geht, sind die Bilder weg.

Mit meiner Testdatei (WOW!!.png) funktioniert es hingegen.

Auffällig ist, dass die andere Datei nicht korrekt in die Tabelle der Dateien übertragen wird, sondern ewig in der Upload-Ansicht verharrt.
Es gibt aber keine Fehlermeldung. Die Datei ist 6,4 MB groß, erlaubt sind Dateigrößen bis 10 MB; bei größeren Dateien kommt auch ein entsprechender Hinweis.
Hier scheint der Upload nie enden zu wollen, was auch ein Hinweis ist, warum sie später verschwindet: sie war nie auf dem Server.

Mein erster Verdacht war, dass vielleicht ein Virenscanner dazwischenfunkt und die Datei auf dem Server direkt wieder löscht. Alles schon mal vorgekommen.
Also lade ich die beiden Bilder per FTP auf den Server, was auch funktioniert und somit gegen die Virenscannerhypothese spricht.

Also zurück in den Browser und mal schauen, was sich so in der Entwicklerkonsole tut. Und dort ist auch ein Eintrag:

Der Upload wurde abgebrochen (413 - Request Entity Too Large). Das hat das JQuery- bzw. PrimeFaces-Framework nicht richtig erkannt und keine Fehlermeldung auf der Oberfläche angezeigt. Ein Bug im Framework, dass dieser spezielle Fall nicht richtig erkannt wird. Das interessiert mich aber nicht so sehr, viel spannender ist der Fehler 413. Erlaubt sind 10 MB, die Datei sind keine 7 MB. Es müsste also eigentlich passen.

In der Anwendung sind 10 MB erlaubt, das sollte nicht das Problem sein, bliebe noch der Anwendungsserver, ein Apache Tomcat, oder der davorgeschaltete ReverseProxy, ein Apache HTTP-Server.

Wenn man dem Netzwerkverkehr folgt sieht man, dass folgende Antwort beim Upload-Versuch zurück kommt:

Das Indiz für die Ursache des Problems ist das unscheinbare Wort: "nginx"

Der Verdacht erhärtet sich beim weiteren Testen:
Wenn ich die WOW-Testdatei in der Anwendung hochlade, erscheint das POST in den Logs des vorgelagerten Apache-ReverseProxies:

Der Versuch des Hochladens des Problembildes erzeugt keinen Log-Eintrag.

Es scheint also so zu sein, dass der Upload bereits vor meiner Anwendung abgebrochen wird.

Der Server

Schauen wir mal, wie es auf dem Test Server aussieht:

Meine Anwendung starte ich über ein Docker-Compose, bestehend aus:

  • Apache HTTP als Reverse Proxy
  • Apache Tomcat
  • PG Admin
  • PostgreSQL Datenbank

Auf dem Server selbst läuft aber nicht nur meine Anwendung, sondern auch noch weitere, die uA über unterschiedliche Domainnamen erreichbar sind. Und das steuert ein Nginx-Server:

Und was finde ich, wenn ich das default File Limit für Nginx suche:

By default, Nginx has a limit of 1MB on file uploads.

Die Lösung

Wir müssen also den file upload für die Anwendung erhöhten auf 10MB, besser 15MB:

vim /etc/nginx/sites-available/myapp
server {
    server_name myapp.test.company.de;

     location / {
	[...]
        client_max_body_size        15M;
     }
     [...]
}

Nach der Änderung die Konfiguration prüfen, um sicher zu gehen, dass keine Fehler eingebaut wurden und beim Neustart (bzw. Reload) der ganze Nginx-Server lahm gelegt wird:

nginx -t

Erst dann den Neustart, bzw. Reload durchführen:

service nginx reload

Test

Abschließend muss natürlich noch der Test erfolgen, ob wir damit erfolgreich waren:
Der Upload funktioniert jetzt! 🥳