Asmir Mustafic

(and a couple of other tools)

PHP CE 2019 - Dresden - Germany

Cancelled

Me

Asmir Mustafic

Me

@goetas

Berlin

Community

  • jms/serializer (maintainer)
  • masterminds/html5 (maintainer)
  • hautelook/templated-uri-bundle (maintainer)
  • goetas-webservices/xsd2php (author)
  • goetas-webservices/xsd-reader (author)
  • goetas-webservices/soap-client (author)
  • goetas/twital (author)

 

  • PHP-FIG secretary

The past

1998-2000

PHP 3, 4

Code

<?php

echo "Hello world";

Windows Notepad

C:\\Project\index.php

Dev environment

Apache + mod_php + MySQL

installed on laptop

Deploy

ftp> put index.php

Internet

Now

More $$$, more users, compute power, environments, tests, configurations....

Code

PHPStorm / Vi / Sublime
GIT / Mercurial
...

Dev environment

RAM RAM RAM RAM

Dev environment

Docker

Dockerfile

$ docker build ...

Docker image

Container

Build

Run

$ docker run ...

Docker

FROM debian:9.1

RUN apt install nginx -y

COPY nginx.conf /etc/nginx/

USER www-data

CMD ["nginx"]

Containerized Applications

Small

Self-contained

12 factor

Small

Self-contained

Dev === Production

Ideal workflow

BUILD

RELEASE

RUN

PUSH

RELEASE

Ideal workflow

Development

<?php

echo "Hello world";

public/index.php

PHP + NGINX

Remember

Docker is client server!

docker run ...
docker build ...
docker rm ...
docker ls ...
docker volume ...
docker images ...
...
dockerd

~REST API

Default LISTEN socket unix:///var/run/docker.sock

Remember

LISTEN tcp://1.2.3.4.5:2375
export DOCKER_HOST="tcp://1.2.3.4:2376"

docker run ...
docker build ...
docker rm ...
docker ls ...
docker volume ...
docker images ...
...
docker login
docker logout

Client only

docker-compose

Docker

Dockerfile

Docker image

Container

Build

Run

Dockerfile

Docker image

Container

Build

Run

Docker Compose

Dockerfile

Docker image

Container

Build

Run

Dockerfile

Docker image

Container

Build

Run


services:
  web:
    image: goetas/web
    command: nginx -f
    build:
      dockerfile: docker/nginx/Dockerfile

  php:
    image: goetas/php
    build: ./apps/docker/php

  cache:
    image: redis

docker-compose.yml

FROM php:7.1.6-fpm

# ...
php Dockerfile
FROM nginx:1.17.2

# ...
nginx Dockerfile
$ docker-compose build
$ docker-compose up

Ideal workflow

PUSH

Done!

Ideal workflow

Build responsibilities

  1. get dependencies

  2. run tests

  3. prepare images
    (and push images to the registry)

Build

The CI server runs the build

 

(running build means just running some bash commands)

Build 0/3

cp .ci.env .env
export TAG=${CI_GIT_SHA}
export BUILD_ID=${CI_BUILD_ID}

Variables

version: '3.4'
services:
  php:
    image: goetas/php:${TAG}
    build:
      context: .
      dockerfile: docker/php/Dockerfile
      args:
        BUILD_ID: ${BUILD_ID}
docker-compose.build.yml
version: '3.4'
services:
  php:
    image: goetas/php:d0be2dc421be4fcd0172e5afceea3970e2f3d940
    build:
      context: .
      dockerfile: docker/php/Dockerfile
      args:
        BUILD_ID: 878
git clone ...

Build 1/3

# build images
docker-compose build

# install composer dependencies
docker-compose run --no-deps --rm php \
         composer install \
         --no-progress --no-interaction \
         --no-scripts --no-plugins

# re-build images with vendor
docker-compose build

Dependencies

Successfully built d9dd746fcd75
Successfully tagged goetas/php:d0be2dc421be4f

Successfully built d9dd746fcd75
Successfully tagged goetas/web:d0be2dc421be4f

Build 2/3

# tests
docker-compose run --no-deps --rm php \
         vendor/bin/phpunit

Run tests

You have tests, right?

PHPUnit 8.3.0 by Sebastian Bergmann and contributors.

...

Time: 0 seconds, Memory: 3.25Mb

OK (3 tests, 4 assertions)

Build 3/3

# login to docker registry
echo Y | docker login -u "$USR" -p "$PASS"

# push images
docker-compose push 

Push images to registry

Pushing web (goetas/web)...
The push refers to a repository [docker.io/goetas/web]
latest: digest: sha256:b3dff3e82a0e12b54a10ed083b65fd size: 1786

Pushing php (goetas/php)...
The push refers to a repository [docker.io/goetas/php]
latest: digest: sha256:af07f1e4e3d1a311f35a92d5336dc2 size: 7836

Ideal workflow

BUILD

Done!

Ideal workflow

Docker Swarm

Docker

Dockerfile

Docker image

Container

Build

Run

Dockerfile

Docker image

