NGINX im Container und docker-gen

source: https://gifsboom.net/post/106865490574/fail-container-video

Das hatte ich lange auf meiner Liste. Bei mir lief immer noch der NGINX nicht im Container. Um ehrlich zu sein hatte ich mir alles schwieriger vorgestellt als es dann am Ende war. Ich halte mir immer alles in docker-compose.yml-Files fest.

nginx:
  image: nginx
  ports:
    - "80:80"
    - "443:443"
  volumes:
    - /etc/nginx:/etc/nginx
    - /etc/letsencrypt:/etc/letsencrypt
    - /var/log/nginx:/var/log/nginx

Dies läßt eine funktionierende Ubuntu-NGINX-Config mit dem offiziellen NGINX-Dockerimage laufen. Der NGINX macht fungiert nur als Reverse-Proxy für ein paar Anwendungen. Also werden Requests an ein Upstream Server weitergeleitet. NGINX macht manchmal Probleme wenn der Upstream Server Hostname nicht auflösbar ist und bricht dann sofort ab. Und dann sitzt man da und wundert sich wieso der Container immer wieder aussteigt. IP-Adressen sind auch auf ihre Art problematisch. Sie können sich unter Docker öfters ändern wenn man nach einem Image-Update den Container neustartet. Also was tun? Ich setze nun docker-gen ein. Der überwacht den Docker-Daemon und erstellt aus einem Template Config-Files. In diesem Fall eine upstream.conf. Die docker-compose.yml sieht so aus:

gen:
  image: jwilder/docker-gen
  volumes:
    - /var/run/docker.sock:/tmp/docker.sock:ro
    - /opt/docker-gen:/data
  volumes_from:
    - nginx_nginx_1
  command: -notify-sighup nginx_nginx_1 -watch -only-exposed /data/nginx.tmpl /etc/nginx/upstream.conf

docker-gen braucht den docker.sock um aus dem Container mit dem Daemon zu kommunizieren. Schließlich muss er mitbekommen wenn es Veränderungen gibt. Sprich Container gestartet werden oder Container nicht mehr da sind. Ich mounte das Arbeitsverzeichnis um auf das Template aus dem Container zugreifen zu können. Dazu binde ich alle Volumes des NGINX Containers ein. Schließlich soll das finale Config-File an die richtige Stelle geschoben werden. -notify-sighup nginx_nginx_1 sagt das er den NGINX Container neustarten soll. Mit -watch beobachtet er den Docker Daemon auf Änderungen. Mit -only-exposed werden nur Container betrachtet die Ports exposen. Dahinter kommt dann das Template welches benutzt werden soll und wohin die Finale Datei geschrieben werden soll. Ich habe mich dafür entschieden nicht die ganze nginx.conf aus dem Template zu generieren. Ich lasse nur die upstream.conf bauen. Dieses inkludiere ich dann in der nginx.conf.

upstream cloud {
                        # ownclouddocker_owncloud_1
                        server 172.17.0.6:80;
}
upstream gogs {
                        # gogs_gogs_1
                        server 172.17.0.5:3000;
}
upstream ttrss {
                        # ttrssdocker_ttrss_1
                        server 172.17.0.8:80;
}

Damit so eine Config erzeugt wird benutze ich dieses Template:

{{ range $host, $containers := groupByMulti $ "Env.VIRTUAL_HOST" "," }}
upstream {{ $host }} {

{{ range $index, $value := $containers }}

    {{ $addrLen := len $value.Addresses }}
    {{ $network := index $value.Networks 0 }}

    {{/* If only 1 port exposed, use that */}}
    {{ if eq $addrLen 1 }}
        {{ with $address := index $value.Addresses 0 }}
            # {{$value.Name}}
            server {{ $network.IP }}:{{ $address.Port }};
        {{ end }}

    {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var */}}
    {{ else if $value.Env.VIRTUAL_PORT }}
        {{ range $i, $address := $value.Addresses }}
            {{ if eq $address.Port $value.Env.VIRTUAL_PORT }}
            # {{$value.Name}}
            server {{ $network.IP }}:{{ $address.Port }};
            {{ end }}
        {{ end }}

    {{/* Else default to standard web port 80 */}}
    {{ else }}
        {{ range $i, $address := $value.Addresses }}
            {{ if eq $address.Port "80" }}
            # {{$value.Name}}
            server {{ $network.IP }}:{{ $address.Port }};
            {{ end }}
        {{ end }}
    {{ end }}
{{ end }}
}

