Categories
Linux

Docker Setup with Traefik, Apache, and Portainer

Requirements

Software

  • Docker and Docker Compose
  • Apache HTTPD 2.4
  • Traefik v3.2
  • Portainer CE

Domain Configuration

Base domain: kabango.eu Required subdomains:

  • www.kabango.eu (main website)
  • kabango.eu (redirects to www)
  • traefik.kabango.eu (Traefik dashboard)
  • portainer.kabango.eu (Portainer interface)

Features

  • Automatic HTTPS with Let's Encrypt
  • HTTP to HTTPS redirect
  • Secure management interfaces
  • Path-based routing for special section
  • Shared Docker network
  • Container management via web interface

Directory Structure

/data/docker/
├── traefik/
│   ├── docker-compose.yml
│   ├── traefik.yml
│   └── config/
│       └── users.txt
├── apache1/
│   ├── docker-compose.yml
│   └── html/
│       └── index.html
├── apache2/
│   ├── docker-compose.yml
│   └── html/
│       └── index.html
└── portainer/
    ├── docker-compose.yml
        └── data/

Configuration Files

Traefik Static Configuration

# /data/docker/traefik/traefik.yml
api:
  dashboard: true

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

providers:
  docker:
    exposedByDefault: false
    network: traefik-net

certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@kabango.eu
      storage: /etc/traefik/acme/acme.json
      httpChallenge:
        entryPoint: web

log:
  level: INFO

Traefik Docker Compose

# /data/docker/traefik/docker-compose.yml
services:
  traefik:
    image: traefik:v3.2
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - traefik-net
    ports:
      - 80:80
      - 443:443
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/etc/traefik/traefik.yml:ro
      - ./config:/etc/traefik/config
      - acme:/etc/traefik/acme
    labels:
      - traefik.enable=true
      - traefik.http.routers.dashboard.rule=Host(`traefik.kabango.eu`)
      - traefik.http.routers.dashboard.service=api@internal
      - traefik.http.routers.dashboard.middlewares=auth
      - traefik.http.routers.dashboard.entrypoints=websecure
      - traefik.http.routers.dashboard.tls.certresolver=letsencrypt
      - traefik.http.middlewares.auth.basicauth.usersfile=/etc/traefik/config/users.txt

volumes:
  acme:

networks:
  traefik-net:
    external: true

Apache1 Docker Compose (Main Website)

# /data/docker/apache1/docker-compose.yml
services:
  apache1:
    image: httpd:2.4
    container_name: apache1
    restart: unless-stopped
    networks:
      - traefik-net
    volumes:
      - ./html:/usr/local/apache2/htdocs
    labels:
      - traefik.enable=true
      - traefik.http.routers.apache1.rule=Host(`kabango.eu`) || Host(`www.kabango.eu`)
      - traefik.http.routers.apache1.entrypoints=websecure
      - traefik.http.routers.apache1.tls.certresolver=letsencrypt
      - traefik.http.services.apache1.loadbalancer.server.port=80
      - traefik.http.middlewares.www-redirect.redirectregex.regex=^https://kabango.eu/(.*)
      - traefik.http.middlewares.www-redirect.redirectregex.replacement=https://www.kabango.eu/$${1}
      - traefik.http.routers.apache1.middlewares=www-redirect

networks:
  traefik-net:
    external: true

Apache2 Docker Compose (Special Section)

# /data/docker/apache2/docker-compose.yml
services:
  apache2:
    image: httpd:2.4
    container_name: apache2
    restart: unless-stopped
    networks:
      - traefik-net
    volumes:
      - ./html:/usr/local/apache2/htdocs
    labels:
      - traefik.enable=true
      - traefik.http.routers.apache2.rule=Host(`kabango.eu`) && PathPrefix(`/special`) || Host(`www.kabango.eu`) && PathPrefix(`/special`)
      - traefik.http.routers.apache2.entrypoints=websecure
      - traefik.http.routers.apache2.tls.certresolver=letsencrypt
      - traefik.http.services.apache2.loadbalancer.server.port=80
      - traefik.http.middlewares.strip-special.stripprefix.prefixes=/special
      - traefik.http.routers.apache2.middlewares=strip-special

networks:
  traefik-net:
    external: true

Portainer Docker Compose

# /data/docker/portainer/docker-compose.yml
services:
  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - traefik-net
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data:/data
    labels:
      - traefik.enable=true
      - traefik.http.routers.portainer.rule=Host(`portainer.kabango.eu`)
      - traefik.http.routers.portainer.entrypoints=websecure
      - traefik.http.routers.portainer.tls.certresolver=letsencrypt
      - traefik.http.services.portainer.loadbalancer.server.port=9000

networks:
  traefik-net:
    external: true

Sample HTML Files

Main Website (apache1):

<!-- /data/docker/apache1/html/index.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Welcome to Kabango.eu</title>
</head>
<body>
    <h1>Welcome to Kabango.eu</h1>
    <p>This is the main website.</p>
    <p>Visit our <a href="/special">special section</a>.</p>
</body>
</html>

Special Section (apache2):

<!-- /data/docker/apache2/html/index.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Special Section - Kabango.eu</title>
</head>
<body>
    <h1>Special Section</h1>
    <p>This is the special section of Kabango.eu</p>
    <p><a href="/">Back to main page</a></p>
</body>
</html>

Installation Steps

  1. Create Docker network:

    docker network create traefik-net
  2. Create required directories:

    mkdir -p /data/docker/{traefik/config,apache1/html,apache2/html,portainer,portainer/data}
  3. Create Traefik basic auth credentials:

    htpasswd -nb admin secure_password > /data/docker/traefik/config/users.txt
  4. Create configuration files:

    • Copy all configuration files to their respective locations as shown above
    • Ensure correct file permissions
  5. Configure DNS: Point these domains to your server's IP:

    • kabango.eu
    • www.kabango.eu
    • traefik.kabango.eu
    • portainer.kabango.eu
  6. Start services in order:

    cd /data/docker/traefik && docker compose up -d
    cd /data/docker/apache1 && docker compose up -d
    cd /data/docker/apache2 && docker compose up -d
    cd /data/docker/portainer && docker compose up -d

Access Points

After setup, the following services will be available:

Security Notes

  1. Docker Socket:

    • The Docker socket (/var/run/docker.sock) is only mounted in containers that require it:
      • Traefik: For container discovery
      • Portainer: For Docker management
    • Other containers don't need and shouldn't have access to the Docker socket
  2. Authentication:

    • Traefik dashboard is protected with basic authentication
    • Portainer requires setting up an admin account on first access
    • All management interfaces are only accessible via HTTPS
  3. Network Security:

    • Services communicate through an isolated Docker network
    • Only necessary ports (80, 443) are exposed on the host
    • Automatic redirection from HTTP to HTTPS