Container

Build

Run

Docker Compose

Dockerfile

Docker image

Container

Build

Run

Dockerfile

Docker image

Container

Build

Run

Dockerfile

Docker image

Container

Build

Run

Docker Swarm

Dockerfile

Docker image

Container

Build

Run

Dockerfile

Docker image

Container

Build

Run

Docker Swarm

  • simple
  • part of docker
  • uses docker-compose syntax
  • 80% of use-cases

Docker Swarm

$ docker swarm init
Swarm initialized: current node (bvz81updecsj6wj) 
 is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join \
    --token SWMTKN-1-3pu6hszjas19xyp7ghgos \
    172.17.0.2:2377

To add a manager to this swarm, 
 run 'docker swarm join-token manager'
 and follow the instructions.

Create a cluster (and a manager node)

Docker Swarm

$ docker swarm join \
    --token SWMTKN-1-3pu6hszjas19xyp7ghgos \
    172.17.0.2:2377
This node joined a swarm as a manager.

Add a worker node to a cluster

Docker Swarm

$ docker node ls
ID              HOSTNAME  STATUS AVAILABILITY MANAGER STATUS
38ciaotwjuri    sw-wrk1   Ready  Active
e216jshn25ck *  sw-man1   Ready  Active       Leader

List nodes from a manager

Docker Swarm

TLS authentication

export DOCKER_HOST="tcp://1.2.3.4:2376"
export DOCKER_TLS_VERIFY="1"
export DOCKER_CERT_PATH="/etc/docker/server.pem"

$ docker node ls

Done?

Ideal workflow

RELEASE

RELEASE

RUN

Step 5. Deploy

docker stack deploy -c docker-stack.yml my_app
version: '3.4'
services:
  web:
    image: goetas/web

  php:
    image: goetas/php

  cache:
    image: redis

docker-stack.yml

Step 5. Deploy

export DOCKER_HOST="tcp://1.2.3.4:2375"

docker stack deploy -c docker-stack.yml my_app
export DOCKER_HOST="tcp://1.2.3.4:2375"

docker stack deploy -c docker-stack.yml my_app-staging

Multiple environments

Done!

Container tips

Application tips

Stateless

  • No data
  • No logs

Disposability

  • fast start
  • fast shutdown 

Communication

  • only port communication, no shared folders
  • no difference between 3rd party services and local services

Configurable

  • configurations must be not part of the image, but provided at run-time

 Concurrency

  • must allow multiple instances of the same process

Docker-Compose tips

COMPOSE_FILE=a.yml:b.yml:c.yml
docker-compose -f a.yml -f b.yml -f c.yml build

Multiple files

COMPOSE_FILE=a.yml:b.yml:c.yml docker-compose build
.env
docker-compose build
version: '3.4'
services:
  web:
    image: goetas/web:${TAG}
    build:
      context: .
      dockerfile: docker/nginx/Dockerfile

  php:
    image: goetas/php:${TAG}
    build:
      context: .
      dockerfile: docker/php/Dockerfile
      args:
        BUILD_ID: ${BUILD_ID}
docker-compose.build.yml

(only what is necessary for the build)

version: '3.4'
services:
  db:
    image: postgres:11.4

  cache:
    image: redis:5.0.5-alpine
docker-compose.yml

Shared configs for all developers

version: '3.4'
services:
  web:
    ports:
      - "8080:80" # 127.0.0.16:80:80

    volumes:
      - .:/app

  php:
    volumes:
      - .:/app

  db:
    volumes:
      - db_data:/var/lib/postgresql/data 

volumes:
  db_data: {}
docker-compose.dev.dist.yml

(copy into docker-compose.dev.yml, ignored by GIT)

Customizations for developers

version: '3.4'
services:
  php:
    environment:
      SECRET_API_KEY: ${TEST_API_KEY}
      SMPT_HOST: smtp
    volumes:
      - ./:/rd

  smtp:
    image: tophfr/mailcatcher
docker-compose.ci.yml

Customizations for CI and Tests

COMPOSE_FILE=
         docker-compose.build.yml: \
         docker-compose.yml: \
         docker-compose.dev.yml \
// Windows use ; 
// COMPOSE_FILE=
//         docker-compose.build.yml; \
//         docker-compose.yml; \
//         docker-compose.dev.yml \

TAG=dev
BUILD_ID=unknown
.dist.env

(copy into .env, ignored by GIT!)

COMPOSE_FILE=
         docker-compose.build.yml: \
         docker-compose.yml: \
         docker-compose.ci.yml
.ci.env

(copy into .env on CI)

FROM php:7.1.6-fpm

# ...
docker/php/Dockerfile
FROM nginx:1.17.2

# ...
docker/nginx/Dockerfile
cp .dist.env .env
cp docker-compose.dev.dist.yml docker-compose.dev.yml
docker-compose up

Build tips

Building images

