Baví mě docker. Už dlouho mě baví docker. S ním píšu všechny weby. Plno toolů pouštím jen přes docker. Ale do teď mi na mých VPS běžel software vždy na bare-metalu. Nějaký ten Apache, MySQL a při spuštění nové služby vždy vše naklikat ve Webminu. Prostě nuda a otrava.

Chtěl jsem konečně mít i své servery konfigurovatelné pomocí kódu. A nechtěl jsem se učit moc nového. Nejdříve jsem zkoušel Kubernetes. Všichni říkají, jak jsou jednoduché. Ale Kubernetes jsou trochu takový ledovec. Na začátku je to deployment na pod a ingress na routování provozu. Následně metriky, logování, volumes, configy, helm …

No rozhodl jsem se na to jít jinak. Na většině projektů používám docker-compose a kamarádím se s docker-compose.yml formátem. Hledal jsem tedy ideálně něco, co by tento formát bez větších změn sežralo a pustilo na mé VPS. Ideálně automaticky přes CI a na svém počítači pustit jen git push a o víc se nestarat.

A našel jsem ideální řešení. Docker Swarm mode!

❗ Neplést si Docker Swarm mode s Docker Swarm - Docker Swarm je starší a nyní zastaralý produkt.

Abych nemusel vypisovat všechno, budu se odkazovat na vinikající příručku Docker Swarm Rocks kde je spousta podstatných kroků instalace.

Instalace

Nejdřív pár základních předpokladů:

  • vlastní VPS - tento blog a infra běží na základní VPS od Wedosu a docela se to fláká
  • účet na Gitlabu - stačí ten zdarma
  • DNS blogu, která ukazuje na IP VPS
  • kus času tenhle návod projít

Setup VPS

Celá VPS musí běžet na platformě, na které poběží Docker. Třeba OpenVZ s tím má problém. Prvním krokem tedy bude instalace dockeru. Hrdě postupuj podle oficiálního návodu.

Já všude využívám naprosto skvělý nástroj direnv takže běž a nainstaluj si jej podle instrukcí taky ;-)

Dále na VPS vytvoř samostatného uživatele jen pro účely deploymentu z CI. Já tohohle uživatele nekreativně nazval deployment. Většinou na to bude tenhle příkaz:

# adduser deployment

Přihlaš se pod tímto uživatelem (nové SSH session, nebo sudo či podobné …)

Vygeneruj SSH klíče a NEzadávej passphrase:

ssh-keygen -t ed25519 -C "<comment>"

Více v nápovědě gitlabu

Následně veřejný klíč (s koncovkou .pub v adresáři ~/.ssh) přidej do autorizovaných klíčů. Název souboru veřejného klíče se může lyšit podle použité metody šifrování klíče. Takže tento příklad platí, pokud jsi použil předchozí příklad.

cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys

To nám v budoucnu zajistí, že se z CI runneru v gitlabu budeme moci připojit pomocí privátního klíče na náš server.

Teď si vyber složku, ve které budou uloženy docker-compose soubory, sloužící k deploymentu. Může to být přímo ~ uživatele, ale já si vytvořil složku ~/stack.

Instalace Docker Swarm mode

Následují 3 “domácí úkoly”. Instalace Docker Swarm mode, Traefiku a Swarmpitu. Tady pomůže příručka Docker Swarm Rocks

  1. Docker Swarm mode
  2. Traefik
  3. Swarmpit

Samořejmě můžeš instalovat i další nástroje podle příručky.

Jekyll & Caddy - příprava blogu

Vytvoř si GIT repozitář. Jestli nejprve lokálně pomocí git init a nebo pomocí nápovědy po vytvoření repozitáře na gitlabu je vcelku jedno.

První budeš potřebovat nějaký obsah blogu. Já jej vytvářím pomocí Jekyllu, což je generátor statických webů napsaný v Ruby. Klidně použij svůj, jen budeš muset na závěr upravit CI skripty.

V repozitáři vytvoř složku pro blog website:

mkdir website

Vybre si vzhled blogu třeba na jekyllthemes.org nebo použij defaultní Minima a nakopíruj obsah repozitáře s tématem do složky website.

Obsah blogu musí Jekyll zkompilovat a na to slouží tento příkaz:

docker run --rm \
    --volume="$PWD/website:/srv/jekyll" \
    --volume="$PWD/vendor/bundle:/usr/local/bundle" \
    -it jekyll/jekyll:4.0 \
    jekyll build