Maintenance

Updating Services

To update any service to the latest version:

cd /data/docker/<service>
docker compose pull
docker compose up -d

Viewing Logs

To view logs for any service:

cd /data/docker/<service>
docker compose logs

Add -f flag to follow the logs:

docker compose logs -f

Backup

Important directories to backup:

  • /data/docker/traefik/config - Traefik configuration
  • /data/docker/apache1/html - Main website content
  • /data/docker/apache2/html - Special section content
  • Portainer data volume - Container configurations

Troubleshooting

  1. Certificate Issues:

    • Check Traefik logs for Let's Encrypt errors
    • Verify DNS records are correct
    • Ensure ports 80 and 443 are accessible
  2. Routing Problems:

    • Verify Traefik router rules in docker-compose labels
    • Check if containers are in the correct network
    • Inspect Traefik dashboard for routing status
  3. Container Access:

    • Use docker compose ps to check container status
    • Verify network connectivity with docker network inspect traefik-net
    • Check container logs for errors
Categories
Development Linux

Apache HTTP in den Container

Aufgabe

Ich möchte die nativen Dienste auf meinem Server zur besseren Verwaltung und als Vorbereitung für eine kommende Migration auf Docker umstellen.
Als Vorbereitung für diese Aufgabe habe ich in Lokaler virtueller Server bereits ein grundlegendes Setup lokal evaluiert.
Heute möchte ich den Apache HTTP Server, der auch als Reverse Proxy dient, in einen Container stecken.

Vorbereitung

Docker deinstallieren

Auf dem Server ist bereits eine alte Docker Installation vorhanden. Diese habe ich als erstes rückstandslos entfernt.

Docker installieren

Hier nur kurz die Befehle, aus Lokaler virtueller Server übernommen:

sudo apt update
sudo apt upgrade -y

# Docker-Repository hinzufügen
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

# Docker installieren:
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

sudo systemctl enable docker
sudo systemctl start docker

# Rechte für den aktuellen Benutzer konfigurieren
sudo usermod -aG docker $USER
newgrp docker

Ordnerstrukur

Die Dateien der Docker Dienste sollen in /data/docker/ liegen.
Ein symbolischer Link von /home/docker soll auf das Verzeichnis zeigen.

sudo mkdir -p /data/docker
sudo ln -s /data/docker /home/docker

sudo chown :docker /data/docker
sudo chmod g+w /data/docker

Apache HTTP Container

Ordnerstruktur

mkdir /data/docker/apache
mkdir /data/docker/apache/config \
    /data/docker/apache/html \
    /data/docker/apache/logs

Daten kopieren

