Hoe start je een applicatie in Docker, en hoe zorg je dat die na een reboot automatisch opnieuw gestart wordt?

Introductie voor Docker-newbies die gewoon Willen Dat Die Zooi Werkt.

Op Mastodon stelde Matijs de vraag:

Does anyone have an introductory article on how to run a dockerized application on a server? All articles I can find either start with 'this is only for dev', or they talk a lot about other stuff and leave out how to:

- run the damn thing
- make sure it restarts when the server reboots.

I have a feeling this is in fact super-easy so everyone just assumes readers know this and skips it.

Goeie vragen, en ja, stiekem is het niet moeilijk - als je eenmaal weet hoe het zit. Ik weet intussen hoe het zit, en kan natuurlijk altijd zelf dat introducerende artikel schrijven. Waarvan akte.

Om de vragen van Matijs te beantwoorden, moeten we bij het begin beginnen.

Wat doet Docker nou eigenljik?

Docker laat je gemakkelijk software "in een container" draaien. Dat "container" slaat een beetje op twee dingen:

  1. De hele applicatie en alles wat ervoor nodig is, bevindt zich in de container
  2. De applicatie is begrensd tot die container en kan zich - in principe - niet bemoeien met wat zich daarbuiten afspeelt.

Dat maakt natuurlijk meteen duidelijk waarom iedereen en zijn moeder er zo ontzettend mee wegloopt, want het is voor zowel ontwikkelaars als voor gebruikers makkelijk: geen gehannes met packages voor verschillende besturingssystemen of distro's, geen gekloot met dependencies, niet het altijd ietwat ongemakkelijke gevoel dat je niet precies weet wat die ene appliactie nou eigenlijk allemaal aan het doen is en of "al je kattenfoto's naar China sturen" daar deel van uitmaakt.

Docker doet zijn kunstje met behulp van een stukje technologie in de Linux-kernel, containers, en regelt daar wat software omheen om er makkelijk mee te werken. Het is niet de enige software die dit doet (en de technologie bestaat niet alleen in Linux), maar momenteel wel de meest bekende en meestgebruikte.

Ik heb dat een paar jaar geleden al eens besproken, maar in het kort werkt de workflow een beetje als volgt:

  • Een Dockerfile definieert de stappen om van nul tot image te komen
  • Een image kan ook weer als basis voor een ander image dienen (php:7-alpine is gebaseerd op alpine)
  • Een image is een "blauwdruk" voor containers. Het image zelf verandert daarbij niet, elke nieuwe container is een verse instantie van het image, die je apart kunt starten, stoppen, weggooien, enzovoorts.
  • Naast images en containers kun je ook networks definiëren (waarmee je kleine netwerkjes maakt om tussen een paar containers, maar niet daarbuiten, te gebruiken)
  • Tenslotte gebruik je volumes om data buiten een container op te slaan, en dus ofwel met meerdere containers te kunnen delen, ofwel te laten bestaan op het moment dat je de container(s) zou weggooien.

Images moeten natuurlijk ergens worden opgeslagen. Zo'n plek noemt men een registry, en de meestgebruikte (want standaard) is Docker Hub.

Dit is allemaal héél erg tl;dr en er zit natuurlijk wat meer nuance in, maar voor nu is het genoeg detail. Wil je meer, dan is er altijd de documentatie.

Hoe draai ik Docker?

Als je op Linux werkt, is het over het algemeen een kwestie van de juiste versie van Docker voor jouw distributie installeren en bij elke boot automatisch te laten opstarten. Hoe het pakket voor Docker precies heet wisselt (uiteraard) per distro, meestal zal dat simpelweg "docker" zijn. Na installatie zal het niet in alle gevallen zo zijn dat Docker automatisch wordt gestart, over het algemeen zal dat neerkomen op

sudo systemctl enable --now docker

om dat voor elkaar te krijgen. Mocht je geen SystemD gebruiken, dan ben je eigenwijs en hopelijk ook kundig genoeg om de handleidingen voor jouw situatie door te spitten... Als het goed is, kun je nu controleren op Docker draait met

sudo docker ps

Mocht je niet alle docker-gerelateerde commando's als root willen uitvoeren, zorg er dan voor dat de gebruiker waarmee je wil werken in de group docker zit:

sudo usermod -aG docker <username>

Uitloggen, inloggen, boom, done.

Ja, maar ik draai MacOS of Windows

Omdat Docker leunt op technologie in de Linux-kernel, draaien de Mac- en Windowsversie op basis van een kleine Virtual Machine met een heel lief klein Linuxje erin. Als het goed is, wordt die VM automatisch bij het opstarten mee opgestart, en anders is dat een instelling ergens in de desktop-app. Verder zou het niet uit moeten maken.

OK, Docker draait, en ik ben zelfs een keer herstart en Docker draait nog steeds, geweldig, hoe start ik nu...

Ja, en hier wordt het interessant: dat hangt ervan af. Om precies te zijn, dat hangt af hoe de ontwikkelaar van je applicatie het allemaal heeft voorbereid. Er zijn nu een paar opties:

  • Er wordt in de documentatie iets geroepen met een docker run-commando
  • Er wordt een docker-compose.yml met de software meegeleverd
  • Er wordt in de hele documentatie met geen woord over Docker gerept. Spijtig.

Ik bewaar de laatste twee even voor een volgende keer, en focus voor nu op de docker run-methode.

Docker run