Výsledek Jekyll uloží do složky website/_site v repozitáři.

Jekyll umí i jednoduchý webserver:

docker run --rm \
    --volume="$PWD/website:/srv/jekyll" \
    --volume="$PWD/vendor/bundle:/usr/local/bundle" \
    -p 4000:4000 \
    -it jekyll/jekyll:4.0 \
    jekyll serve

A blog bude dostupný na http://localhost:4000/

Taskfile

Tak jsme si procvičili docker ale zadávat stále dokola takhle dlouhé příklady je nepraktické. Proto mám ve svém repozitáři soubor Taskfile. Je to takový jednoduchý task runner.

Skládá se z bash funkcí, které můžeš volat pomocí ./Taskfile help - to help je funkce v souboru. A nebo si v .bashrc vytvoříš tenhle malý helper:

alias run=./Taskfile

A potom můžeš spouštět příkazy pomocí run help. Což je kratší a pohodlnější :-)

Zároveň se v Taskfile opakují určité příkazy a proto nyní použijeme příkaz direnv, který sis určitě nainstaloval.

Nejprve vytvoř soubor .envrc a do něj vepiš:

export JEKYLL_VERSION=4.0
export APP_NAME=---NAZEV APLIKACE MALYMI PISMENY---
export IMAGE_NAME=---NAZEV REPOZITARE---/${APP_NAME}
export IMAGE_TAG=latest
export DOMAIN=---DNS DOMENA APLIKACE---
export VPS=---DNS VPS---
export DEPLOYMENT_USER=deployment

A změň všechna ---XXX--- na odpovídající tvému projektu.

Potom spusť

direnv allow

A od této chvílse se ti vždy všechny proměnné, které jsme definovali v .envrc exportují při vstupu do adresáře a při opuštění adresáře je naopak direnv smaže z aktuálního kontextu shellu.

Celý Taskfile si můžeš zkopírovat z mého repozitáře, nebo si napši vlastní. Já budu dále pracovat s tím mým.

Caddy

Teď potřebujeme nějakého pomocníka, který nám bude statické html soubory servovat. Tedy webserver. Vybral jsem Caddy, protože je extra malý a extra jednoduše konfigurovatelný. Výborně se hodí na malé VPS.

Vytvoř si konfigurační soubor Caddyfile v adresáři webserver/caddy s následujícím obsahem:

# The Caddyfile is an easy way to configure your Caddy web server.
#
# Unless the file starts with a global options block, the first
# uncommented line is always the address of your site.
#
# To use your own domain name (with automatic HTTPS), first make
# sure your domain's A/AAAA DNS records are properly pointed to
# this machine's public IP, then replace the line below with your
# domain name.
:80

# Set this path to your site's directory.
root * /srv

# Enable the static file server.
file_server

# Another common task is to set up a reverse proxy:
# reverse_proxy localhost:8080

# Or serve a PHP site through php-fpm:
# php_fastcgi localhost:9000

# Refer to the Caddy docs for more information:
# https://caddyserver.com/docs/caddyfile

Jak vidíš, ke konfiguraci serveru potřebujeme akorát 3 řádky. Všechno ostatní je omáčka 🙂

Teď si vytvoř Dockerfile v rootu projektu. Ten nám bude sloužit na build výsledného image s Caddy webserverem a kopií staticky generovaného blogu.

FROM caddy:2-alpine

COPY webserver/caddy/config/Caddyfile /etc/caddy/Caddyfile
COPY website/_site /srv

Opět velmi jednoduché, pouze nakopírujeme do základního image konfigurační soubor a pak vygenerovaný blog.

Nejdříve buidni image pro webserver:

docker build -t $IMAGE_NAME:$IMAGE_TAG .

Všimni si, že používám v příkazu proměnné, které jsme si definovali v .envrc.

Nyní můžeme vše zkusit pustit (nezapomeň zbuildit Jekyll 😉 ):

docker run -d --rm -p 8080:80 $IMAGE_NAME:$IMAGE_TAG

A blog bude dostupný na http://localhost:8080/

Deploy

Teď už nám zbývá vyzkoušet jen lokální deploy. Aby fungoval, nezapomeň přidat veřejný SSH klíč na server pod uživatele deployment třeba takto:

ssh-copy-id deployment@tvuj.server.cz

Taky musíme pushnout image webserveru do registry na gitlabu. K tomu je potřeba se nejprve přihlásit a k přihlášení budeš pravděpodobně potřebovat svůj personal token.

