This is my first article on this platform, and the goal here is to keep things simple, practical, and easy to follow.
You've probably had trouble installing PHP extensions, configuring Nginx, or installing a database on your local machine, just like me; Docker can make your life much easier. In this guide, we will build a modern and simple development environment using:
- PHP 8.4 (FPM)
- Composer
- Nginx
- MariaDB
- Docker and Docker Compose
All services will run together with a single command.
Project structure
Start by creating the following folder structure:
root-folder/
├── docker/
│ ├── nginx/
│ │ └── default.conf
│ └── php/
│ └── Dockerfile
├── src/
│ └── index.php
├── docker-compose.yml
└── composer.json
Each folder has a clear responsibility:
-
docker/php: PHP image and extensions -
docker/nginx: Nginx configuration -
src: application source code
PHP 8.4 with Composer (Dockerfile)
Create the file docker/php/Dockerfile:
# Base image with PHP 8.4 and FPM
FROM php:8.4-fpm
# Install system dependencies and PHP extensions
RUN apt-get update && apt-get install -y \
git \
unzip \
libzip-dev \
&& docker-php-ext-install pdo pdo_mysql zip
# Install Composer from the official image
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Set the working directory
WORKDIR /var/www/html
What this Dockerfile does
- Uses PHP 8.4 FPM, ideal for Nginx
- Installs common system tools (
git,unzip) - Enables essential PHP extensions for database and zip handling
- Adds Composer without manual installation
Simple Nginx configuration
Create the file docker/nginx/default.conf:
server {
listen 80;
index index.php index.html;
root /var/www/html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
What this configuration does
- Listens on port
80 - Serves files from
/var/www/html - Forwards
.phprequests to the PHP container via FastCGI
This setup is minimal but works well for APIs and simple PHP applications.
Docker Compose (the core of the setup)
Create the file docker-compose.yml in the project root:
version: '3.9'
services:
php:
build: ./docker/php
container_name: php84
volumes:
- ./src:/var/www/html
depends_on:
- mariadb
nginx:
image: nginx:latest
container_name: nginx
ports:
- "8080:80"
volumes:
- ./src:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- php
mariadb:
image: mariadb:11
container_name: mariadb
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: app
MYSQL_USER: app
MYSQL_PASSWORD: app
ports:
- "3306:3306"
volumes:
- mariadb_data:/var/lib/mysql
volumes:
mariadb_data:
What this docker-compose.yml does
This file defines and connects all services needed for the development environment.
version
version: '3.9'
Defines the Docker Compose file format version. Version 3.9 is stable and widely supported.
php service
php:
build: ./docker/php
container_name: php84
volumes:
- ./src:/var/www/html
depends_on:
- mariadb
- Builds a custom PHP image using the
Dockerfileinsidedocker/php - Mounts the local
srcfolder into the container so changes are reflected instantly - Depends on MariaDB, ensuring the database container starts first
nginx service
nginx:
image: nginx:latest
container_name: nginx
ports:
- "8080:80"
volumes:
- ./src:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- php
- Uses the official Nginx image
- Maps port
8080on your machine to port80inside the container - Shares the same application code with PHP
- Loads the custom Nginx configuration file
- Depends on the PHP container to handle
.phprequests
mariadb service
mariadb:
image: mariadb:11
container_name: mariadb
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: app
MYSQL_USER: app
MYSQL_PASSWORD: app
ports:
- "3306:3306"
volumes:
- mariadb_data:/var/lib/mysql
- Runs MariaDB version 11
- Automatically creates a database and user on startup
- Exposes port
3306for local connections (optional for development) - Stores database data in a named volume
Volumes
volumes:
mariadb_data:
This named volume persists database data even if containers are stopped or recreated.
Services overview
- php: runs PHP 8.4 with Composer
- nginx: handles HTTP requests
- mariadb: provides a persistent database
The mariadb_data volume ensures that database data is not lost when containers stop.
PHP test file (with database connection)
Create the file src/index.php:
<?php
$host = 'mariadb';
$db = 'app';
$user = 'app';
$pass = 'app';
$charset = 'utf8mb4';
$port = 3306;
$dsn = "mysql:host={$host};port={$port};dbname={$db};charset={$charset}";
try {
$pdo = new PDO(
$dsn,
$user,
$pass,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
echo "PHP is running and connected to MariaDB successfully!";
} catch (PDOException $e) {
echo "Database connection failed: {$e->getMessage()}";
}
What this test file does
- Uses PDO, the recommended way to connect to databases in PHP
- Connects to MariaDB using the service name
mariadbas the host - Reuses the same credentials defined in
docker-compose.yml - Prints a success message if the connection works
- Displays an error message if something goes wrong
If you see the success message in the browser, it means PHP, Nginx, and MariaDB are communicating correctly.
Composer configuration
Create the file composer.json:
{
"name": "example/docker-php",
"require": {}
}
To run Composer inside the PHP container:
docker compose run --rm php composer install
This command uses the same PHP environment defined in Docker, avoiding local PHP version issues.
Running the environment
From the project root, run:
docker compose up -d --build
Then open your browser:
If everything is working, you should see:
PHP is running and connected to MariaDB successfully!
Conclusion
With this setup you get:
- PHP 8.4 ready for development
- Composer fully integrated
- Nginx as a lightweight web server
- MariaDB with persistent data
- A clean and isolated development environment
This is a solid starting point for plain PHP projects and APIs.
That's all for today, folks!
Top comments (1)
Very good, I'll use it on my next project