De documentatie van je software vermeldt iets over een command dat met docker run begint. Daar zal uiteindelijk de naam van een image in voorkomen, en daarbij nog een aantal extra opties. Een simpel voorbeeld is dat van Zipkin, wat Matijs wilde draaien:

The quickest start is to run the latest image directly:

docker run -d -p 9411:9411 openzipkin/zipkin

Dat commando start een container op van het image openzipkin/zipkin op Docker Hub, geeft die een willekeurige naam (die je ook terugziet in een docker ps en in plaats van het container-ID kunt gebruiken in diverse vervolgopdrachten), en laat hem in de achtergrond draaien met -d. De applicatie die binnen de container draait, zou normaal bereikbaar zijn via TCP-poort 9411, dus om ervoor te zorgen dat wie die buiten het micronetwerkje - binnen de container - kunnen gebruiken, wordt die poort doorgelust met -p buiten:binnen. Heb je meerdere poorten, dan kun je meerdere van die opties meegeven.

De eerste keer dat je dit commando uitvoert, wordt eerst het image (en eventueel een image dat de basis vormde, en eventueel een image dat daarvoor de basis vormde, enzovoorts - in geval van Zipkin zijn het 12 lagen) binnengeharkt en uitgepakt, daarna is dat niet meer nodig en start de container dus veel sneller op.

Als laatste regel in de uitvoer krijg je het container-ID terug:

➜  ~ docker run -d -p 9411:9411 openzipkin/zipkin
Unable to find image 'openzipkin/zipkin:latest' locally
latest: Pulling from openzipkin/zipkin
24f0c933cbef: Pull complete 
69e2f037cdb3: Pull complete 
59a80cb28236: Pull complete 
3e010093287c: Pull complete 
e8a2f01ee178: Pull complete 
4ba4d0487b37: Pull complete 
c9c6c255df13: Pull complete 
e9f4a56101fe: Pull complete 
21c56b02dc78: Pull complete 
bfabf13976c4: Pull complete 
45d52baeedd9: Pull complete 
a1a5cce8a593: Pull complete 
Digest: sha256:eca59b59b8b4d2345e703872bd9206c804a48cb1a1ac80e8604038060de63e8b
Status: Downloaded newer image for openzipkin/zipkin:latest
a74b0b12fa57bb83e776b63977f455db0fd81785611075d772f5fb353d24eb5c

➜  ~ docker ps -l
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS                              NAMES
a74b0b12fa57        openzipkin/zipkin   "/busybox/sh run.sh"   5 minutes ago       Up 5 minutes        9410/tcp, 0.0.0.0:9411->9411/tcp   thirsty_cerf

In dit geval die reeks die met a74b0... begint. De willekeurige naam van deze container is thirsty_cerf (het is altijd een combinatie van twee woorden). In de meeste gevallen kun je in vervolgcommando's volstaan met de eerste 6 karakters van het ID of de naam.

Alternatief: je geeft de container een specifieke naam bij het opstarten:

docker run -d -p 9411:9411 openzipkin/zipkin --name zipkin

...maar als je dat direct na dat vorige commando doet, krijg je een melding die je meldt dat er een poort al in gebruik is. Logisch, want poort 9411 was al toegewezen aan de vorige container. Die moet je eerst stoppen voordat je een nieuwe kunt starten:

➜  ~ docker stop thirsty_cerf # of wat de naam van jouw specifieke container was, of het ID
thirsty_cerf
➜  ~ docker rm thirsty_cerf # de oude container verwijderen, anders blijft die staan
➜  ~ docker run -d -p 9411:9411 --name zipkin openzipkin/zipkin
d98cfd81b5cddfa0ea8ea62bcdfa74c3540b2474833229ab01e42f11dff5763f

Zelfde image, nieuwe container (en dus een nieuw ID), dit keer met een naam die we kunnen onthouden.

Mocht je de computer herstarten, dan wordt deze container niet opnieuw meegestart. Om dat wél te doen, voeg je een optie aan de commando-regel toe:

➜  ~ docker stop zipkin
➜  ~ docker rm zipkin
➜  ~ docker run -d -p 9411:9411 --name zipkin --restart unless-stopped openzipkin/zipkin

Da's alles. Tenzij je nu expliciet je zipkin-container stopt, wordt deze na een herstart weer opnieuw opgestart. Tadaa!

Als je eenmaal je container naar je zin hebt geconfigureerd en je wilt niet iedere keer een nieuwe container starten, maar de vorige herstarten, dan gebruik je niet rm en run, maar alleen stop en start:

➜  ~ docker stop zipkin
➜  ~ docker start zipkin

Dit behoudt je container, met alle data die daarin intussen wellicht is opgeslagen (en die niet in een apart volume staat).

Resumerend

  • Met docker run start je een container op aan de hand van een bestaand image.
  • Je kunt daarbij opties meegeven om je container een unieke, herkenbare naam te geven, om netwerkpoorten ernaartoe open te zetten en om volumes te koppelen voor permanentere opslag van data.
  • Je kunt ook containers automatisch bij het opstarten van Docker opnieuw laten starten
  • Met docker run start je iedere keer een nieuwe, unieke container op. Met docker stop stop je die, en met docker start start je diezelfde container weer opnieuw op.
  • Met docker rm verwijder je een container die je niet meer nodig hebt.
  • Met docker ps kun je alle draaiende containers op je systeem zien.

Voor nu wilde ik het daarbij laten - het verhaal wordt al zo lang genoeg. Binnenkort ga ik verder met docker-compose en andere methodes.

Vragen, opmerkingen of aanvullingen zijn welkom!