Docker provides an easy way to set up a LEMP or LAMP stack for local PHP web development using containers. In this comprehensive guide, I‘ll walk you through best practices for installing Docker and Docker Compose on Ubuntu 20.04 and using them to create a LAMP server from scratch.

Why Docker?

Before jumping into the technical details, let‘s discuss why Docker is worth learning and using for local development:

  • Portability – Docker containers can run unchanged on any OS or infrastructure. This prevents environment inconsistencies.

  • Isolation – Containers isolate dependencies and configs for each service. This improves organization for complex stacks.

  • Speed – Containers start almost instantly compared to VMs. Saving major development time.

  • Scalability -Docker sets up apps ready for horizontal scaling out to multiple hosts.

And as evidence of these benefits, Docker adoption has rapidly grown:

  • Over 5 million Docker hosts active globally
  • 67% of companies running Docker in production
  • Container market size to reach $17 billion by 2027

With powerful capabilities like these, it‘s no wonder Docker is a must-learn skill for devops engineers and full-stack developers.

Now let‘s get hands-on with building Docker services for PHP…

Step 0 – Project Structure Best Practices

Before installing and running services, let‘s discus how to structure Docker projects:

project-name
├── docker-compose.yml
├── Dockerfile
└── app
    ├── src
    └── config

Some guidelines to follow:

  • Have explicit Dockerfiles for custom images
  • Abstract app code into a subdirectory
  • Separate app config from source code
  • .dockerignore unneeded directories in builds

This setup improves organizing, reuse, and encapsulation of Docker apps.

Now we‘ll follow these best practices for our LAMP project…

Step 1 – Install Docker Engine

Docker is now included in Ubuntu‘s standard repositories, making installation easy.

First, update your package index:

sudo apt update

Then install Docker:

sudo apt install docker.io

After the installation completes, verify Docker is running:

sudo systemctl status docker

You should see active (running) in green text indicating the service is running properly.

Finally, add your user to the docker group to avoid using sudo for every docker command:

sudo usermod -aG docker ${USER} 

Log out and back in for the changes to take effect.

You can now run docker commands without sudo. Verify this worked by running:

docker version

And you should see output without needing sudo.

Step 2 – Install Docker Compose

While Docker handles containers, Docker Compose handles multi-container applications and environments. So we‘ll need it to coordinate our LAMP containers.

Start by installing the dependencies needed for compiling python packages:

sudo apt install python3-pip libffi-dev python3-dev build-essential

Next install Docker Compose from pip, the Python package manager:

sudo pip3 install docker-compose

Check that Docker Compose is installed correctly:

docker-compose --version

Take note of the version number installed for your records.

Step 3 – Define Project Layout

Now we‘ll set up directories and files based on best practices:

lamp-project
├── docker-compose.yml
├── Dockerfile
└── app
    ├── src
    └── config

First, make the project root and app dirs:

mkdir -p lamp-project/app/{src,config} 

src will store our PHP code later.

config will store application config files.

This keeps our web app code separate from Docker config for clean organization.

Step 4 – Create Docker Compose File

Here we‘ll create a docker-compose.yml file that defines our LAMP stack containers and settings:

nano docker-compose.yml

Then define services:

version: ‘3‘

services:

  apache:
    image: httpd:2.4
    container_name: apache 
    ports:
      - "8080:80"  
    volumes:
      - ./app/src:/var/www/html
    depends_on:
      - php
      - db

  php: 
    build: ./Dockerfile  
    container_name: php
    volumes:
      - ./app/src:/var/www/html

  db:
    image: mysql:8.0 
    container_name: db 
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: examplepassword  
      MYSQL_DATABASE: exampledb
    volumes:   
      - dbdata:/var/lib/mysql

volumes:
  dbdata:

Let‘s analyze what this Compose file does:

  • Uses official images from Docker Hub for Apache and MySQL
  • The PHP service builds from a custom Dockerfile
  • Containers are named for easy identification
  • Ports are exposed for web traffic (Apache) and database traffic (MySQL)
  • Volumes mount to sync our host code into the containers
  • MySQL credentials and database name are defined

This gives us a good starting point to now extend and customize!