{{ end }}

Damit docker-gen die passenden Container findet die er betrachten soll, müssen den Containern ein paar Environment-Variabeln mitgegeben werden. Hier zum Beispiel mit Gogs:

gogs:
  image: gogs/gogs
  ports:
    - "3000"
  volumes:
    - "/srv/www/gogs:/data"
  environment:
    - VIRTUAL_HOST=gogs
    - VIRTUAL_PORT=3000

VIRTUAL_HOST gibt dem ganzen einen Namen. Den brauchen wir dann in der eigentlichen vhost-Config. VIRTUAL_PORT wird gebraucht wenn der Port nicht 80 ist. Dann ignoriert docker-gen sonst den Container.

Die NGINX-Config sieht dann so aus:

server {
        listen 443;
        server_name git.foo.bar;

        ssl on;
        ssl_certificate /etc/letsencrypt/live/git.foo.bar/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/git.foo.bar/privkey.pem;

        client_max_body_size 50m;

        location / {
                proxy_pass http://gogs;
                proxy_set_header X-Forwarded-Host $server_name;
                proxy_set_header X-Forwarded-Proto https;
                proxy_set_header X-Forwarded-For $remote_addr;
        }

}

Unter proxy_pass taucht VIRTUAL_HOST wieder auf. In der NGINX Config spiegelt sich das wie folgt wieder:

http {

        ##
        # Basic Settings
        ##

        ...
        ...
        ...

        ##
        # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;

        ##
        # Upstream
        ##

        include /etc/nginx/upstream.conf;

}

Nun alles Container starten und genießen oder sowas.

Das Letsencrypt Docker Moped

Darauf haben wir lange gewartet: SSL für alle!!1!!!111! Wie oft musste ich Leuten erzählen das sie keine Angst haben müssen vor dem Polizisten mit dem Schlagstock (zumindestens nicht vor dem im Chrome Browser). Ich hatte persöhnlich nie etwas gegen selbstsignierte Zertifikate, wenn ich nicht gerade Geld über diese Seiten schubsen musste. Aber könnte nun alles der Vergangenheit angehören. Letsencrypt bieten nun einen einfachen Weg an um sich seine Zertifikate unterschreiben zu lassen. Und nicht nur das. Sie wollen Tools mitliefern, die ohne viel Vorwissen die Arbeit für einen erledigen. Sprich Keys erzeugen und den Zertfikatsrequest. Dann findet eine überprüfung statt ob die Domain für die ihr das Zertifikat haben wollt, auch wirklich euch gehört. Ein wichtiger Schritt. Nur weil ihr keine Hunderte von Euro für ein Zertifikat ausgibt, sollt ihr trotzdem das Recht haben das der Transport über das Netz verschlüßelt ist. Ohne Schlagstock-Polizisten-Warning. Nun geht das Projekt in die BETA-Phase und die üblich Verdächtigen(WARNUNG: fefe link) ranten rum. Räudiges Python und alles viel zu einfach... und vor allem... jeder weiß doch wie man openssl richtig bedient und wo man richtige Zertifikate bekommt. Ich finde den Ansatz gut. Natürlich ist der Client noch nicht dort angekommen wo er ankommen will. Aber es funktioniert. Der Client bietet mehrere Modi an. Der eine öffent die Ports 80 und 443 und Letsencrypt schaut dann ob sie erreichbar sind und tauschen ein paar Sachen aus. Problem: Auf dem Server läuft meist schon ein Webserver und blockiert die Ports. Ich muss also NGINX stoppen, den Client starten und dann NGINX wieder anschieben. Irgendwie nicht so schön. Eine andere Möglichkeit ist webroot. Dafür legt der Client ein paar Ordner in das Root-Verzeichnis des Webservers und der Dienst schaut dann dort nach. Problem hierbei: Ich habe in meine NGINX keine Files im Root. Ich benutze NGINX nur als Reverse-Proxy. Es gibt Hilfe:

