NGINX im Container und docker-gen

source: http://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.

Ansible Playbooks und variable Hosts

Ich benutze Ansible für viele Sachen. Unter anderem habe ich ein Set an Playbooks und Roles um meine Server so anzupassen das ich mich auf ihnen wohl fühle. Bis jetzt habe ich immer Ansible auch auf den Servern installiert und es dann lokal ausgeführt. Aber dabei nutze ich nicht das eigentlich Ansible Feature: das Ganze Remote über SSH auszurollen. Problem: In den Playbooks muss es die Zeile - hosts: geben. Aber eigentlich will ich das alles Variabel ausführen können. Zum Beispiel nutze ich die gleichen Files auch um meine Vagrant Container einzurichten. Wieso also beim Aufruf die Hosts dem Playbook nicht einfach übergeben? Die Lösung ist dann doch wieder einmal einfacher als man denkt. Man benutzt die Möglichkeit in Playbooks und Roles Variabeln einzusetzen.

---
- hosts: "{{ hosts }}"
  roles:
    - git
    - tmux
    - vim
    - zsh

Diese Variabel übergeben wir beim Aufruf von Ansible.

ansible-playbook base.yml --extra-vars="hosts=myservers"

Jenkins und die Dockerception

Nach meinen ersten Experimenten mit Jenkins und Docker geht es nun wieder einen Schritt weiter. Mein Jenkins läuft im Docker-Container hinter einem NGINX im Docker-Container. Was könnte also noch eine Steigerung sein? Noch schöner wäre es wenn die Tests bzw Builds in eigenen Containern laufen, die dann nach dem Run wieder in sich zusammen fallen. Ich versuche mal alles zusammen zuschreiben.

Docker in Docker

Der beste Weg ist es den Docker-Socket als Volume an den Container durchzureichen. Hier der docker-compose-Eintrag:

jenkins:
  container_name: jenkins
  build: ./jenkins/
  volumes:
    - "/srv/www/jenkins:/var/jenkins_home"
    - "/usr/bin/docker:/bin/docker"
    - "/usr/lib/x86_64-linux-gnu/libapparmor.so.1.1.0:/usr/lib/x86_64-linux-gnu/libapparmor.so.1"
    - "/var/run/docker.sock:/var/run/docker.sock"
  ports:
    - "8080:8080"
    - "50000:50000"

Wir müssen nicht nur den Socket durchreichen, sondern auch das Docker-Binary und eine Library (war in diesem Fall nötig). Sonst nichts besonderes hier. Die Ports sind einmal für die Jenkins Weboberfläche und einmal für die Kommunikation mit den Slaves die sich im Netz befinden. In meinem Fall ein Windows Build Slave.

Docker in Jenkins

Ich benutze das offizielle Jenkins Docker Image mit ein paar Anpassungen. Das Dockerfile sieht wie folgt aus:

FROM jenkins

USER root

