Webapps als Container: Ghost-Blog mit Docker deployen

Es ist ja länger nichts passiert hier letztes Jahr. Ich hatte einiges um die Ohren. Letzte Woche wollte ich meine Blog-Software updaten und stelle fest: So einfach wird das nicht.

Sobald ein größerer Versionssprung ansteht vor allem von den alten Ghost Versionen vor 1.x, steht einiges an Arbeit an. PHP7 und Upgrade von Debian auf Version 9. Das will ich mir noch nicht antun. Da ich auf diesem Server noch einige andere Anwendungen laufen habe.

Also musste eine andere Lösung her. Ich wollte auch nicht mehr mit npm installieren und überhaupt... Ich hasse es, für kleinste Anwendungen einen Haufen an Abhängigkeiten manuell zum Laufen bringen zu müssen.

Ich habe letztes Jahr bereits ein größeres Projekt auf Containerlösung umgestellt. Vor allem aber stellte sich mir jetzt die Frage: Warum nicht einfach diese kleinen Webapps auch alle in eigene Container packen? Zumal es mittlerweile sowieso für fast alles fertige Container mit Minimal-Images zum Download gibt.

So auch für Ghost. Fertigen Container runterladen, richtig einstellen, fertig! Ganz so einfach ist es nicht aber auch nicht viel aufwendiger. Ich zeige dir kurz wie ich es gemacht habe.

Installiert habe ich unter Debian Jessie.

Docker installieren

Zuerst müssen wir wie hier beschrieben Docker installieren.

apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties common
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
echo "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list
apt-get update
apt-get install docker-ce
docker run hello-world

Image pullen

Jetzt holen wir uns das nötige Docker-Image. Zum updaten ziehst du dir später einfach nur das aktuellste Image. Die Daten liegen außerhalb des Containers wie wir gleich sehen werden. Wir können dieses Verzeichnis aber bereits anlegen. Hier werden später Blogeinträge, Einstellungen etc. gespeichert.

mkdir -p /srv/docker/ghost/
docker pull ghost

Da wir den Container nicht manuell starten wollen, benötigen wir noch ein Systemd Unitfile, welches das für uns erledigt (vim /etc/systemd/system/ghost.service).

[Unit]
Description=GHost Service
After=docker.service
Requires=docker.service

[Service]
#ExecStartPre=-/usr/bin/docker kill ghost
ExecStartPre=-/usr/bin/docker rm ghost
#ExecStartPre=-/usr/bin/docker pull ghost
ExecStart=/usr/bin/docker run --name ghost --publish 2368:2368 --env 'NODE_ENV=production' --volume /srv/docker/ghost/:/var/lib/ghost/content --volume /srv/docker/ghost/config.production.json:/var/lib/ghost/config.production.json ghost
ExecStop=/usr/bin/docker stop ghost

[Install]
WantedBy=multi-user.target

Du kannst die Zeile mit dem pull einkommentieren. Dann wird vorm Start des Containers automatisch die aktuellste Version heruntergeladen.

Jetzt müssen wir noch ein paar Grundeinstellungen für Ghost vornehmen. Grundsätzlich passiert das in einer Datei namens config.production.json. Diese müssen wir außerhalb des Containers halten, damit sie nicht bei einem Container Update überschrieben wird.

vim /srv/docker/ghost/config.production.json

{
  "url": "https://www.linuxfrickeln.de",
  "server": {
    "port": 2368,
    "host": "0.0.0.0"
  },
  "database": {
    "client": "sqlite3",
    "connection": {
      "filename": "/var/lib/ghost/content/data/ghost.db"
    }
  },
  "mail": {
    "from": "'Web Admin' <mailmaster@linuxfrickeln.de>",
    "transport": "SMTP",
        "options": {
            "host": "<smtp-host>",
            "port": "25",
            "auth": {
                "user": "mailmaster",
                "pass": "<password>"
            }
        }
  },
  "logging": {
    "transports": [
      "file",
      "stdout"
    ]
  },
  "process": "systemd",
  "paths": {
    "contentPath": "/var/lib/ghost/content"
  }
}

Mehr Informationen zu den relevanten Einstellungen findest du hier.

Anschließend können wir den Container abfeuern.

systemctl enable ghost && systemctl daemon-reload && systemctl start ghost
netstat -tulpen | grep 2368

Persistente Daten

Der Container steht und startet automatisch. Nun müssen wir uns noch um unsere Daten kümmern.

Deine Daten liegen nun in oben angelegtem Ordner unter ls -la /srv/docker/ghost/. Dank der mount Option wird dieses lokale Verzeichnis nun beim Start in den Container gemountet und stirbt nicht jedes Mal mit dem Container.

Proxy

Als Letztes solltest du den Ghost Port noch per Webproxy absichern, denn in dem minimalen Ghost-Image ist kein vernünftiger Webserver enthalten.

Ich benutze NGINX (vim /etc/nginx/sites-available/linuxfrickeln.nginx).

server {
    listen 80;
    listen [::]:80;
    server_name linuxfrickeln.de www.linuxfrickeln.de;
    return 301 https://www.linuxfrickeln.de$request_uri;
}

server {
    listen 443;
    listen [::]:443;
    server_name linuxfrickeln.de www.linuxfrickeln.de;
    root /usr/share/nginx/www/linuxfrickeln.de;

    location / {
        proxy_set_header    X-Real-IP $remote_addr;
        proxy_set_header    Host      $http_host;
        proxy_set_header    X-Forwarded-Proto $scheme;
        proxy_pass          http://127.0.0.1:2368;
    }

    ssl on;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_ciphers 'AES256+EECDH:AES256+EDH';
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout  5m;
    ssl_dhparam </path/to/your/dhparams.pem>;
    add_header Strict-Transport-Security max-age=535680000;
    ssl_certificate /etc/letsencrypt/live/linuxfrickeln.de/fullchain.pem;
    ssl_certificate_key </path/to/your/private_key_file.key>;

    access_log      /var/log/nginx/linuxfrickeln.de.access.log;
    error_log       /var/log/nginx/linuxfrickeln.de.error.log;
}

Nach dem Speichern noch aktivieren:

ln -s /etc/nginx/sites-available/linuxfrickeln.nginx /etc/nginx/sites-enabled/linuxfrickeln.nginx