Das NGINX Snippet

location /.well-known/acme-challenge {
    alias /etc/letsencrypt/webrootauth/.well-known/acme-challenge;
    location ~ /.well-known/acme-challenge/(.*) {
        add_header Content-Type application/jose+json;
    }
}

Und wenn wir dieses Snippet nun in den server-Teil der NGINX config "includen" haben wir die passende Location damit Letsencrypt alles findet.

Letsencrypt im Docker-Container

Der Client versucht sich vor jedem Ausführen nochmal selber zu updaten. Keine schlechte Idee in der BETA-Phase. Ich dachte nur ich versuche alles ein wenig aufzubrechen und in ein Docker-Image zu gießen. Dies sollte (Dank Alpine-Linux) schön klein Sein und schon alle wichtigen Optionen im ENTRYPOINT beinhalten. Das man nur noch die Domains, für die man Zertifikate haben will, anhängt. Der Gedanke dahinter: Man kann es einfach in die Crontab packen und alle Abhängigkeiten sind im Container gefangen. Die Repo liegt hier. Für noch mehr Convenience nutze ich auch mein geliebtes docker-compose.

letsencrypt:
  build: letsencrypt/
  volumes:
    - /etc/letsencrypt:/etc/letsencrypt
    - /var/lib/letsencrypt:/var/lib/letsencrypt
  environment:
    EMAIL: marvin@xsteadfastx.org
    WEBROOTPATH: /etc/letsencrypt/webrootauth
  command:
    "-d emby.xsteadfastx.org -d cloud.xsteadfastx.org -d reader.xsteadfastx.org -d git.xsteadfastx.org"

Ich benutze Compose so oft wie es geht. Dort kann ich schön alles definieren wie der Container ausgeführt werden soll. Eine saubere Sache. Erstmal werden die Volumes eingebunden. Diese beiden Verzeichnise braucht der Letsencrypt-Client. Dann werden zwei Environment-Variablen definiert. Email und der Pfad in dem der Client die Files zur Authentifizierung ablegt. Als command werden die Domains jeweils mit der Option -d angehängt. Mit docker-compose up wird erst das Image gebaut und dann ausgeführt.

Ich bin gespannt wie es weitergeht. Natürlich ist mein Container auch nur der erste Entwurf... aber ich werde weiter daran basteln.

Emby mit NGINX als Reverse-Proxy

Ich bin ja vor einiger Zeit von Plex auf Emby gewächselt um sich meiner Mediendateien anzunehmen. Auch wenn Plex seinen Job gut machte, gibt es dort in mir ein kleinen Ort der bei nicht Open-Source Software ein wenig rebelliert und sich nicht so fluffig anfühlt. Zum Glück gibt es Emby. Es fühlt sich an manchen Ecken noch nicht so perfekt an wie Plex, macht seinen Job aber anständig. Dazu kommt ein Feature was ich stark vermisst habe: Encoding bei Audio-Files. Ich will über mein Handy kein FLAC streamen. Emby kann auch Serien selber verwalten. Das bedeutet, dass man die Files in ein Verzeichnis schmeißt und Emby die dann umbenennt und wegsortiert. Emby läuft bei mir in einem Docker Container und ich leite den Port an meinem Router weiter. Eine noch schönere Lösung ist ein Reverse-Proxy. Dann könnte alles unter einer Subdomain aufrufbar sein. Ein NGINX ist schon im Einsatz. Als Proxy für ein paar andere Docker Container. Also wieso nicht einfach auch für Emby benutzen? Bis jetzt läuft auch alles ziemlich gscheit.

