Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Deploy PHP Apps with Docker Compose - The Essentials

Deploy PHP Apps with Docker Compose - The Essentials

Learn the six essentials steps to deploying applications with Docker Compose.

Are you tired of hearing how "simple" it is to deploy apps with Docker Compose, because your experience is more one of frustration? Have you read countless blog posts and forum threads that promised to teach you how to deploy apps with Docker Compose, only for one or more essential steps to be missing, outdated, or broken?

Some years back, I was in exactly this position. In my quest to deploy apps that I'd developed locally with Docker Compose, I’d read the documentation along with countless blog posts, yet was left more frustrated than anything else.

So, I decided to sit down and figure out the simplest approach to deploying apps with Docker Compose and then to document my findings. This talk is the result of what I learned.

If the slides take your interest, you can download a copy of the book that I based the talk on, called Deploy With Docker Compose, from https://deploywithdockercompose.com.

Web Dev with Matt

November 19, 2021
Tweet

Other Decks in Programming

Transcript

  1. Nice to meet you • @settermjd • @webdevwithmatt • Coding

    since 1996 • Mostly web-based apps • Once wrote VB/Access • Used Docker since 2015 I’m Matthew
  2. How to Deploy PHP Apps Using Docker Compose Picture: Docker

    Compose logo courtesy of Unixmen / https: // www.unixmen.com/container-docker-compose-ubuntu-16-04/
  3. What Will You Learn About Docker? 1 2 Build a

    local development environment Build a production environment from the development version 3 Build and tag self-contained Docker images
  4. What Will You Learn About Docker? 4 5 6 Push

    the Docker images to a Docker image registry Test the production deployment Deploy to production
  5. source: https://docs.docker.com/get-started/overview/#docker-architecture “ Docker uses a client-server architecture. The Docker

    client talks to the Docker daemon, which does the heavy lifting of building, running, and distributing your Docker containers. ” The Docker Architecture
  6. Can Be Easier Than... Picture: Docker Swarm logo courtesy of

    Sum Global / https: // sumglobal.com/wp-content/uploads/2017/07/docker-swarm-logo.png Picture: Kubernetes logo courtesy of wikimedia.org / https: // commons.wikimedia.org/wiki/File:Kubernetes_logo.svg
  7. Why Deploy With Docker? • Can be a simpler •

    Use unified commands • Deploy to production like development • Share setups • Integrate with CI/CD tools
  8. What Do You Need? • A broad familiarity with Docker

    • Docker Engine on Linux • Docker Desktop on macOS and Microsoft Windows • Docker Compose V2 • A Docker image registry • A deployment server running Docker
  9. docker ├── nginx │ ├── default.conf │ └── Dockerfile ├──

    php │ └── Dockerfile ├── database │ ├── Dockerfile │ └── dump.sql ├── docker-compose.prod.yml ├── docker-compose.dev.yml 
 └── docker-compose.yml The file & directory structure
  10. The file & directory structure Sub-divided by service docker ├──

    nginx │ ├── default.conf │ └── Dockerfile ├── php │ └── Dockerfile ├── database │ ├── Dockerfile │ └── dump.sql ├── docker-compose.prod.yml ├── docker-compose.dev.yml 
 └── docker-compose.yml
  11. The file & directory structure A Dockerfiles and supporting files

    docker ├── nginx │ ├── default.conf │ └── Dockerfile ├── php │ └── Dockerfile ├── database │ ├── Dockerfile │ └── dump.sql ├── docker-compose.prod.yml ├── docker-compose.dev.yml 
 └── docker-compose.yml
  12. The file & directory structure A base Docker Compose file

    docker ├── nginx │ ├── default.conf │ └── Dockerfile ├── php │ └── Dockerfile ├── database │ ├── Dockerfile │ └── dump.sql ├── docker-compose.dev.yml ├── docker-compose.prod.yml └── docker-compose.yml
  13. The file & directory structure Environment-specific Docker Compose files docker

    ├── nginx │ ├── default.conf │ └── Dockerfile ├── php │ └── Dockerfile ├── database │ ├── Dockerfile │ └── dump.sql ├── docker-compose.prod.yml ├── docker-compose.dev.yml 
 └── docker-compose.yml
  14. FROM php:8.1.1-fpm-alpine3.15 as base RUN docker-php-ext-install pdo_mysql RUN if [

    "$ENV" = "development" ]; then \ RUN docker-php-ext-install pecl install xdebug \ && docker-php-ext-enable xdebug; \ fi The PHP Dockerfile
  15. “With multi-stage builds, you use multiple FROM statements in your

    Dockerfile.” source: https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
  16. The PHP Dockerfile FROM php:8.1.1-fpm-alpine3.15 as base RUN docker-php-ext-install pdo_mysql

    RUN if [ "$ENV" = "development" ]; then \ RUN docker-php-ext-install pecl install xdebug \ && docker-php-ext-enable xdebug; \ fi
  17. The PHP Dockerfile FROM php:8.1.1-fpm-alpine3.15 as base RUN docker-php-ext-install pdo_mysql

    RUN if [ "$ENV" = "development" ]; then \ RUN docker-php-ext-install pecl install xdebug \ && docker-php-ext-enable xdebug; \ fi
  18. The PHP Dockerfile FROM php:8.1.1-fpm-alpine3.15 as base RUN docker-php-ext-install pdo_mysql

    RUN if [ "$ENV" = "development" ]; then \ RUN docker-php-ext-install pecl install xdebug \ && docker-php-ext-enable xdebug; \ fi
  19. FROM php:8.1.1-fpm-alpine3.15 as composer RUN apk add - - update

    git RUN curl -- silent - - fail -- location -- retry 3 -- output /tmp/installer.php -- url https: / / raw.githubusercontent.com/composer/getcomposer.org/cb19f2aa3aeaa2006c0cd69a7ef011eb31463067/web/ installer \ && php -r " \ \$signature = '48e3236262b34d30969dca3c37281b3b4bbe3221bda826ac6a9a62d6444cdb0dcd0615698a5cbe587c3f0fe57a54d8f5'; \ \$hash = hash('sha384', file_get_contents('/tmp/installer.php')); \ if (!hash_equals(\$signature, \$hash)) { \ unlink('/tmp/installer.php'); \ echo 'Integrity check failed, installer is either corrupt or worse.' . PHP_EOL; \ exit(1); \ }" \ && php /tmp/installer.php -- no-ansi - - install-dir=/usr/bin -- filename=composer \ && composer -- ansi -- version -- no-interaction \ && rm -f /tmp/installer.php The PHP Dockerfile
  20. FROM php:8.1.1-fpm-alpine3.15 as composer RUN apk add - - update

    git RUN curl -- silent - - fail -- location -- retry 3 -- output /tmp/installer.php -- url https: / / raw.githubusercontent.com/composer/getcomposer.org/cb19f2aa3aeaa2006c0cd69a7ef011eb31463067/web/ installer \ && php -r " \ \$signature = '48e3236262b34d30969dca3c37281b3b4bbe3221bda826ac6a9a62d6444cdb0dcd0615698a5cbe587c3f0fe57a54d8f5'; \ \$hash = hash('sha384', file_get_contents('/tmp/installer.php')); \ if (!hash_equals(\$signature, \$hash)) { \ unlink('/tmp/installer.php'); \ echo 'Integrity check failed, installer is either corrupt or worse.' . PHP_EOL; \ exit(1); \ }" \ && php /tmp/installer.php -- no-ansi - - install-dir=/usr/bin -- filename=composer \ && composer -- ansi -- version -- no-interaction \ && rm -f /tmp/installer.php The PHP Dockerfile
  21. FROM php:8.1.1-fpm-alpine3.15 as composer RUN apk add - - update

    git RUN curl -- silent - - fail -- location -- retry 3 -- output /tmp/installer.php -- url https: / / raw.githubusercontent.com/composer/getcomposer.org/cb19f2aa3aeaa2006c0cd69a7ef011eb31463067/web/ installer \ && php -r " \ \$signature = '48e3236262b34d30969dca3c37281b3b4bbe3221bda826ac6a9a62d6444cdb0dcd0615698a5cbe587c3f0fe57a54d8f5'; \ \$hash = hash('sha384', file_get_contents('/tmp/installer.php')); \ if (!hash_equals(\$signature, \$hash)) { \ unlink('/tmp/installer.php'); \ echo 'Integrity check failed, installer is either corrupt or worse.' . PHP_EOL; \ exit(1); \ }" \ && php /tmp/installer.php -- no-ansi - - install-dir=/usr/bin -- filename=composer \ && composer -- ansi -- version -- no-interaction \ && rm -f /tmp/installer.php The PHP Dockerfile
  22. FROM php:8.1.1-fpm-alpine3.15 as dependencies WORKDIR /var/www/html COPY . . COPY

    -- from=composer /usr/bin/composer /usr/bin/ RUN /usr/bin/composer install \ -- no-dev - - no-ansi -- no-plugins -- no-progress -- no-scripts \ -- classmap-authoritative -- no-interaction \ -- quiet The PHP Dockerfile
  23. FROM php:8.1.1-fpm-alpine3.15 as dependencies WORKDIR /var/www/html COPY . . COPY

    -- from=composer /usr/bin/composer /usr/bin/ RUN /usr/bin/composer install \ -- no-dev - - no-ansi -- no-plugins -- no-progress -- no-scripts \ -- classmap-authoritative -- no-interaction \ -- quiet The PHP Dockerfile
  24. FROM php:8.1.1-fpm-alpine3.15 as dependencies WORKDIR /var/www/html COPY . . COPY

    -- from=composer /usr/bin/composer /usr/bin/ RUN /usr/bin/composer install \ -- no-dev - - no-ansi -- no-plugins -- no-progress -- no-scripts \ -- classmap-authoritative -- no-interaction \ -- quiet The PHP Dockerfile
  25. FROM php:8.1.1-fpm-alpine3.15 as dependencies WORKDIR /var/www/html COPY . . COPY

    -- from=composer /usr/bin/composer /usr/bin/ RUN /usr/bin/composer install \ -- no-dev - - no-ansi -- no-plugins -- no-progress -- no-scripts \ -- classmap-authoritative -- no-interaction \ -- quiet The PHP Dockerfile
  26. I 😍 Alpine Linux Picture: Alpine Linux logo courtesy of

    techcentral.ie / http: // www.techcentral.ie/wp-content/uploads/2017/02/Alpine_Linux_logo_web.jpg
  27. • Smaller than other distros • Around 5MB in size

    • Reduces hosting costs • Reduces transfer costs • Reduces deployment time • Is a Docker best practice I 😍 Alpine Linux Picture: Alpine Linux logo courtesy of techcentral.ie / http: // www.techcentral.ie/wp-content/uploads/2017/02/Alpine_Linux_logo_web.jpg
  28. DROP TABLE IF EXISTS records; CREATE TABLE records ( id

    int unsigned NOT NULL, first_name varchar(200) NOT NULL, last_name varchar(200) NOT NULL, email_address varchar(255) NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; dump.sql
  29. LOCK TABLES records WRITE; INSERT INTO records VALUES (2,'Steve','McGarrett','[email protected]'), (3,'Danny','Williams','[email protected]'),

    (4,'Kamekona','','[email protected]'),(5,'Chin Ho','Kelly','[email protected]'), (6,'Kono','Kalakaua','[email protected]'), (7,'Lou','Grover','[email protected]'), (8,'Duke','Lukela','[email protected]'), (9,'Max','Bergman','[email protected]'), (10,'Jerry','Ortega','[email protected]'), (11,'Adam','Noshimuri','[email protected]'); UNLOCK TABLES; dump.sql
  30. services: nginx: restart: unless-stopped depends_on: - php healthcheck: test: ["CMD",

    "curl", "-f", "http: // localhost:8000/api/ping"] interval: 60s timeout: 3s retries: 3 The NGINX Service Configuration
  31. services: nginx: restart: unless-stopped depends_on: - php healthcheck: test: ["CMD",

    "curl", "-f", "http: // localhost:8000/api/ping"] interval: 60s timeout: 3s retries: 3 The NGINX Service Configuration
  32. The NGINX Service Configuration services: nginx: restart: unless-stopped depends_on: -

    php healthcheck: test: ["CMD", "curl", "-f", "http: // localhost:8000/api/ping"] interval: 60s timeout: 3s retries: 3
  33. The restart Directive’s Options Setting Description no Docker does not

    restart a container for any reason. always Docker will always restart a container if it stops. If it is manually stopped, it is restarted only when Docker daemon restarts or the container itself is manually restarted. on-failure Docker will restart a container if the exit code indicates an on-failure error. unless-stopped Docker will always restart a container — except when the container is stopped (manually or otherwise).
  34. The restart Directive’s Options Setting Description no Docker does not

    restart a container for any reason. always Docker will always restart a container if it stops. If it is manually stopped, it is restarted only when Docker daemon restarts or the container itself is manually restarted. on-failure Docker will restart a container if the exit code indicates an on-failure error. unless-stopped Docker will always restart a container — except when the container is stopped (manually or otherwise).
  35. The restart Directive’s Options Setting Description no Docker does not

    restart a container for any reason. always Docker will always restart a container if it stops. If it is manually stopped, it is restarted only when Docker daemon restarts or the container itself is manually restarted. on-failure Docker will restart a container if the exit code indicates an on-failure error. unless-stopped Docker will always restart a container — except when the container is stopped (manually or otherwise).
  36. The restart Directive’s Options Setting Description no Docker does not

    restart a container for any reason. always Docker will always restart a container if it stops. If it is manually stopped, it is restarted only when Docker daemon restarts or the container itself is manually restarted. on-failure Docker will restart a container if the exit code indicates an on-failure error. unless-stopped Docker will always restart a container — except when the container is stopped (manually or otherwise).
  37. The restart Directive’s Options Setting Description no Docker does not

    restart a container for any reason. always Docker will always restart a container if it stops. If it is manually stopped, it is restarted only when Docker daemon restarts or the container itself is manually restarted. on-failure Docker will restart a container if the exit code indicates an on-failure error. unless-stopped Docker will always restart a container — except when the container is stopped (manually or otherwise).
  38. services: nginx: restart: unless-stopped depends_on: - php healthcheck: test: ["CMD",

    "curl", "-f", "http: // localhost:8000/api/ping"] interval: 60s timeout: 3s retries: 3 The NGINX Service Configuration
  39. services: nginx: restart: unless-stopped depends_on: - php healthcheck: test: ["CMD",

    "curl", "-f", "http: // localhost:8000/api/ping"] interval: 60s timeout: 3s retries: 3 The NGINX Service Configuration
  40. server { # ... other configuration options location / {

    try_files $uri $uri/ /index.php; } location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass php:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param APPLICATION_ENV development; fastcgi_intercept_errors off; fastcgi_buffer_size 16k; fastcgi_buffers 4 16k; } }
  41. The PHP Service Configuration php: build: context: ./ dockerfile: docker/php/Dockerfile

    target: base args: ENV: development volumes: - .:/var/www/html
  42. The PHP Service Configuration php: build: context: ./ dockerfile: docker/php/Dockerfile

    target: base args: ENV: development volumes: - .:/var/www/html
  43. The PHP Service Configuration php: build: context: ./ dockerfile: docker/php/Dockerfile

    target: base args: ENV: development volumes: - .:/var/www/html
  44. docker compose \ -- file docker-compose.yml \ 
 -- file

    docker-compose.dev.yml \ up -- build Deploy the Application Locally
  45. [+] Running 4/4 ⠿ Network docker-essentials-test-build_default Created 0.1s ⠿ Container

    docker-essentials-test-build_php_1 Created 0.2s ⠿ Container docker-essentials-test-build_nginx_1 Created 0.3s ⠿ Container docker-essentials-test-build_database_1 Created 1.2s Attaching to database_1, nginx_1, php_1 php_1 | [18-Nov-2021 13:19:29] NOTICE: fpm is running, pid 1 php_1 | [18-Nov-2021 13:19:29] NOTICE: ready to handle connections nginx_1 | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/ nginx_1 | 2021/11/18 13:19:30 [notice] 1#1: using the "epoll" event method nginx_1 | 2021/11/18 13:19:30 [notice] 1#1: nginx/1.21.3 nginx_1 | 2021/11/18 13:19:30 [notice] 1#1: built by gcc 8.3.0 (Debian 8.3.0-6) nginx_1 | 2021/11/18 13:19:30 [notice] 1#1: OS: Linux 5.10.47-linuxkit nginx_1 | 2021/11/18 13:19:30 [notice] 1#1: start worker processes nginx_1 | 2021/11/18 13:19:30 [notice] 1#1: start worker process 32 nginx_1 | 2021/11/18 13:19:30 [notice] 1#1: start worker process 33 Docker Compose Up Output
  46. docker compose \ 
 -- file docker-compose.yml \ 
 --

    file docker-compose.dev.yml \ up -d Deploy the Application Locally
  47. docker compose ps docker compose \ -- file docker-compose.yml \

    
 -- file docker-compose.dev.yml \ ps NAME COMMAND SERVICE STATUS PORTS docker-essentials_database_1 "docker-entrypoint.s…" database running 0.0.0.0:3306 -> 3306/tcp, 33060/tcp docker-essentials_nginx_1 "/docker-entrypoint.…" nginx running 0.0.0.0:8080 -> 80/tcp, docker-essentials_php_1 "docker-php-entrypoi…" php running 9000/tcp
  48. docker compose ps docker compose \ -- file docker-compose.yml \

    
 -- file docker-compose.dev.yml \ ps database NAME COMMAND SERVICE STATUS PORTS docker-essentials_database_1 "docker-entrypoint.s…" database running 0.0.0.0:3306 -> 3306/tcp, 33060/tcp
  49. docker compose logs docker compose \ -- file docker-compose.yml \

    
 -- file docker-compose.dev.yml \ logs -- follow database
  50. Quick Recap • Created an intuitive directory structure • Created

    Dockerfiles & supporting files for each service • Created a Docker Compose configuration • Deployed the application locally • Did basic testing and debugging
  51. FROM nginx:alpine WORKDIR /var/www/html COPY ./docker/nginx/default.prod.conf /etc/nginx/conf.d/default.conf COPY . .

    RUN apk add -- update nodejs npm \ && npm install RUN NODE_ENV=production npx tailwindcss \ -i src/css/styles.css \ -o public/css/styles.css \ -- minify The NGINX Production Dockerfile
  52. FROM nginx:alpine WORKDIR /var/www/html COPY ./docker/nginx/default.prod.conf /etc/nginx/conf.d/default.conf COPY . .

    RUN apk add -- update nodejs npm \ && npm install RUN NODE_ENV=production npx tailwindcss \ -i src/css/styles.css \ -o public/css/styles.css \ -- minify The NGINX Production Dockerfile
  53. FROM nginx:alpine WORKDIR /var/www/html COPY ./docker/nginx/default.prod.conf /etc/nginx/conf.d/default.conf COPY . .

    RUN apk add -- update nodejs npm \ && npm install RUN NODE_ENV=production npx tailwindcss \ -i src/css/styles.css \ -o public/css/styles.css \ -- minify The NGINX Production Dockerfile
  54. FROM nginx:alpine WORKDIR /var/www/html COPY ./docker/nginx/default.prod.conf /etc/nginx/conf.d/default.conf COPY . .

    RUN apk add -- update nodejs npm \ && npm install RUN NODE_ENV=production npx tailwindcss \ -i src/css/styles.css \ -o public/css/styles.css \ -- minify The NGINX Production Dockerfile
  55. FROM nginx:alpine WORKDIR /var/www/html COPY ./docker/nginx/default.prod.conf /etc/nginx/conf.d/default.conf COPY . .

    RUN apk add -- update nodejs npm \ && npm install RUN NODE_ENV=production npx tailwindcss \ -i src/css/styles.css \ -o public/css/styles.css \ -- minify The NGINX Production Dockerfile
  56. FROM nginx:alpine WORKDIR /var/www/html COPY ./docker/nginx/default.prod.conf /etc/nginx/conf.d/default.conf COPY . .

    RUN apk add -- update nodejs npm \ && npm install RUN NODE_ENV=production npx tailwindcss \ -i src/css/styles.css \ -o public/css/styles.css \ -- minify The NGINX Production Dockerfile
  57. docker build \ -- file ./docker/nginx/Dockerfile \ -- tag webdevwithmatt/webserver-alpine:latest

    . docker build \ -- file ./docker/php/Dockerfile \ -- tag webdevwithmatt/php-runtime-alpine:latest . docker build \ -- file ./docker/database/Dockerfile \ -- tag webdevwithmatt/database-alpine:latest . Build the Docker Images
  58. $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE webdevwithmatt/webserver-alpine

    latest 8c6f620e0709 9 seconds ago 432MB webdevwithmatt/php-runtime-alpine latest 6544148b5c30 9 seconds ago 495MB webdevwithmatt/database-alpine latest 6544148b5c30 9 seconds ago 495MB Check the Docker Images
  59. docker login - - username <username> -- password <password> docker

    push webdevwithmatt/webserver-alpine:latest docker push webdevwithmatt/php-runtime-alpine:latest docker push webdevwithmatt/database-alpine:latest Push the Docker Images
  60. Deploy to Production • Set DOCKER_HOST • Use -H or

    --host options with Docker commands • Use a Docker Context
  61. Deploy to Production DOCKER_HOST=ssh: / / webdevwithmatt@<prod host> docker compose

    up export DOCKER_HOST=ssh: // webdevwithmatt@<prod host> && \ docker compose up docker -H ssh: // webdevwithmatt@<prod host> compose up docker - - host ssh: / / webdevwithmatt@<prod host> compose up
  62. source: https://docs.docker.com/engine/context/working-with-contexts/ A single Docker CLI can have multiple contexts.

    Each context contains all of the endpoint and security information required to manage a different cluster or node. The docker context command makes it easy to configure these contexts and switch between them.
  63. Docker Contexts • Create a context • Set it as

    the default context • Run Docker commands
  64. Docker Contexts docker context create remote \ -docker "host=ssh: /

    / deploy@<your_digitalocean_droplets_ip>" docker context use remote
  65. docker context ls NAME TYPE DESCRIPTION DOCKER ENDPOINT default moby

    Current DOCKER_HOST based configuration unix: // / var/run/docker.sock remote * moby ssh: // webdevwithmatt@<prod host> Verify the Context
  66. Final Recap 1. Built the application image 2. Stored the

    image in an accessible registry 3. Set the Docker daemon to communicate with 4. Built a deployment configuration 5. Deployed the application to a remote host
  67. What About a Build Pipeline? Picture: Github Actions courtesy of

    dev.to / https: // res.cloudinary.com/practicaldev/image/fetch/s -- vBB9KCW4 -- /c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https: // dev-to-uploads.s3.amazonaws.com/i/twfkvzk83m0cpjwz26js.png 
 Picture: circleci logo courtesy of freelogovectors.net / https: // cdn.freelogovectors.net/wp-content/uploads/2020/11/circle-ci-logo.png Picture: TeamCity logo courtesy of freebiesupply.com / https: // cdn.freebiesupply.com/logos/large/2x/teamcity-icon-logo-png-transparent.png
  68. Want to Dive Deeper? • It goes into greater depth

    • It's packed with tips and tricks • It's free... • ...in return for your email address Get the book! https: / / deploywithdockercompose.com