sudo cp -r /etc/apache2/* /data/docker/apache/config
sudo cp -r /var/www/html/* /data/docker/apache/html
sudo cp -r /var/log/apache2 /data/docker/apache

sudo chown -R :docker /data/docker/apache
sudo chmod -R g+w /data/docker/apache

mv /data/docker/apache/apache2/* /data/docker/apache/logs
rm -rf /data/docker/apache/apache2

Docker Compose Datei

docker-compose.yml für Apache HTTP im Verzeichnis /data/docker/apache:

services:
  apache:
    image: httpd:2.4
    container_name: apache
    restart: always
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./config:/usr/local/apache2/conf
      - ./html:/usr/local/apache2/htdocs
      - ./logs:/usr/local/apache2/logs

erster Start

Auf dem Server Apache HTTP stoppen:

# Service finden
systemctl list-unit-files --type service
# Service stoppen
sudo systemctl stop apache2

Container-Apache starten:

cd /data/docker/apache
docker compose up

Ausgabe:

[+] Building 0.0s (0/0)
Attaching to apache
apache  | httpd: Could not open configuration file /usr/local/apache2/conf/httpd.conf: No such file or directory
apache exited with code 0

Das hat also schon mal so gar nicht geklappt. Woran kann es liegen? Zur Analyse interaktiv in dem Container agieren:

docker compose run -it --entrypoint /bin/bash apache

Ich kann im Container sehen, dass die Konfigurations-Dateien vorhanden sind, d.h. die Docker-Compose-Konfig ist an der Stelle korrekt.
Allerdings fehlt die geforderte httpd.conf.
Bei Ubuntu heißt die Datei apache2.conf, der Docker Container erwartet aber eine httpd.conf. Als Workaround lege ich eine httpd.confan, die auf die apache2.conf verweist:

Include /usr/local/apache2/conf/apache2.conf


Jetzt bekomme ich beim Starten des Containers andere Fehlermeldungen.

Aufräumen

Das entwickelt sich nicht wie gewünscht, ich breche ab und räume auf:

docker compose down -v
sudo rm -rf /data/docker/apache

Kleiner Apache

Um einen minimalen Teilerfolg feiern zu können, setzte ich einen Apache im Container auf, der die HTML-Seiten auf Port 9080 ausliefert.

mkdir /data/docker/apache
mkdir /data/docker/apache/logs

cd /data/docker/apache
vim docker-compose.yml

docker compose up -d
docker logs apache
services:
  apache:
    image: httpd:2.4
    container_name: apache
    restart: always
    ports:
      - 9080:80
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /var/www/html:/usr/local/apache2/htdocs
      - ./logs:/usr/local/apache2/logs

Fazit

Der naive "Lift and Shift" Ansatz hat mal wieder nicht funktioniert.
Die Pfade sind bei der nativen Ubuntu Installation und dem Container Apache unterschiedlich. Der simple Workaround mit der httpd.conf-Datei war ein erster Schritt, danach hätten noch Umgebungsvariablen wie APACHE_RUN_DIR gesetzt werden müssen.
Dann hätte ich noch einige Pfade vom Server in den Container mappen müssen.
Und dann ist da noch der Let's encrypt Certbot, der doch mehr mit der Apache Installation verdrahtet ist, als ich am Anfang dachte. Den hätte ich auch noch im Container installieren müssen.
Sicherlich alles machbar, aber für eine Interimslösung zu aufwändig. Am Ende soll ja Traefik SSL und Reverse Proxy übernehmen. Daher belasse ich es hier erstmal.

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 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 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
    • 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

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:

    sudo apt update
    sudo apt upgrade -y
  2. Abhängigkeiten installieren:

    sudo apt install -y ca-certificates curl gnupg
  3. Docker-Repository hinzufügen:

    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:

    sudo apt update
    sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  5. Docker-Dienst aktivieren:

    sudo systemctl enable docker
    sudo systemctl start docker
  6. Rechte für den aktuellen Benutzer konfigurieren:

    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:

docker compose version

3.0 Traefik Netzwerk anlegen

Alle mit dem Traefik Dienst verbundenen Container sollen im selben Netzwerk liegen:

docker network create traefik-net

3. Traefik installieren

  1. Arbeitsverzeichnis erstellen:

    mkdir ~/traefik && cd ~/traefik
  2. docker-compose.yml für Traefik erstellen:

    vim docker-compose.yml

    Inhalt:

    
    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:

entryPoints:
  web:
    address: ":80"

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

api:
  dashboard: true
  insecure: true
  1. Container starten:

    docker compose up -d
    # Log Files checken
    docker logs traefik
  2. 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

  3. Dashboard aufrufen: Das Traefik-Dashboard ist unter http://traefik.kaulbach.local erreichbar.

4. Webserver installieren

  1. Arbeitsverzeichnis erstellen:

    mkdir ~/web && cd ~/web
  2. Beispielseite erstellen:

    mkdir ~/web/html
    echo "&lt;h1>Hello, World! (c) DerIngo&lt;/h1>" > ~/web/html/index.html
  3. docker-compose.yml für den Webserver erstellen:

    vim docker-compose.yml

    Inhalt:

    
    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
  1. Webseite aufrufen: Die Webseite ist unter http://kaulbach.local erreichbar.

5. Portainer installieren

  1. Arbeitsverzeichnis erstellen:

    mkdir ~/portainer && cd ~/portainer
  2. docker-compose.yml für Portainer erstellen:

    vim docker-compose.yml

    Inhalt:

    
    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
  1. 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

  2. Dashboard aufrufen: Das Traefik-Dashboard ist unter http://portainer.kaulbach.local erreichbar.

  3. Portainer konfigurieren

    • Passwort für den User "admin" festlegen: "adminpassword".
    • Environment "local" ist bereits angelegt

6. ein weiterer Webserver

  1. Webserver anlegen und starten:

    mkdir -p ~/web2/html
    echo "&lt;h1>Hello vom geheimnisvollen 2. Server&lt;/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:

    
    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.<br>
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.<br>
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.<br>
Die Dateien habe ich in [GitHub](https://github.com/DerIngo/localvirtualserver) eingecheckt.
Categories
Database Development Linux

Performance-Analyse

Performance-Analyse einer PostgreSQL-Datenbank mit pg_stat_statements

Bei einer Webanwendung mit einer PostgreSQL-Datenbank stießen wir auf Performance-Probleme. Hier beschreibe ich, wie man Performance-Probleme in PostgreSQL mittels der Erweiterung pg_stat_statements analysieren kann. Da die Erweiterung nicht auf der Produktivdatenbank installiert ist, zeige ich, wie man eine lokale Testumgebung aufsetzt.

Überblick der Vorgehensweise

  1. Backup der Produktivdatenbank erstellen
  2. Lokale Testdatenbank mit Docker aufsetzen
  3. pg_stat_statements installieren und konfigurieren
  4. Performance-Analyse durchführen

Vorbereitung

Zuerst prüfen wir die Version der Datenbank, um die passende Docker-Umgebung zu wählen:

SELECT version();
-- PostgreSQL 13.4


Backup erstellen

Das Backup erfolgt mittels Docker, um eine konsistente Umgebung zu gewährleisten. Hier das Script backup.sh:

targetfile=backup_`date +%Y-%m-%d`.sql

docker run --rm \
  --network=host \
  -e PGPASSWORD=PASSWORD \
  postgres:13.4-bullseye \
  pg_dump -h localhost -U myuser myschema > $targetfile

gzip $targetfile

Die Backup-Datei wird auf den lokalen Rechner übertragen.

Lokale Testdatenbank aufsetzen

Zuerst entfernen wir eine eventuell vorhandene alte Testinstanz und starten dann einen neuen Container:

docker stop mydb-performancetest && docker rm mydb-performancetest

docker run -d \
  --name mydb-performancetest \
  -e POSTGRES_USER=myuser \
  -e POSTGRES_PASSWORD=PASSWORD \
  -e POSTGRES_DB=myuser \
  -p 6432:5432 \
  postgres:13.4-bullseye

Verwendet wird Port 6432, um Konflikte mit einer möglicherweise laufenden lokalen PostgreSQL-Instanz zu vermeiden.

Daten importieren

Das Backup wird dann in die lokale Datenbank importiert, über das Script import.sh:

#!/bin/bash

# Check if arguments were passed
if [ $# -eq 0 ]; then
        echo "Error: No arguments provided"
            echo "Usage: $0 "
            exit 1
fi

# Check if file exists
if [ ! -f "$1" ]; then
      echo "Error: $1 is not a valid file."
        exit 1
fi
echo "File found: $1"

importfile=$(readlink -f "$1")
server=localhost
port=6432
username=myuser
password=PASSWORD
databasename=myuser

echo psql -h $server -U $username -d $databasename -f $importfile

docker run --rm \
  --network=host \
  -v $importfile:/script.sql \
  -e PGPASSWORD=$password \
  postgres:13.4-bullseye \
  psql -h $server -p $port -U $username -d $databasename -f script.sql

pg_stat_statements installieren

Um pg_stat_statements zu aktivieren, müssen wir die PostgreSQL-Konfiguration anpassen. Dazu bearbeiten wir die postgresql.conf. Da in dem Postgres Container kein Editor enthalten ist, ex- und importieren wir die Datei, um sie bearbeiten zu können:

# Export & bearbeiten:
docker cp mydb-performancetest:/var/lib/postgresql/data/postgresql.conf .
## shared_preload_libraries = 'pg_stat_statements' in einem Editor hinzufügen

# Re-Import & Neustart:
docker cp postgresql.conf mydb-performancetest:/var/lib/postgresql/data/postgresql.conf
docker restart mydb-performancetest

Danach aktivieren wir die Erweiterung in der Datenbank:

CREATE EXTENSION pg_stat_statements;

Performance-Analyse

Nach dem Ausführen der kritischen Anwendungsfälle können wir die problematischen Queries identifizieren:

SELECT
    query,
    calls,
    total_exec_time,
    min_exec_time,
    max_exec_time,
    mean_exec_time,
    rows,
    (total_exec_time / calls) AS avg_time_per_call
FROM
    pg_stat_statements
ORDER BY
    total_exec_time DESC
LIMIT 10;

Beispiel-Ergebnisse

Hier ein anonymisiertes Beispiel der Ausgabe:

query                              | calls | total_exec_time | min_exec_time | max_exec_time | mean_exec_time | rows  | avg_time_per_call
----------------------------------+-------+----------------+---------------+---------------+----------------+-------+-------------------
SELECT * FROM large_table WHERE... | 1500  | 350000.23      | 150.32       | 890.45        | 233.33         | 15000 | 233.33
UPDATE complex_table SET...       | 500   | 180000.45      | 250.12       | 750.89        | 360.00         | 500   | 360.00

Die Ergebnisse zeigen:

  • Anzahl der Aufrufe (calls)
  • Gesamtausführungszeit in ms (total_exec_time)
  • Minimale und maximale Ausführungszeit (min/max_exec_time)
  • Durchschnittliche Ausführungszeit (mean_exec_time)
  • Anzahl der betroffenen Zeilen (rows)

Besonders interessant sind Queries mit:

  • Hoher Gesamtausführungszeit
  • Hoher durchschnittlicher Ausführungszeit
  • Großer Differenz zwischen minimaler und maximaler Ausführungszeit
  • Unerwartet hoher Anzahl von Aufrufen

Reset

SELECT pg_stat_statements_reset();

Fazit

Mit dieser Methode können wir Performance-Probleme systematisch analysieren, ohne die Produktivumgebung zu beeinflussen.

Die Ergebnisse aus pg_stat_statements geben uns wichtige Hinweise, welche Queries optimiert werden sollten.

Categories
Development Java Linux

GitHub Codespace

I was on JCON 2024 and beside other interesting talks I heard one talk about cloud-based IDEs, and I wanted to try out, if GitHub Codespaces could work for me.

Explore the evolving landscape of cloud-based integrated development environments (IDEs), focusing on Gitpod, GitHub codespaces and Devpod. Compare and contrast these cloud IDEs with traditional counterparts, emphasizing the role of container technology, specifically the devcontainer specification. The discussion includes advances, existing limitations, and the potential for developing polyglot, container-based distributed applications. A live demo illustrates the rapid setup and coding process across various languages and frameworks, showcasing testing capabilities and seamless deployment to Kubernetes. Discover how custom additions enhance flexibility. Additionally, uncover the impact of cloud IDEs on teaching and team projects, ensuring consistent development setups for enhanced efficiency and streamlined processes.

[EN] Codespaces, Gitpod, Devpod ... what cloud and container-based IDEs can do for you
by Matthias Haeussler (Novatec Consulting GmbH)

Create GitHub Account

Go to GitHub and create an account. Free plan is suitable.

Create Repository

Create a new repository with name “workshop”. Add a README file.

Create Codespace

TODO: funktioniert das GIF?

Change Keyboard Layout to German: In the lower right corner click on “Layout: US” and enter “German” in the upcoming window.

TODO: Ich hätte gerne die Sprache von Visual Code auf Englisch umgestellt. Wie?

Work in the Terminal

Copy & Paste

Type something into the terminal.
Mark it with your mouse.
One Right Click to copy into Clipboard.
Another Right Click to paste from Clipboard.

Timezone

Set Timzone to Europe -> Berlin

sudo dpkg-reconfigure tzdata

Internet

Do we have access to the Internet? Let’s try with curl:

curl google.com

HTTPie

A modern alternative to curl is HTTPie:

Install httpie:

sudo apt update && \
sudo apt upgrade -y && \
sudo apt install httpie -y

This will take a few minutes. Meanwhile we can work in another Terminal window. Later we come back and test HTTPie:

http google.com

Additional Terminal window

Open a second Terminal with bash:

VIM

ls -lisah
touch test.sh
ls -lisah
vim test.sh
chmod +x test.sh
./test.sh
name=Ingo
echo "My name is $name"
echo "But here I am: $(whoami)"

Python

Do we have Python in our Codespace? Which version(s)?

python3 --version
python --version
vim hello_world.py
python hello_world.py
# Print "Hello World" to the console 
print("Hello World") 

Docker

docker --version
docker-compose --version
docker run hello-world 

Apache HTTPD

docker run -p 8888:80 httpd

Open in Browser:

Find all open Ports in the Ports-Tab:

Normally Port 8888 should be listed here.
We need to add Port, just enter 8888:

Open Website just with a click on the Globus-Icon.

When we try to open the address in another browser, we will see a GitHub-Login.
When we login with another GitHub-Account, we will get a 404-error. Because the page is Private.
Switch to Public:

Now we can access the page in another brower.

At the end we can shutdown HTTPD with + in Terminal window. It should automatically disapear in the Ports-Tab. If not, you can remove it manually.

Microsoft Edge - Caching problem

Open the Public page in MS Edge.
Make the page Private again. Try to open in a new browser, won’t work.
Reload (the Public loaded) page in MS Edge: You can still see the site!
This is a cached version and we need to force MS Edge to reload from server.

Open Developer Tools (F12 or ++), then you can Right Click on the reload button to have additional options:

Java

java --version
vim HelloWorld.java
javac HelloWorld.java
java HelloWorld
rm -f HelloWorld*
class HelloWorld { 
  public static void main(String args[]) { 
      System.out.println("Hello World"); 
  } 
}

Run Java Source as Shell Scripts

type -a java
# java is /home/codespace/java/current/bin/java
# java is /usr/local/sdkman/candidates/java/current/bin/java

vim HelloWorld.sh
chmod +x HelloWorld.sh
./HelloWorld.sh
rm HelloWorld.sh
#!/home/codespace/java/current/bin/java --source 21 

class HelloWorld { 
  public static void main(String args[]) { 
      System.out.println("Hello World"); 
  } 
}

Maven

Start

We create a new pom.xml from scratch.
We need a template. We will take “The Basics”-one from the Apache Maven POM Reference page.

    
      4.0.0

      org.codehaus.mojo
      my-project
      1.0
    
mvn --version
vim pom.xml
mvn clean verify 

Sample Project

Open pom.xml in Explorer (GUI) and change:

  • org.codehaus.mojo to org.acme
  • my-project to workshop

No need to save: Changes are automatically saved

To doublecheck that everything is still ok run mvn clean verify  again.

mkdir -p src/main/java/org/acme
touch src/main/java/org/acme/HelloWorld.java 

Open HelloWorld.java with + in GUI-Editor.

Install Extension Pack for Java as suggested:

And also the next two suggestions:

package org.acme;

class HelloWorld { 
  public static void main(String args[]) { 
      System.out.println("Hello World"); 
  } 
}
mvn package
java -classpath ./target/workshop-1.0.jar org.acme.HelloWorld

Maven - different version

In our Codespace we have Maven 3.9.6 and Java 21.
Let’s test with a different version. We will use Docker.

Official Maven Image on DockerHub.

We want to re-use the local Maven Cache. Let’s find out where it is:

sudo apt install locate -y
sudo updatedb
locate .m2
# /home/codespace/.m2

Adjust the “How to use this image” command:

docker run -it --rm \
--name workshop-maven-project \
-v /home/codespace/.m2:/root/.m2 \
-v "$(pwd)":/usr/src/workshop \
-w /usr/src/workshop \
maven:3.3-jdk-8 \
mvn clean package
java -classpath ./target/workshop-1.0.jar org.acme.HelloWorld

Sourcecode management

We have 7 uncommited changes, but only 2 files should go into the repository:

What we need is a .gitignore file.

touch .gitignore

There are two template files we will copy:

Now there are only 3 files we can commit:

Now we can see these files in our repository:

Secrets

Use GitHub Secrets for API-keys etc.

In the upper-right corner of any page, click your profile photo, then click Settings. Under Codespaces we can set our secrets:

In our Codespace we can access the secret as environment variable:

A running codespace has to be restarted!

Cleanup

Delete all files:

rm -rf target && \
rm -rf src && \
rm pom.xml && \
rm README.md && \
rm .gitignore

Stage & commit changes:

Now we have a clean repository:

Close browser window with codespace and delete the codespace:

Delete the repository:

Go to Settings → General → Danger Zone → Delete this repository

Categories
Linux

SSH Key-Based Authentication on Linux Servers

I want to login from one Linux server to another Linux server without the need to enter a password.

Create SSH keys

# Login to server 1
ssh-keygen

Output:

Generating public/private rsa key pair.
Enter file in which to save the key (/home/'USERNAME'/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/'USERNAME'/.ssh/id_rsa
Your public key has been saved in /home/'USERNAME'/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:27U2nApZQLSwS1UVh2Lw4aDM/B9/gR0Uum1ppRrqjpg 'USERNAME'@server1
The key\'s randomart image is:
+---[RSA 3072]----+
|    +o+ .o       |
| + . Bo+o .      |
|  = o.*...       |
|   o + o .       |
|    + o S   .    |
|     . X * +     |
| .    * @ O      |
|E . .. B * .     |
| ..+..o o        |
+----[SHA256]-----+

The utility will prompt to select a location for the keys that will be generated. By default, the keys will be stored in the ~/.ssh directory within user’s home directory. The private key will be called id_rsa and the associated public key will be called id_rsa.pub.

Now we have a private and public key that we can use to authenticate.

Copy keys to server 2

We will use the same keys on server 2:

scp ~/.ssh/id_rsa     USERNAME@server2ip:~/.ssh
scp ~/.ssh/id_rsa.pub USERNAME@server2ip:~/.ssh

Create authorized_keys on both servers:

ssh-copy-id USERNAME@localhost
ssh-copy-id USERNAME@server2ip

Connect to server 2

ssh USERNAME@server2ip
# or just
ssh server2ip

Now we SHOULD connect to server2 without the need to enter a password.

Failing - But why?

Unluckily in my case I still have to enter a password. But why?

# Login to server 2
ssh server2ip
Password:

# check auth.log
less /var/log/auth.log
...
rexec line 15: Deprecated option RSAAuthentication
...

# check sshd_config
less /etc/ssh/sshd_config
...
RSAAuthentication no
...

So in my case the RSA-Authentication has been disabled.
As the default is enabled by purpose, this has been done by purpose.
Unfortunatly I am not the admin or manager of the server, so I can not change this settings.

Categories
Development Linux

Docker Logs lesen

Problem

Der Server wird gestartet mit:

sudo docker-compose up --detach

Dadurch werden die Logfiles mit dem User root geschrieben und ich kann sie mit meinem User ingo nicht lesen.

Auf der Console kann ich das leicht mit einem vorangestellten sudo lösen, aber um mal eben schnell in die Logfiles rein zu schauen würde ich gerne mein graphisches Tool WinSCP verwenden

Lösung

Man kann Docker / Docker Compose mit einem User starten und mit dem würden dann vermutlich auch die Logfiles geschrieben werden.
Als ich das mit einem Tomcat Image getestet hatte, ist es daran gescheitert, dass mit meinem User ingo auf bestimmte Verzeichnisse im Container nicht zugegriffen werden konnte.

Gelöst habe ich es dann so, dass ich nicht den User, oder User und Gruppe, gesetzt habe, sondern nur die Gruppe.
So wird mit dem root User gearbeitet, die Dateien gehören dem User root und für die gesetzte Gruppe sind sie lesbar.
Mein User muss natürlich ebenfalls in der Gruppe sein.

Gruppe anlegen:

sudo groupadd -g 1001 logfilegroup

Die Group ID ist relativ willkürlich gesetzt. Eigentlich hatte ich groupadd ohne das -g Flag aufgerufen und dann mit cat /etc/group die Group ID rausgesucht. Hier wollte ich das Statement mit explizitem setzen der Group ID hinschreiben, da ich es auch im Projekt verwendet hatte, um auf jedem Server die selbe Group ID zu haben.

User der Gruppe hinzufügen:

sudo usermod --append --groups logfilegroup ingo

Mit den Befehlen groups oder id kann man die Gruppen seines Users sehen, die neue logfilegroup wird aber erst in einer neuen Shell hinzugefügt. Also entweder die Shell schließen und neu öffnen, oder mit su ingo weiter arbeiten.

ingo$ sudo usermod --append --groups logfilegroup ingo
ingo$ groups
ingo adm
ingo$ su ingo
Password:
ingo$ groups
ingo adm logfilegroup

Docker Compose File:

Im Docker Compose File muss die Group ID gesetzt werden, mit dem Namen der Gruppe geht es nicht.

version: "3.2"
services:
  melba-web:
    image: tomcat:10.1.18-jre21 # https://hub.docker.com/_/tomcat
    restart: always
    container_name: myapp-tomcat
    user: :1001
    ports: 
      - "8080:8080"
    environment:
      - _JAVA_OPTIONS=-Duser.language=de -Duser.country=DE
    volumes:
      - ./log/tomcat:/usr/local/tomcat/logs
sudo docker-compose up --detach

ls -lisah log/tomcat/*
4211764 4.0K drwxr-xr-x 2 ingo ingo  4.0K Feb  8 16:52 .
4211762 4.0K drwxr-xr-x 4 ingo ingo  4.0K Feb  5 16:42 ..
4205212  24K -rw-r----- 1 root logfilegroup  24K Feb  8 16:21 catalina.2024-02-08.log
4211758  28K -rw-r----- 1 root logfilegroup  24K Feb  8 16:58 localhost_access_log.2024-02-08.txt
Categories
Linux

Upgrade Ubuntu 20.04 to 22.04 on WSL

Check current version:

lsb_release -a

Make 20.04 installation is up-to-date:

sudo apt update && sudo apt full-upgrade

Close Ubuntu on WSL terminal, and open PowerShell terminal:

wsl -l -v 
## Confirm the distribution name and adjust below if needed
wsl --terminate Ubuntu

Open new Ubuntu on WSL terminal:

sudo do-release-upgrade
##
sudo apt update && sudo apt upgrade -y
sudo apt autoremove

Check current version:

lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.3 LTS
Release:        22.04
Codename:       jammy

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! 🥳

Categories
Development Linux

Kubernetes

Everyone talks about Kubernetes, therefore I thought, it might be a good idea to get some experiences with this tool and install it on my developer machine.

To run a single node cluster of Kubernetes on my machine I will install Minikube.

My developer machine is a Windows computer with Ubuntu 20.04 on WSL 2.

Here my notes while I go along this tutorial: How To Install Minikube on Ubuntu 22.04|20.04|18.04

Step 3: Download minikube on Ubuntu

I will put the minikube binary under /usr/local/bin directory since it is inside $PATH.

wget https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
chmod +x minikube-linux-amd64
sudo mv minikube-linux-amd64 /usr/local/bin/minikube

Confirm version installed

$ minikube version
minikube version: v1.30.1
commit: 08896fd1dc362c097c925146c4a0d0dac715ace0

Step 4: Install kubectl on Ubuntu

We need kubectl which is a command line tool used to deploy and manage applications on Kubernetes:

curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
chmod +x ./kubectl
sudo mv ./kubectl /usr/local/bin/kubectl

Check version:

$ kubectl version -o json  --client
{
  "clientVersion": {
    "major": "1",
    "minor": "27",
    "gitVersion": "v1.27.3",
    "gitCommit": "25b4e43193bcda6c7328a6d147b1fb73a33f1598",
    "gitTreeState": "clean",
    "buildDate": "2023-06-14T09:53:42Z",
    "goVersion": "go1.20.5",
    "compiler": "gc",
    "platform": "linux/amd64"
  },
  "kustomizeVersion": "v5.0.1"
}

Step 5: Starting minikube on Ubuntu

$ minikube start
😄  minikube v1.30.1 on Ubuntu 20.04
✨  Automatically selected the docker driver. Other choices: none, ssh
📌  Using Docker driver with root privileges
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
💾  Downloading Kubernetes v1.26.3 preload ...
    > preloaded-images-k8s-v18-v1...:  397.02 MiB / 397.02 MiB  100.00% 3.21 Mi
    > gcr.io/k8s-minikube/kicbase...:  373.53 MiB / 373.53 MiB  100.00% 2.76 Mi
🔥  Creating docker container (CPUs=2, Memory=6300MB) ...
🐳  Preparing Kubernetes v1.26.3 on Docker 23.0.2 ...
    ▪ Generating certificates and keys ...
    ▪ Booting up control plane ...
    ▪ Configuring RBAC rules ...
🔗  Configuring bridge CNI (Container Networking Interface) ...
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying Kubernetes components...
🌟  Enabled addons: default-storageclass, storage-provisioner
🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

Step 6: Minikube Basic operations

To check cluster status, run:

$ kubectl cluster-info
Kubernetes control plane is running at https://127.0.0.1:32769
CoreDNS is running at https://127.0.0.1:32769/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

Note that Minikube configuration file is located under ~/.minikube/machines/minikube/config.json

To View Config, use:

$ # cat ~/.minikube/machines/minikube/config.json
$ kubectl config view
apiVersion: v1
clusters:
- cluster:
    certificate-authority: /home/ingo/.minikube/ca.crt
    extensions:
    - extension:
        last-update: Thu, 29 Jun 2023 12:34:35 CEST
        provider: minikube.sigs.k8s.io
        version: v1.30.1
      name: cluster_info
    server: https://127.0.0.1:32769
  name: minikube
contexts:
- context:
    cluster: minikube
    extensions:
    - extension:
        last-update: Thu, 29 Jun 2023 12:34:35 CEST
        provider: minikube.sigs.k8s.io
        version: v1.30.1
      name: context_info
    namespace: default
    user: minikube
  name: minikube
current-context: minikube
kind: Config
preferences: {}
users:
- name: minikube
  user:
    client-certificate: /home/ingo/.minikube/profiles/minikube/client.crt
    client-key: /home/ingo/.minikube/profiles/minikube/client.key

To check running nodes:

$ kubectl get nodes
NAME       STATUS   ROLES           AGE   VERSION
minikube   Ready    control-plane   26m   v1.26.3

Access minikube VM using ssh:

$ minikube ssh
docker@minikube:~$
docker@minikube:~$ exit
logout

Let's doublecheck that minikube is a running Docker container:

$ sudo docker container ls
CONTAINER ID   IMAGE                                 COMMAND                  CREATED          STATUS          PORTS                                                                                                                                  NAMES
e36590b3ea7e   gcr.io/k8s-minikube/kicbase:v0.0.39   "/usr/local/bin/entr…"   28 minutes ago   Up 28 minutes   127.0.0.1:32772->22/tcp, 127.0.0.1:32771->2376/tcp, 127.0.0.1:32770->5000/tcp, 127.0.0.1:32769->8443/tcp, 127.0.0.1:32768->32443/tcp   minikube

To stop a running local kubernetes cluster, run:

minikube stop

To delete a local kubernetes cluster, use:

minikube delete

To start local kubernetes cluster:

minikube start

Step 7: Minikube Addons

Display installed and enabled Minikube addons:

$ minikube addons list
|-----------------------------|----------|--------------|--------------------------------|
|         ADDON NAME          | PROFILE  |    STATUS    |           MAINTAINER           |
|-----------------------------|----------|--------------|--------------------------------|
| ambassador                  | minikube | disabled     | 3rd party (Ambassador)         |
| auto-pause                  | minikube | disabled     | Google                         |
| cloud-spanner               | minikube | disabled     | Google                         |
| csi-hostpath-driver         | minikube | disabled     | Kubernetes                     |
| dashboard                   | minikube | enabled ✅   | Kubernetes                     |
| default-storageclass        | minikube | enabled ✅   | Kubernetes                     |
| efk                         | minikube | disabled     | 3rd party (Elastic)            |
| freshpod                    | minikube | disabled     | Google                         |
| gcp-auth                    | minikube | disabled     | Google                         |
| gvisor                      | minikube | disabled     | Google                         |
| headlamp                    | minikube | disabled     | 3rd party (kinvolk.io)         |
| helm-tiller                 | minikube | disabled     | 3rd party (Helm)               |
| inaccel                     | minikube | disabled     | 3rd party (InAccel             |
|                             |          |              | [info@inaccel.com])            |
| ingress                     | minikube | disabled     | Kubernetes                     |
| ingress-dns                 | minikube | disabled     | Google                         |
| istio                       | minikube | disabled     | 3rd party (Istio)              |
| istio-provisioner           | minikube | disabled     | 3rd party (Istio)              |
| kong                        | minikube | disabled     | 3rd party (Kong HQ)            |
| kubevirt                    | minikube | disabled     | 3rd party (KubeVirt)           |
| logviewer                   | minikube | disabled     | 3rd party (unknown)            |
| metallb                     | minikube | disabled     | 3rd party (MetalLB)            |
| metrics-server              | minikube | disabled     | Kubernetes                     |
| nvidia-driver-installer     | minikube | disabled     | Google                         |
| nvidia-gpu-device-plugin    | minikube | disabled     | 3rd party (Nvidia)             |
| olm                         | minikube | disabled     | 3rd party (Operator Framework) |
| pod-security-policy         | minikube | disabled     | 3rd party (unknown)            |
| portainer                   | minikube | enabled ✅   | 3rd party (Portainer.io)       |
| registry                    | minikube | disabled     | Google                         |
| registry-aliases            | minikube | disabled     | 3rd party (unknown)            |
| registry-creds              | minikube | disabled     | 3rd party (UPMC Enterprises)   |
| storage-provisioner         | minikube | enabled ✅   | Google                         |
| storage-provisioner-gluster | minikube | disabled     | 3rd party (Gluster)            |
| volumesnapshots             | minikube | disabled     | Kubernetes                     |
|-----------------------------|----------|--------------|--------------------------------|

Start dashboard:

ingo:~$ minikube dashboard --port=42827 &
[6] 55787
ingo:~$ 🤔  Verifying dashboard health ...
🚀  Launching proxy ...
🤔  Verifying proxy health ...
🎉  Opening http://127.0.0.1:42827/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ in your default browser...
👉  http://127.0.0.1:42827/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/

Open URL in Browser:

kubernetes dashboard

To enable a module use command:

minikube addons enable 

Example:

$ minikube addons enable portainer
❗  portainer is a 3rd party addon and is not maintained or verified by minikube maintainers, enable at your own risk.
❗  portainer does not currently have an associated maintainer.
    ▪ Using image docker.io/portainer/portainer-ce:2.15.1
🌟  The 'portainer' addon is enabled

But I have no clue, what to do with the enabled 'portainer' addon. 🤷‍♂️

For the next steps I used a tutorial from kubernetes:

Create a Deployment

Deployments are the recommended way to manage the creation and scaling of Pods.

  1. Use the kubectl create command to create a Deployment that manages a Pod. The Pod runs a Container based on the provided Docker image.
# Run a test container image that includes a webserver
kubectl create deployment hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.39 -- /agnhost netexec --http-port=8080

2. View the Deployment:

$ kubectl get deployments
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
hello-node   0/1     1            0           9s

3. View the Pod:

$ kubectl get pods
NAME                          READY   STATUS    RESTARTS   AGE
hello-node-7b87cd5f68-rj79x   1/1     Running   0          67s

4. View cluster events:

kubectl get events

5. View the kubectl configuration:

kubectl config view

Create a Service

By default, the Pod is only accessible by its internal IP address within the Kubernetes cluster. To make the hello-node Container accessible from outside the Kubernetes virtual network, you have to expose the Pod as a Kubernetes Service.

  1. Expose the Pod to the public internet using the kubectl expose command:
kubectl expose deployment hello-node --type=LoadBalancer --port=8080

The --type=LoadBalancer flag indicates that you want to expose your Service outside of the cluster.

The application code inside the test image only listens on TCP port 8080. If you used kubectl expose to expose a different port, clients could not connect to that other port.

2. View the Service you created:

$ kubectl get services
NAME         TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
hello-node   LoadBalancer   10.101.148.235        8080:31331/TCP   2m52s
kubernetes   ClusterIP      10.96.0.1                443/TCP          71m

On cloud providers that support load balancers, an external IP address would be provisioned to access the Service. On minikube, the LoadBalancer type makes the Service accessible through the minikube service command.

3. Run the following command:

$ minikube service hello-node
|-----------|------------|-------------|---------------------------|
| NAMESPACE |    NAME    | TARGET PORT |            URL            |
|-----------|------------|-------------|---------------------------|
| default   | hello-node |        8080 | http://192.168.49.2:31331 |
|-----------|------------|-------------|---------------------------|
🏃  Starting tunnel for service hello-node.
|-----------|------------|-------------|------------------------|
| NAMESPACE |    NAME    | TARGET PORT |          URL           |
|-----------|------------|-------------|------------------------|
| default   | hello-node |             | http://127.0.0.1:34597 |
|-----------|------------|-------------|------------------------|
🎉  Opening service default/hello-node in default browser...
👉  http://127.0.0.1:34597
❗  Because you are using a Docker driver on linux, the terminal needs to be open to run it.

Open http://127.0.0.1:34597/ in a browser:

hello-node

4. View Pods and Services created in 'default' namespace:

$ kubectl get pod,svc -n default
NAME                              READY   STATUS    RESTARTS   AGE
pod/hello-node-7b87cd5f68-rj79x   1/1     Running   0          12m

NAME                 TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
service/hello-node   LoadBalancer   10.101.148.235        8080:31331/TCP   8m54s
service/kubernetes   ClusterIP      10.96.0.1                443/TCP          77m

5. Cleanup

$ kubectl delete service hello-node
service "hello-node" deleted
$ kubectl delete deployment hello-node
deployment.apps "hello-node" deleted

Next Steps from Crash-Kurs Kubernetes.

Filebased Deployment

Create deployment file:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 8080

Create Deployment:

kubectl apply -f deployment.yaml
kubectl get deployment

Create & Start Service:

kubectl expose deployment nginx-deployment --type=LoadBalancer --port=80
kubectl list service

minikube service nginx-deployment &

Check in Browser:

Cleanup:

kubectl delete service nginx-deployment
kubectl delete deployment nginx-deployment

Update deployment from nginx to httpd:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: httpd
        image: httpd:latest
        ports:
        - containerPort: 8080

Create Deployment:

kubectl apply -f deployment.yaml
kubectl get deployment

Create & Start Service:

kubectl expose deployment nginx-deployment --type=LoadBalancer --port=80
kubectl list service

minikube service nginx-deployment &

Check in Browser:

Also check in Terminal:

$ http http://127.0.0.1:37563
HTTP/1.1 200 OK
Accept-Ranges: bytes
Connection: Keep-Alive
Content-Length: 45
Content-Type: text/html
Date: Thu, 29 Jun 2023 14:39:47 GMT
ETag: "2d-432a5e4a73a80"
Keep-Alive: timeout=5, max=100
Last-Modified: Mon, 11 Jun 2007 18:53:14 GMT
Server: Apache/2.4.57 (Unix)

It works!

Cleanup:

kubectl delete service nginx-deployment
kubectl delete deployment nginx-deployment

Filebased Service

Create deployment file:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
    version: "1.0"
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
  labels:
    app: nginx
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
  selector:
    app: nginx

Create Deployment & Service:

kubectl apply -f deployment.yaml
kubectl get deployment
kubectl get service

Start Service:

minikube service nginx-service &

Check in Terminal:

$ http http://127.0.0.1:45137
HTTP/1.1 200 OK
Accept-Ranges: bytes
Connection: keep-alive
Content-Length: 615
Content-Type: text/html
Date: Thu, 29 Jun 2023 14:57:23 GMT
ETag: "6488865a-267"
Last-Modified: Tue, 13 Jun 2023 15:08:10 GMT
Server: nginx/1.25.1




Welcome to nginx!



Welcome to nginx!

If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.

Cleanup:

$ kubectl delete -f deployment.yaml
deployment.apps "nginx-deployment" deleted
service "nginx-service" deleted

Website from nginx-Webserver

Build Docker Image

Simple Website:




Page Title


This is a Heading

This is a paragraph.

Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.

Dockerfile for an nginx webserver to deliver this website:

FROM nginx

COPY index.html /usr/share/nginx/html

EXPOSE 80

Build, run and test image:

docker build -t myweb-image .
docker run -it -p 80:80 --name myweb-container myweb-image

$ http http://localhost
HTTP/1.1 200 OK
Accept-Ranges: bytes
Connection: keep-alive
Content-Length: 763
Content-Type: text/html
Date: Thu, 29 Jun 2023 15:57:54 GMT
ETag: "649da8a5-2fb"
Last-Modified: Thu, 29 Jun 2023 15:52:05 GMT
Server: nginx/1.25.1




Page Title


This is a Heading

This is a paragraph.

Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.

Cleanup:

docker container rm myweb-container
docker image rm myweb-image

Use Docker Image in Minikube

Minikube runs on an own Docker Daemon, so we can not use 'standard' local Docker Image Repository, but the Minikube one.

Change current Shell environment to Minikube-Docker and build Docker Image:

eval $(minikube docker-env)

docker build -t myweb-image .
# set additional Tag
docker image tag myweb-image:latest myweb-image:1.0

Doublecheck in Minikube:

$ minikube ssh

docker@minikube:~$ docker images
REPOSITORY                                TAG       IMAGE ID       CREATED              SIZE
myweb-image                               latest    5193eeef7975   About a minute ago   187MB

Kubernetes Configuration

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myweb-deployment
  labels:
    app: nginx
    version: "1.0"
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myweb-app
  template:
    metadata:
      labels:
        app: myweb-app
    spec:
      containers:
      - name: myweb-container
        image: myweb-image:1.0
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: myweb-service
  labels:
    app: myweb-app
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
  selector:
    app: myweb-app

Create Deployment & Service:

kubectl apply -f myweb.yaml
kubectl get deployment
kubectl get service

Start Service:

minikube service myweb-service &

Check in Terminal:

$ http http://127.0.0.1:38915
HTTP/1.1 200 OK
Accept-Ranges: bytes
Connection: keep-alive
Content-Length: 763
Content-Type: text/html
Date: Thu, 29 Jun 2023 16:19:18 GMT
ETag: "649da8a5-2fb"
Last-Modified: Thu, 29 Jun 2023 15:52:05 GMT
Server: nginx/1.25.1




Page Title


This is a Heading

This is a paragraph.

Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.

Cleanup:

$ kubectl delete -f myweb.yaml

Work in a pod

Start some pods & service and display them:

kubectl apply -f myweb.yaml
kubectl get all -o wide

Output:

NAME                                    READY   STATUS    RESTARTS   AGE     IP            NODE       NOMINATED NODE   READINESS GATES
pod/myweb-deployment-565b64686c-2nnrl   1/1     Running   0          3m42s   10.244.0.39   minikube              
pod/myweb-deployment-565b64686c-m4p4c   1/1     Running   0          3m42s   10.244.0.41   minikube              
pod/myweb-deployment-565b64686c-sx6sx   1/1     Running   0          3m42s   10.244.0.40   minikube              

NAME                    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE     SELECTOR
service/kubernetes      ClusterIP   10.96.0.1               443/TCP        23h     
service/myweb-service   NodePort    10.97.251.106           80:32715/TCP   3m42s   app=myweb-app

NAME                               READY   UP-TO-DATE   AVAILABLE   AGE     CONTAINERS        IMAGES            SELECTOR
deployment.apps/myweb-deployment   3/3     3            3           3m42s   myweb-container   myweb-image:1.0   app=myweb-app

NAME                                          DESIRED   CURRENT   READY   AGE     CONTAINERS        IMAGES            SELECTOR
replicaset.apps/myweb-deployment-565b64686c   3         3         3       3m42s   myweb-container   myweb-image:1.0   app=myweb-app,pod-template-hash=565b64686c
i

Start a Shell in pod on IP: 10.244.0.41:

kubectl exec -it myweb-deployment-565b64686c-m4p4c -- /bin/bash
apt update
apt install httpie inetutils-ping -y

# Connect to another Pod via IP
http 10.244.0.39
# Connect to Service via IP
http 10.97.251.106
# Connect to Service via Service Name
http myweb-service

# Check IP of Service
ping myweb-service
## OUTPUT:
# PING myweb-service.default.svc.cluster.local (10.97.251.106): 56 data bytes

exit

Cleanup:

$ kubectl delete -f myweb.yaml

Environment Variable

Sample how to set an environment variable via deployment file:

Add env section to deployment file:

[...]
    spec:
      containers:
      - name: myweb-container
        image: myweb-image:1.0
        ports:
        - containerPort: 80
        env:
        - name: MY_ENV_1
          value: My Value No 1
        - name: MY_ENV_2
          value: My Value No 2
---
[...]

Start Pods, jump into Pod and check values:

kubectl apply -f myweb.yaml
kubectl get all -o wide

kubectl exec -it myweb-deployment-864984686b-5p7dn -- /bin/bash

## Inside Pod:
echo $MY_ENV_1
# Output: My Value No 1
echo $MY_ENV_2
# Output: My Value No 2
exit

# Cleanup:
kubectl delete -f myweb.yaml