server {
    listen 80;
    server_name emby.meinedomain.tld;

    keepalive_timeout 180;
    client_max_body_size 1024M;

    location / {
        proxy_pass http://127.0.0.1:8096;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $remote_addr;
        proxy_set_header X-Forwarded-Protocol $scheme;
        proxy_redirect off;

        # Send websocket data to the backend aswell
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Wichtig ist hier auch der Teil mit dem Websockets. Die müssen natürlich auch weitergeschubst werden. Zum Glück bietet NGINX da den passenden Support. Ja ich weiß, noch kein SSL. Wird aber nachgeholt.

Danke an Karbowiak.

NGINX force https

Am liebsten setze ich zur Zeit NGINX als Webserver ein. Vor allem für meine Owncloud installation und ein paar Python Sachen. Das "Problem" war bis jetzt, dass er Anfragen auf "http" nicht auf "https" weitergeleitet hat. Nicht wirklich ein Problem, sondern nur nervig wenn man wiedermal vergessen hat explezit "https" davor zu schreiben. In der Config mit dem funktionierenden SSL-Server Teil schreibe ich einen zweiten "server"-Bereich:

server {
    listen 80;
    server_name cloud.xsteadfastx.org;
    return 301 https://$server_name$request_uri;
}

Viele regeln das mit Regex, laut NGINX-Wiki ist dies aber die saubere Methode.

Isso ersetzt Disqus

Disqus ist böse. Zumindestens geht das gerade ganz schön durch die Blogosphere. Mal davon abgesehen bin ich immer Fan vieles selber zuhosten. Gerade wenn man darauf steht vieles auszuprobieren und nach Alternativen zu stöbern. bl1nk hat mich dabei auf Isso aufmerksam gemacht. Also stand das Ganze auf meiner Liste der Sachen die ich mit Pelican machen wollte. Und schon ging es los. Das ganze sollte in einer virtualenv-Umgebung laufen, mit Supervisor und gunicorn gestartet werden und durch NGINX geschleust werden.

Also erstmal mit "virtualenv" die Umgebung anlegen, isso und gunicorn installieren:

virtualenv isso
cd isso
. bin/activate
pip install isso
pip install gunicorn

Dies sind die drei Werte die ich in der isso.cfg (die liegt in meinem Fall unter /etc/isso/) angepasst habe:

[general]
# hier liegt das sqlite file mit den kommentaren
dbpath = /var/lib/isso/comments.db

# von diesem host werden kommentare zugelassen
host = http://code.xsteadfastx.org/

# isso hört auf diesem interface
[server]
listen = http://localhost:8004

Auf meiner Todo-Liste stehen noch Notifications per SMTP. Das kommt dann später. Supervisor habe ich mit der Distributions-Paketverwaltung installiert. Unter "/etc/supvervisor/conf.d/" habe ich "isso.conf" angelegt.

[program:isso]
command=/srv/www/comments/bin/gunicorn -b 127.0.0.1:8004 --preload isso.run
directory=/var/lib/isso
environment=PATH="/srv/www/comments/bin",ISSO_SETTINGS="/etc/isso/isso.cfg"
user=www-data
autostart=yes

Das schöne an Supervisor ist, dass man unter "environment" gleich noch ein paar Variabeln deklarieren kann. Läuft das Isso durch gunicorn und supervisor, kann man sich um NGINX kümmern. Dazu habe ich die config angelegt:

server {
    listen 80;
    server_name comments.xsteadfastx.org;
    access_log  /var/log/nginx/access.log;

    location / {
        proxy_pass http://127.0.0.1:8004;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Jetzt läuft schon mal Isso. Nun musste ich mein Pelican-Theme anpassen. Dazu habe ich in der "pelicanconf.py" die Variabel "ISSO_URL" eingeführt. Und "DISQUS_SITENAME" auskommentiert.

#DISQUS_SITENAME = "xsteadfastxistryingtocode"
ISSO_URL = 'http://comments.xsteadfastx.org'

Im Theme-Template Ordner habe ich das File "isso.html" angelegt.

{% if ISSO_URL %}
  <script data-isso="{{ ISSO_URL }}"
        data-isso-css="true"
        data-isso-lang="en"
        data-isso-reply-to-self="false"
        src="{{ ISSO_URL }}/js/embed.min.js">
      </script>
      <section id="isso-thread"></section>
{% endif %}

In "album.html" muss noch an der richtigen Stelle...

{% include 'isso.html' %}

...eingefügt werden. Falls es nicht ganz klar ist, kann man mein Beispiel auf GitHub anschauen.