docker login registry.gitlab.com

Následně pushni obraz do registry:

docker push $IMAGE_NAME:$IMAGE_TAG

Na serveru musíš mít samozřejmě již vše nainstalované, včetně utility direnv.

Jako poslední věc potřebuješ docker-compose.stack.yml pro naši aplikaci

version: '3.3'

services:
  app:
    # image name z .envrc - image již musí být pushnutý do gitlab registry
    image: ${IMAGE_NAME}:${IMAGE_TAG}
    volumes:
      # toto jsou doporučené volumes pro Caddy server
      - www-data:/data
      - www-config:/config
    networks:
      # vlastní síť pro tuto aplikaci
      - net
      # nutná síť pro propojení s traefik
      - traefik-public
    # tato sekce je speciálně pro deployment a nevyskytuje se normálne v docker-compose
    deploy: 
      # limity pro aplikaci
      resources: 
        limits:
          cpus: '0.25'
          memory: 128M
      # podmínky pro deployment aplikace - pokud máš více VPS v jednom swarmu 
      # a chceš ji mít na konkrétní VPS
      placement: 
      # já zde porovnávám VPS label s názvem VPS z .envrc souboru, pro to jsem 
      # musel přidat label k VPS pomocí 
      # docker node update --label-add vps.name=vps2.biberle.cz $(docker info -f '\{\{.Swarm.NodeID\}\}')
      # (příkaz bech těch \ )
      # možno vynechat pro samostatnou VPS
        constraints: 
          - node.labels.vps.name == ${VPS}
      # labely, které říkají traefiku, jak má nakonfigurovat provoz na tuto aplikaci, 
      # jakou má DNS a že má použít HTTPS a certifikát generovaný LetsEncrypt
      labels: 
        - traefik.enable=true
        - traefik.docker.network=traefik-public
        - traefik.constraint-label=traefik-public
        - traefik.http.routers.${APP_NAME}-http.rule=Host(`${DOMAIN?Variable not set}`)
        - traefik.http.routers.${APP_NAME}-http.entrypoints=http
        - traefik.http.routers.${APP_NAME}-http.middlewares=https-redirect
        - traefik.http.routers.${APP_NAME}-https.rule=Host(`${DOMAIN?Variable not set}`)
        - traefik.http.routers.${APP_NAME}-https.entrypoints=https
        - traefik.http.routers.${APP_NAME}-https.tls=true
        - traefik.http.routers.${APP_NAME}-https.tls.certresolver=le
        - traefik.http.services.${APP_NAME}.loadbalancer.server.port=80

networks:
  net:
    driver: overlay
    attachable: true
  traefik-public:
    external: true

volumes:
  www-data:
    driver: local
  www-config:
    driver: local

Následující příkazy provedou deploy:

# smaže adresář aplikace na serveru - nemá vliv na již běžící deployment - konfigurace je uložená v dockeru
ssh -t $DEPLOYMENT_USER@$VPS "rm -rf /home/$DEPLOYMENT_USER/stack/$APP_NAME"

# vytvoří adresář aplikace na serveru
ssh -t $DEPLOYMENT_USER@$VPS "mkdir -p /home/$DEPLOYMENT_USER/stack/$APP_NAME"

# zkopíruje nové nastavení .envrc na server
scp .envrc $DEPLOYMENT_USER@$VPS:/home/$DEPLOYMENT_USER/stack/$APP_NAME/.envrc

# zkopíruje nový předpis pro stack deploy
scp docker-compose.stack.yml $DEPLOYMENT_USER@$VPS:/home/$DEPLOYMENT_USER/stack/$APP_NAME/docker-compose.stack.yml

# povolí veškeré změny v .envrc souboru k načtené
ssh -t $DEPLOYMENT_USER@$VPS "direnv allow /home/$DEPLOYMENT_USER/stack/$APP_NAME"

# spustí deploy
ssh -t $DEPLOYMENT_USER@$VPS "cd /home/$DEPLOYMENT_USER/stack/$APP_NAME/ ; direnv exec ./ docker stack deploy -c docker-compose.stack.yml $APP_NAME --with-registry-auth"

Pokud vše funguje správně, měl bys za chvíli vidět ve Swarmpitu novou aplikaci, stáhnutí obrazu z gitlabu a následný deploy. Nakonec se nakonfiguruje podle labelů traefik a blog bude přístupný.

Automatizace s Gitlab-CI