node_modules/
var/
vendor/*
.git
.dockerignore
.gitignore
.gitlab-ci.yml
.php_cs.cache
docker-compose*
README.md

.dockerignore

Docker is client server

docker build .

Building images

FROM php:7.3.6-fpm

# ...

Exact versions

Building images

FROM php:7.3.6-fpm

RUN apt-get update
RUN apt-get install -y nginx

Repeatable builds

FROM php:7.3.6-fpm

RUN apt-get update && \
    apt-get install -y nginx

Building images

FROM php:7.3.6-fpm

RUN apt-get update \
    && apt-get install -y git

COPY index.php index.php
COPY src src

Cache friendly and fast

FROM php:7.3.6-fpm

COPY src src
COPY index.php index.php

RUN apt-get update \
    && apt-get install -y git

Building images

Cache origin

version: '3.4'
services:
  php:
    image: goetas/php:${TAG}
    build:
      context: .
      dockerfile: docker/php/Dockerfile
      cache-from:
        - goetas/php:${TAG}
        - goetas/php:${CACHE_TAG}
IMAGES=$(
  docker-compose config 
    | python \
      -c 'import sys,yaml,json; json.dump(yaml.load(sys.stdin), sys.stdout)' 
    | jq -r  '([.services[].build.cache_from]|flatten) | del(.[]|nulls)[]'  
    | sort 
    | uniq
)

for i in $IMAGES; do docker pull "$i" || true; done

Download caches

Building images

FROM nginx:1.14.0

RUN apt update && \
    apt install vim -y

RUN rm -rf /var/cache/apt

Small images

FROM nginx:1.14.0

RUN apt update \
    && apt install vim -y \
    && rm -rf /var/cache/apt

Building images

Build arguments

version: '3.4'
services:
  php:
    image: goetas/php:${TAG}
    build:
      context: .
      dockerfile: docker/php/Dockerfile
      args:
        BUILD_ID: ${BUILD_ID}
FROM php:7.3.6-fpm

ARG BUILD_ID unknown

RUN echo "$BUILD_ID" > config/verson.txt

Building images

FROM nginx:1.14.0

COPY docker/nginx/www.conf /www.conf
COPY docker/nginx/entrypoint.sh /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

Environment variables everywhere

#!/bin/bash -e

envsubst < /www.conf > /etc/nginx/nginx.conf

/entrypoint.sh

server {
    listen ${HTTP_PORT};
    root /var/www/app/public;
}

/www.conf

Swarm tips

Logs

STDOUT / STDERR

version: '3.4'
services:
  php:
    image: goetas/php:${TAG}

    logging:
      driver: gelf
      options:
        gelf-address: "udp://logging.myserver.com:12302"

none, local, json-file, syslogm journald, gelf, fluentd, awslogs, splunk, etwlogs, gcplogs, logentries

Global services

One container on each node of the cluster

version: '3.4'
services:
  php:
    image: goetas/php:${TAG}

    deploy: 
       mode: global

Scale services

version: '3.4'
services:
  php:
    image: goetas/php:${TAG}

    deploy: 
       replica: 5

Run 5 copies of the PHP container

Container placement

version: '3.4'
services:
  php:
    image: goetas/php:${TAG}

    deploy:
      placement:
        constraints:
          - engine.labels.data-center == eu-west-1
          - node.role == frontend

Decide on which node place the containers

$ docker node update --add-label "role=frontend" 2l9kajahy2

Resource usage

version: '3.4'
services:
  php:
    image: goetas/php:${TAG}

    deploy:
      resources:
        limits:
          cpus: '0.50'
          memory: '50M'
        reservations:
          cpus: '0.25'
          memory: '20M'

Limit/Reserve CPU or memory

Health checks

version: '3.4'
services:
  web:
    image: goetas/web:${TAG}

    healthcheck:
      test: curl -f -sS http://localhost/health
      interval: 20s
      timeout: 5s
      retries: 2
      start_period: 2s

Check container health

Restart on failure

Secrets

version: '3.4'
services:
  web:
    image: goetas/web:${TAG}
    secrets:
      - ssh_private_key
      - ssl_certificate

  ssh_private_key:
    file: ./my_key.rsa
  ssl_certificate:
    external: true

Forward secret credentials/configurations

/run/secrets/ssh_private_key and /run/secrets/ssl_certificate
crated inside the containers

$ docker secret create ssl_certificate my.crt

Update policy

version: '3.4'
services:
  web:
    image: goetas/web:${TAG}

    deploy:
      replicas: 6
      update_config:
        parallelism: 2
        delay: '10s'
        order: start-first
      rollback_config:
        parallelism: 2
        delay: '20s'
        order: start-first

Timing for container updates/rollback

Kubernetes?

Thank you!

Deploy your PHP app with Docker Swarm - PHPCE 2019

By Asmir Mustafic

Deploy your PHP app with Docker Swarm - PHPCE 2019

Setup your local environment, setup the remote server, push the code, done. Your application is ready and deployed. This talks is a walk-trough on how to setup a continuous integration and delivery pipeline to have your PHP code pushed right to production using modern CI tools and Docker Swarm.

  • 210
Loading comments...

More from Asmir Mustafic