Als developer loop je wel eens tegen complexe projecten aan die onderling weer dependencies met elkaar hebben, hoe zorg je er nou voor dat dit gestroomlijnd lokaal gaat werken? Een antwoord hierop is Docker Compose en in dit blog vertel ik je daar wat meer over.

Het krachtige concept van micro services neemt al een aantal jaar langzaam aan de industrie over. Grote monolitische services maken langzaam plaats voor zwermen van kleine autonome microservices die samenwerken. Het proces gaat gepaard met een andere markttrend: Containers. Samen helpen ze ons veerkrachtige systemen te bouwen.

Containers veranderen niet alleen de architectuur van systemen, maar ook de structuur van omgevingen die worden gebruikt om ze te creëren. Wanneer software nu in containers wordt gedistribueerd, hebben ontwikkelaars de volledige vrijheid om te beslissen welke applicaties ze nodig hebben. Als resultaat kunnen complexe omgevingen zoals een React app met een API en WebSockets binnen enkele seconden worden geïnstantieerd. Softwareontwikkeling wordt eenvoudiger en effectiever.

Deze veranderingen veroorzaken natuurlijk ook een nieuw probleem. Hoe kan ik als ontwikkelaar er voor zorgen dat mijn ontwikkelomgeving gelijk is aan de uiteindelijke productie omgeving?

Docker Compose introductie

Het antwoord op deze vraag is Docker Compose:
“Een tool voor het definiëren en uitvoeren van complexe applicaties met Docker. Met Compose definieer je een applicatie met meerdere containers in één enkel bestand, en start je jouw applicatie vervolgens op met één enkel commando die alles doet wat nodig is om het te laten draaien.” https://docs.docker.com/compose/

In dit blog gaat we kijken hoe we Docker Compose kunnen gebruiken om een lokale ontwikkelomgeving op te zetten.

Functionaliteit

De hoofdzakelijke functie van Docker Compose is het creëren van een microservice architectuur, dus de containers en de onderlinge relaties. Maar deze tool kan nog veel meer.

Alle functionaliteit van Docker Compose is beschikbaar via het docker-compose commando, welke lijkt op de functionaliteit van Docker:

build      Build or rebuild services
bundle     Generate a Docker bundle from the Compose file
config     Validate and view the Compose file
create     Create services
down       Stop and remove containers, networks, images, and volumes
events     Receive real time events from containers
exec       Execute a command in a running container
help       Get help on a command
images     List images
kill       Kill containers
logs       View output from containers
pause      Pause services
port       Print the public port for a port binding
ps         List containers
pull       Pull service images
push       Push service images
restart    Restart services
rm         Remove stopped containers
run        Run a one-off command
scale      Set number of containers for a service
start      Start services
stop       Stop services
top        Display the running processes
unpause    Unpause services
up         Create and start containers
version    Show the Docker-Compose version information

Ze lijken niet alleen op elkaar, maar gedragen zich als Docker tegenhangers. Het enige verschil is dat zij effect hebben op het gehele multi-container architectuur die gedefinieerd staat in het docker-compose.yml configuratie bestand en niet een enkele container.

Je zult misschien merken dat niet alle Docker commando’s in docker-compose aanwezig zijn, deze zijn namelijk niet logisch voor een multi-container setup.

Docker Compose workflow

Er zijn drie stappen om Docker Compose te gebruiken:
1. Definieer elke service in een Dockerfile.
2. Definieer de services en de relaties tot elkaar in het docker-compose.yml bestand.
3. Gebruik docker-compose up  om het systeem te starten.

Ik ga de workflow laten zien aan de hand van twee voorbeelden. In het eerste voorbeeld laat ik de basis structuur zien. En in het tweede voorbeeld gaan we ervoor zorgen dat die daadwerkelijk gedraaid kan worden.

Basis structuur

Als eerst gaan we een basis mappen structuur opzetten hoe ik het meestal doe voor complexere projecten, echter zijn er meerdere wegen die naar Rome leiden en dus kan je dit ook op vele andere manieren doen.

Project-map

├── apps             > In deze map komen alle sub-projecten
├── containers       > In deze map komen alle custom containers 
├── data             > In deze map komt bv. De mysql db te staan
├── logs             > In deze map komen de logs van bv. nginx
└── docker-compose.yml

Nu we de basis mappen structuur hebben kunnen we de subprojecten toevoegen, denk hierbij aan een API, OAuth, front-end enz. Als voorbeeld hebben een Laravel API en een React front-end met daartussen een WebSocket communicatie laag (Laravel Echo Server). In werkelijkheid zijn dit 9 van de 10 keer Git Submodules aangezien je dit qua versie beheer apart wil houden, maar hier gaan we voor nu niet op in.

Project-map