Určitě nechceš všechny tyhle příkazy spouštět ručně. Na to je tu poslední kousek skládanky. Soubor .gitlab-ci.yml, který řekne Gitlabu, jaké skripty spustit pro build a následný deploy tvé aplikace.

V CI používám vlastní docker obraz. Přidávám do základního docker image balíčky pro openssh-client, curl, bash a direnv. Celý tento skript můžeš pouštět při každém běhu tvé pipeline v before_script:, jen to bude pomalejší …

Celý soubor ti opět okomentuju.

default:
  # vlastní docker obraz ve kterém běží všechny následující skripty
  image: registry.gitlab.com/vojtabiberle/docker:latest

variables:
  JEKYLL_VERSION: '4.0'

services:
  # díky této service můžeš buildit docker v dockeru
  - docker:dind

stages:
  - build
  - push
  - deploy

before_script:
  # login into registry
  - echo -n $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
  # tady je dobré místo na instalaci balíčků do základního docker obrazu, pokud si nevytvoříš vlastní

Build:
  stage: build
  only:
    - master
  script:
    # build webu pomocí Jekyllu
    - docker run --volume="$PWD/website:/srv/jekyll" jekyll/builder:$JEKYLL_VERSION jekyll build
    # build webserveru s Caddy
    - docker pull $CI_REGISTRY_IMAGE:latest || true
    - docker build --pull --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    # push do registry s tagem ID commitu
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

Push latest:
  variables:
    # v tomto jobu pracuji jen s dockerem a nepotřebuji tedy klonovat git
    GIT_STRATEGY: none
  stage: push
  only:
    - master
  script:
    # přidání tagu latest vždy k poslednímu commitu, který pushnu
    - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
    - docker push $CI_REGISTRY_IMAGE:latest

Push tag:
  variables:
    GIT_STRATEGY: none
  stage: push
  only:
    - tags
  script:
    # pokud pushnu do gitu jakýkoliv tag, vytvoří mi i specifický tag pro obraz - to je dobré proto, abych mohl nasadit specifickou verzi blogu - stačí jen přepsat v docker-compose.stack.yml
    - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME

Deploy:
  stage: deploy
  before_script:
    # add SSH keys for deploy
    - mkdir -p ~/.ssh
    # přidání VPS certifikátu do known hosts
    - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts
    # přidání privátního klíče pro deploy
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | ssh-add -
    # loading all variables from .envrc
    - direnv allow
    - eval "$(direnv export bash)"
  script:
    - ./Taskfile deploy
  only:
    - master

Inspiraci k .gitlab-ci.yml jsem bral z: https://blog.callr.tech/building-docker-images-with-gitlab-ci-best-practices/

Ještě než budeš jásat, že je hotovo, musíš udělat poslední krok.

CI variables

V gitlabu si otevři Settings > CI / CD > Variables a přidej 2 proměnné.

$SSH_KNOWN_HOST

Na serveru spusť příkaz

ssh-keyscan localhost

Který ti vypíše veřejný SSH klíč pro server. Je dobré jej ověřovat, aby se někdo nepokoušel unést SSH spojení.

$SSH_PRIVATE_KEY

Zde bude obsahem privátní klíč uživatele deployment na VPS. Bez toho se CI nebude moci připojit k serveru a spustít deployment.

A když nyní vše commitneš a pushneš do repositáře, měl by gitlab rozpoznat .gitlab-ci.yml, pustit CI pipeline. Zbuildí blog a webserver a následně dá pomocí docker-compose.stack.yml příkaz dockeru k deploymentu nové verze tvého blogu 🎉

A to je absolutní konec. Pokud jsi dočetl až sem, gratuluji. Je to výkon!

Když objevíš chybu nebo nepřesnost, pošli mi merge-request nebo založ nové issue.

Závěrem

Tohle je samozřejmě pouze začátek. Právě sis otevřel cestu k tomu, podobným způsobem obsluhovat jakýkoliv software na tvé VPS. Teď už jen vezmeš Taskfile, upravíš proměnné v .envrc, zkopíruješ docker-compose.stack.yml a přidáš potřebné services pro novou aplikaci a nakonec zkopíruješ .gitlab-ci.yml a provedeš patřičné změny v buildu. Některé projekty nebudou build potřebovat vůbec, takže budeš do repozitáře pushnovat jen, pokud si budeš přát změnit konfiguraci aplikace.

A to je už opravdu všechno. Někdy příště ti možná ukážu další aplikaci. Sám si brousím zuby na WikiJS nebo možná BookStack.