RUN apt-get update \
 && apt-get install -y \
    rsync \
 && rm -rf /var/lib/apt/lists/*

RUN addgroup --gid 116 docker \
 && usermod -a -G docker jenkins

USER jenkins

Ich installiere ein rsync und füge den Benutzer jenkins der docker-Gruppe hinzu damit der die Container starten, beenden und löschen kann.

Jenkins hat viele Plugins. Diese wiederum haben kaum bis garkeine Dokumentation oder klaffende Lücken in ihr. Also habe ich erstmal, ziemlich naiv, das Docker Plugin installiert. Jenkins braucht erstmal eine Connection zu Docker. Dazu gehen wie in die Jenkins System Konfiguration in die Cloud Sektion. Neben einem Namen sollten wir auch die Docker URL eintragen. Die kann auch ein Socket sein. In diesem Fall unix:///var/run/docker.sock. Nun klappt die Kommunikation.

tox-jenkins-slave

Nun brauchen wir ein Docker-Image das als Jenkins-Slave taugt. Ich habe mich ganz an dem Image von evarga orientiert. Mein Image beinhaltet ein paar Änderungen. Zum Beispiel installiere ich nicht das gesammte JDK, sondern nur ein Headless-JRE. Sonst installiert er mir schön diverse X-Libraries. Die braucht man nicht um den Jenkins-Slave-Client auszuführen. Ich bin riesen tox-Fan. Ein Tool um Tests in diversen Python Versionen auszuführen. Alles getrennt durch Virtualenv's. Dazu braucht tox aber auch die Python Versionen installiert. Dies funktioniert dank dem PPA von fkrull ganz wunderbar. Zum Schluß installiere ich tox selber und fertig ist das Image. Ach ja, ein openssh-server und git wird auch noch gebraucht. Hier das Dockerfile:

FROM ubuntu:trusty

RUN gpg --keyserver keyserver.ubuntu.com --recv-keys DB82666C \
 && gpg --export DB82666C | apt-key add -

RUN echo deb http://ppa.launchpad.net/fkrull/deadsnakes/ubuntu trusty main >> /etc/apt/sources.list \
 && echo deb-src http://ppa.launchpad.net/fkrull/deadsnakes/ubuntu trusty main >> /etc/apt/sources.list

RUN apt-get update \
 && apt-get install -y \
    git \
    openssh-server \
    openjdk-7-jre-headless \
    python-pip \
    python2.3 \
    python2.4 \
    python2.5 \
    python2.6 \
    python3.1 \
    python3.2 \
    python3.3 \
    python3.4 \
    python3.5 \
 && rm -rf /var/lib/apt/lists/*

RUN pip install tox

RUN sed -i 's|session    required     pam_loginuid.so|session    optional     pam_loginuid.so|g' /etc/pam.d/sshd \
 && mkdir -p /var/run/sshd

RUN adduser --quiet jenkins \
 && echo "jenkins:jenkins" | chpasswd

EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]

Es wird ein jenkins Benutzer mit dem Password jenkins angelegt. Wenn sich jemand daran stört soll er sich das Image selber bauen und dies verändern.

Um es zum Einsatz zu bringen müssen wir unter Configure System -> Cloud -> Docker -> Add Docker Template folgendes einstellen:

  1. Docker Image: xsteadfastx/tox-jenkins-slave
  2. Remote Filing System Root: /home/jenkins
  3. Labels: tox-jenkins-slave
  4. Launch method: Docker SSH computer launcher
  5. Credentials: Add a new one with username jenkins and password jenkins
  6. Remote FS Root Mapping: /home/jenkins
  7. Remove volumes: True

Mir war nicht klar das das Label ausschlaggebend ist um den Job auch wirklich auf den passenden Container laufen zu lassen. Ohne Label kann dieser nicht getriggert werden.

Nun können wir im Job den Container einsetzen:

  1. Docker Container: True
  2. Restrict where this project can be run: True
  3. Label Expression: tox-jenkins-slave

Sachen mit denen ich noch so gekämpft habe

Aus irgendeinem Grund hatte ich riesige Probleme mit den Locales in dem Container. Einige Tests haben Vergleichsdaten in Textfiles gespeichert. Falsche Locales haben die Tests gebrochen. Das Absurde... egal was ich tat, es half einfach nichts. LANG setzen in der .bashrc oder in /etc/profile wurde vollständig ignoriert. Startete ich den Container manuell und loggte mich per SSH ein, kein Problem, die Tests liefen durch. Der Slave gestartet von Jenkins, völlige Ingoranz meiner Config. Also musste ich es direkt in das Python Job Script packen:

import os
import tox

os .environ['LANG'] = 'C.UTF-8'
os.chdir(os.environ['WORKSPACE'])

tox.cmdline()

Ich setze die Environment-Variabel direkt im Script, gehe in den Jenkins-Workspace und führe tox aus. Ein Umweg, aber es funktioniert.

Schenkt den PyPI-Servern eine Auszeit

Noch eine Kleinigkeit: Da die Abhängigkeiten für die Tests bei jedem Durchlauf neu heruntergeladen werden lohnt sich der Einsatz von devpi. Der kann sehr viel mehr, wird aber von mir als einfacher PyPI-Mirror missbraucht. Ich habe dafür auch ein kleines Image. Läuft der Container sieht mein Jenkins Job kompletto so aus:

import os
import tox

os.environ['LANG'] = 'C.UTF-8'

os.makedirs('/home/jenkins/.config/pip')
with open('/home/jenkins/.config/pip/pip.conf', 'w') as f:
  f.write('[install]\ntrusted-host = 192.168.1.8')

os.environ['PIP_INDEX_URL'] = 'http://192.168.1.8:3141/root/pypi/+simple/'

os.chdir(os.environ['WORKSPACE'])

tox.cmdline(['-e py33,py34,py35'])

Von nun an wird der Mirror benutzt.

source: http://www.tumblr.com

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.