Step 5 – Create PHP Dockerfile

The docker-compose.yml file points to a local Dockerfile to build a custom PHP image.

Create this now:

nano Dockerfile

Add the following, which installs extensions we need:

FROM php:7.4-apache  

RUN docker-php-ext-install pdo pdo_mysql mysqli

Let‘s explain the purpose of each line:

  • Base off the official 7.4-apache image
  • Enable pdo, pdo_mysql for MySQL connectivity from PHP
  • Also enable the mysqli driver for redundancy

This gives our PHP container crucial database access.

Step 6 – Configure Environment Variables

Hard-coding credentials and settings directly in config files is an anti-pattern.

A better approach is using environment variables.

First, let‘s create an env file to store variables:

nano app/config/.env  

Add the following contents:

MYSQL_ROOT_PASSWORD=strongpassword 
MYSQL_DATABASE=mydatabase

Then reference these variables in docker-compose.yml:

db:
  image: mysql:8.0
  environment:
    MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}  
    MYSQL_DATABASE: ${MYSQL_DATABASE} 

Now credentials are abstracted out of the compose file, adding security.

The .env file offers easy central storage for any other variables needed.

Step 7 – Networking: Attached vs Bridged

There are two main networking models in Docker:

Attached: Containers share the host‘s network stack. Simpler setup.

Bridged: Containers sit on their own subnet(s). More isolation.

The choice depends on app requirements. Our LAMP stack works fine sharing the host‘s networking.

Verify this is enabled in Docker settings:

/etc/docker/daemon.json 

With contents:

{
  "default-address-pools": [  
    {    
      "base":"172.17.0.0/12",
      "size": 16
    }
  ] 
}

Restart Docker after updating daemon settings.

Attached networking will now suffice for our needs.

Step 8 – Launch Containers

Our configuration is complete. Let‘s start the LAMP stack:

docker-compose up -d

This launches and links the defined containers detached.

Verify they are running:

docker-compose ps

You should see:

   Name                  Command               State                 Ports
-----------------------------------------------------------------------------------------
apache          /usr/sbin/httpd -DFOREGROUND    Up      0.0.0.0:8080->80/tcp
db              docker-entrypoint.sh --def ...   Up      0.0.0.0:3306->3306/tcp   
php             docker-php-entrypoint php-fpm   Up      9000/tcp

If any container fails to start, run:

docker-compose logs 

And inspect the output for errors.

Step 9 – Create PHP Script

With containers operational, let‘s create a test script.

In app/src, make a PHP file:

// test.php

<?php
$host = ‘db‘; 
$user = ‘root‘;
$pass = getenv(‘MYSQL_ROOT_PASSWORD‘);
$db = getenv(‘MYSQL_DATABASE‘);

$conn = new mysqli($host, $user, $pass, $db);  

if ($conn->connect_error) {
    die("Connection failed");
}

echo "Connected successfully";  
?>

This connects to our MySQL container using credentials defined in .env.

If it prints Connected successfully – our LAMP app works!

Step 10 – Load Balancing for Scaling

Our project now has a functioning local LAMP stack. But in production, a single PHP/Apache instance creates scaling issues under high traffic.

The Dockerized architecture sets us up nicely to run replicated containers across multiple hosts.

Then a load balancer divides requests between them:

Adding load balancing to scale out PHP/Apache containers

As traffic grows, we continue adding container replicas behind the load balancer to handle more demand. No modifications required to application code!

For even bigger scale, we could combine Docker with Kubernetes clusters and orchestration. But load balancing provides an easy first step towards scalability behind a production domain.

Next Steps

There are lots of directions you could take this basic LAMP stack setup:

  • Add containers like phpMyAdmin, Redis, ElasticSearch
  • Try out alternate web servers like Nginx
  • Create custom Dockerfiles for extended MySQL configuration
  • Rebuild images using Debian, CentOS base distros
  • Add code and deploy a PHP app like Laravel
  • Implement CI/CD pipelines to automate testing and deploy updates
  • Enhance logging, monitoring and security

I encourage you to utilize this Docker LAMP foundation for your own PHP projects. Please leave any questions below!

Similar Posts