├── apps
│ ├── laravel-api
│ ├── lavavel-echo-server
│ └── react
├── containers	
├── data
├── logs
└── docker-compose.yml

Wanneer we dit hebben staan kunnen we er over nadenken welke containers de sub-projecten nodig hebben.

Laravel API
• Nginx
• PHP-FPM
• Redis (voor de communicatie naar Laravel Echo Server en als queue, session en cache driver)
• MySQL

Laravel Echo Server
• Node
• Redis

React front-end (dit is voornamelijk om productie builds te testen, niet voor development)
• Nginx

Als eerst gaan we in de containers map wat bestanden en mappen aanmaken voor containers die wat extra configuratie nodig hebben. Op de configuratie van specifieke containers gaan we nu niet in, aangezien dit op zichzelf al een blog verdient. Maar niet getreurd ik heb hier alle containers al geconfigureerd.

Project-map

├── apps
│ ├── laravel-api
│ ├── lavavel-echo-server
│ └── react
├── containers	
│ ├── mysql
│ │ ├── Dockerfile
│ │ ├── docker-mysql-init.sql
│ │ └── my.cnf
│ ├── nginx
│ │ ├── Dockerfile
│ │ ├── nginx.conf
│ │ └── sites
│ │   ├── api.conf
│ │   └── app.conf
│ └── php-fpm
│   ├── Dockerfile
│   └── debug.ini
├── data
├── logs
└── docker-compose.yml

Docker Compose

Het is nu eindelijk tijd dat we de docker-compose.yml file gaan configureren, in deze configuratie kunnen we per container aangeven wat deze container nodig heeft om te draaien.

version: "3.1"
services:
 ### MySQL Container #########################################

  mysql:
    container_name: mysql
    build:
      context: ./containers/mysql
      args:
        - MYSQL_VERSION=5.7
    environment:
      - MYSQL_DATABASE=app_db
      - MYSQL_USER=app_user
      - MYSQL_PASSWORD=secret
      - MYSQL_ROOT_PASSWORD=secret
    volumes:
      - ./data/mysql:/var/lib/mysql:cached
      - ./containers/mysql/docker-mysql-init.sql:/docker-entrypoint-initdb.d/dump.sql:cached
    ports:
      - 3306:3306
    networks:
      - backend

  ### Redis Container #########################################

  redis:
    image: redis
    container_name: redis
    volumes:
      - ./data/redis:/var/lib/redis:cached
    ports:
      - 6379:6379
    environment:
      - ALLOW_EMPTY_PASSWORD=yes
    networks:
      - backend

  ### PHP-FPM Container #######################################

  php-fpm:
    container_name: php-fpm
    build:
      context: ./containers/php-fpm
      args:
        - WITH_XDEBUG=true
    volumes:
      - ./apps/:/var/www:cached
      - ./containers/php-fpm/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini:cached
    expose:
      - 9000
    depends_on:
      - mysql
      - redis
    environment:
      - PHP_IDE_CONFIG="serverName=app"
    networks:
      - backend

  ### NGINX Server Container ##################################

  nginx:
    container_name: nginx
    build:
      context: ./containers/nginx
      args:
        - PHP_UPSTREAM_CONTAINER=php_fpm
        - PHP_UPSTREAM_PORT=9000
    volumes:
      - ./apps/:/var/www:cached
      - ./logs/nginx/:/var/log/nginx:cached
      - ./containers/nginx/sites/:/etc/nginx/sites-available:cached
    ports:
      - 80:80
      - 443:443
    depends_on:
      - php-fpm
    networks:
      - frontend
      - backend

  ### node Container ########################################

  node:
    image: node:alpine
    container_name: node
    volumes:
      - ./apps/laravel-echo-server:/app:cached
    working_dir: /app
    ports:
      - 4000:4000
    restart: always
    command: sh -c "npm start"
    networks:
      - frontend
      - backend

### Networks Setup ##########################################

networks:
  frontend:
    driver: "bridge"
  backend:
    driver: "bridge"

Het enige wat we nu nog zouden moeten doen is docker-compose up draaien. Natuurlijk zal dit nu niks doen aangezien er nog geen applicatie is, maar aan de hand van dit voorbeeld zou je zonder veel problemen nu wel je eigen configuratie moeten kunnen maken. Dus ik daag jullie bij deze uit om een bestaande applicatie die je nu met bijvoorbeeld met MAMP draait, nu met Docker Compose te gaan draaien. Succes!!

Mocht je vragen hebben of kom je er niet uit? Mail me dan even!

Geschreven door: Nick Verschoor

Meer kennis bijspijkeren? Kom dan naar onze Meetup: Ode aan de Code!

Bekijk onze Meetups

Wij zijn altijd op zoek naar getalenteerde vakgenoten!

Bekijk onze vacatures