NIXX/DEVv1.14.0
ArticlesFavorites
Sign In
Sign In
Articles

Welcome to our blog

A curated collection of insightful articles, practical guides, and expert tips designed to simplify your workflow

Cover image for: Dockerizing Your PHP Application for Local Development
October 18, 20255 MIN READ min readBy ℵi✗✗

Dockerizing Your PHP Application for Local Development

Learn how to dockerize your PHP application for local development. Set up PHP, MySQL, and Nginx in Docker for a smooth, consistent workflow.

phpwebdevdocker
ℵi✗✗

ℵi✗✗

Full-Stack Developer

Passionate about building tools and sharing knowledge with the developer community.

Was this helpful?

Popular Posts

  • NixOS vs. Arch Linux: Which One Belongs in Your Dev Setup?

    NixOS vs. Arch Linux: Which One Belongs in Your Dev Setup?

    5 MIN READ min read

  • How to Enable HTTPS on Localhost in Under 2 Minutes

    How to Enable HTTPS on Localhost in Under 2 Minutes

    3 MIN READ min read

  • Migrating from Create React App (CRA) to Vite: A Step-by-Step Guide

    Migrating from Create React App (CRA) to Vite: A Step-by-Step Guide

    4 MIN READ min read

  • Array Destructuring in PHP: A Practical Guide for Modern Developers

    Array Destructuring in PHP: A Practical Guide for Modern Developers

    5 MIN READ min read

Recommended Products

  • Apple iPad (7th Gen)

    Apple iPad (7th Gen)

    4.3
  • Fitbit Versa 4

    Fitbit Versa 4

    4.3
  • JBL Flip 6

    JBL Flip 6

    4.8
  • Dell 24 Monitor — SE2425HM Full HD

    Dell 24 Monitor — SE2425HM Full HD

    4.7

May contain affiliate links

Topics

webdev33productivity16cybersecurity12javascript11automation9guide8react7typescript7php6tutorial6freelancing5github actions5privacy5how to4Node.js4
+111 more topics →
🇺🇸USD ACCOUNTOpen a free US-based USD accountReceive & save in USD — powered by ClevaSponsoredInterserver Hosting#1 VALUEAffordable, reliable hosting from $2.50/mo99.9% uptimeSponsored

A PHP-FPM and Nginx setup separates web serving from PHP execution, which more closely mirrors how most production PHP applications are deployed than the Apache-with-mod-php approach. Nginx handles static files and proxies dynamic requests to PHP-FPM over FastCGI. In Docker, each concern runs in its own container: Nginx, PHP-FPM, and MySQL.

This guide builds that setup with Docker Compose, covering the Dockerfile, Nginx configuration, Compose file, environment variable handling, and database connectivity.

What this covers:

  • Project structure for a PHP-FPM Nginx setup

  • Writing the PHP-FPM Dockerfile

  • Nginx FastCGI configuration

  • Docker Compose for PHP, Nginx, and MySQL

  • Connecting PHP to MySQL using the service name as host

  • Environment variables with a .env file

  • Useful commands for managing the environment


Step 1: Verify Docker Is Installed

docker --version
docker compose version

Install Docker Desktop from docker.com if either command fails. Docker Desktop includes both the daemon and the Compose plugin.


Step 2: Set Up the Project Structure

mkdir php-docker-app && cd php-docker-app

Create the following structure:

php-docker-app/
├── src/
│   └── index.php
├── Dockerfile
├── docker-compose.yml
├── nginx.conf
└── .env

Add a test file to confirm the setup works:

<?php
// src/index.php
phpinfo();

Step 3: Write the Dockerfile for PHP-FPM

FROM php:8.2-fpm

# Install extensions for MySQL access
RUN docker-php-ext-install pdo pdo_mysql mysqli

# Set working directory
WORKDIR /var/www/html

# Copy application files
COPY ./src /var/www/html

PHP-FPM (php:8.2-fpm) runs as a process manager that listens for FastCGI connections. Nginx forwards PHP requests to it over the network rather than running PHP itself. The pdo, pdo_mysql, and mysqli extensions cover both PDO-based and procedural MySQL access.


Step 4: Configure Nginx

Create nginx.conf:

server {
    listen 80;
    server_name localhost;

    root /var/www/html;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location ~ /\.ht {
        deny all;
    }
}

fastcgi_pass php:9000 routes PHP requests to the PHP-FPM container. Inside Docker Compose, containers reach each other by service name, so php resolves to the PHP-FPM container's IP. Port 9000 is the default PHP-FPM port.

The location ~ /\.ht block denies access to .htaccess files, which are Apache-specific and should not be served directly.

The try_files directive is updated from the original $uri /index.php?$query_string to $uri $uri/ /index.php?$query_string to handle directory requests correctly, which matters for frameworks that expect directory-style URLs.


Step 5: Write the Docker Compose File

Create docker-compose.yml:

services:
  php:
    build: .
    volumes:
      - ./src:/var/www/html
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ./src:/var/www/html
    ports:
      - "8080:80"
    depends_on:
      - php
    networks:
      - app-network

  mysql:
    image: mysql:8.0
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - app-network

volumes:
  db_data:

networks:
  app-network:
    driver: bridge

A few deliberate choices:

nginx:alpine uses the Alpine-based Nginx image, which is smaller than nginx:latest (based on Debian). For local development the difference is minor, but it is a better habit.

restart: unless-stopped on the MySQL service restarts the container automatically if it crashes or if the Docker daemon restarts, but not if the container is explicitly stopped with docker compose stop.

The MySQL credentials reference ${VARIABLE_NAME} from the .env file rather than hardcoded values.

The named volume db_data persists the MySQL data between container restarts. Without it, the database is lost every time the container stops.

The version field at the top of Compose files is deprecated in current Docker Compose versions and is omitted here.


Step 6: Create the .env File

Create .env in the project root:

MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_DATABASE=app_db
MYSQL_USER=appuser
MYSQL_PASSWORD=apppassword

Add .env to .gitignore and commit a .env.example with placeholder values:

# .env.example
MYSQL_ROOT_PASSWORD=
MYSQL_DATABASE=
MYSQL_USER=
MYSQL_PASSWORD=

Docker Compose reads the .env file automatically when it is in the same directory as docker-compose.yml. The ${MYSQL_ROOT_PASSWORD} references in the Compose file expand to the values in .env.


Step 7: Build and Start the Environment

docker compose up -d --build

The first run builds the PHP image and pulls Nginx and MySQL. Visit http://localhost:8080 to confirm Nginx is serving the PHP info page.

Useful commands:

# Follow logs for all services
docker compose logs -f

# Follow logs for a specific service
docker compose logs -f nginx

# Open a shell in the PHP container
docker exec -it $(docker compose ps -q php) sh

# Stop containers (preserves volumes)
docker compose stop

# Remove containers (preserves volumes)
docker compose down

# Remove containers and volumes (deletes the database)
docker compose down -v

Step 8: Connect PHP to MySQL

Inside Docker Compose, the MySQL host is the service name mysql, not localhost. Update src/index.php:

<?php

$host = getenv('MYSQL_HOST') ?: 'mysql';
$dbname = getenv('MYSQL_DATABASE');
$user = getenv('MYSQL_USER');
$pass = getenv('MYSQL_PASSWORD');

try {
    $pdo = new PDO(
        "mysql:host=$host;dbname=$dbname;charset=utf8mb4",
        $user,
        $pass,
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
    );
    echo "Connected to database: $dbname";
} catch (PDOException $e) {
    echo "Connection failed: " . $e->getMessage();
}

utf8mb4 is the correct charset for full Unicode support, including emoji. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION converts PDO errors into exceptions, which is the recommended setting for both development and production.

The environment variables (MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD) passed to the MySQL container are also available to PHP if the php service is given matching environment entries in docker-compose.yml:

php:
  build: .
  environment:
    MYSQL_DATABASE: ${MYSQL_DATABASE}
    MYSQL_USER: ${MYSQL_USER}
    MYSQL_PASSWORD: ${MYSQL_PASSWORD}
  volumes:
    - ./src:/var/www/html
  networks:
    - app-network

MySQL startup timing: MySQL takes several seconds to initialize on first start. PHP may attempt the connection before MySQL is ready. Add a healthcheck to the mysql service and a condition: service_healthy to depends_on to prevent this:

mysql:
  image: mysql:8.0
  healthcheck:
    test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
    interval: 5s
    timeout: 5s
    retries: 10

php:
  depends_on:
    mysql:
      condition: service_healthy

Common Issues

Port 8080 is already in use. Change "8080:80" to another host port, such as "8081:80".

MySQL connection refused. The most common cause is MySQL not having finished initializing. Add the healthcheck described above. docker compose logs mysql shows the initialization progress.

Nginx 502 Bad Gateway. PHP-FPM is not running or not reachable. Run docker compose logs php to check for PHP startup errors. Confirm fastcgi_pass php:9000 in nginx.conf matches the service name in docker-compose.yml.

Code changes not reflected. The volume mount (./src:/var/www/html) should reflect changes immediately. If a change is not visible, confirm the file was saved and try a hard refresh in the browser. PHP-FPM does not need to be restarted for PHP file changes.


Key Takeaways

  • PHP-FPM and Nginx run in separate containers. Nginx forwards PHP requests to PHP-FPM via FastCGI using the service name as the host (fastcgi_pass php:9000).

  • Use nginx:alpine for a smaller Nginx image.

  • Named volumes (db_data) persist the MySQL database between container restarts. docker compose down -v removes them.

  • The .env file holds credentials. Docker Compose reads it automatically. Never commit it.

  • The MySQL host from PHP is the Compose service name mysql, not localhost.

  • Add a MySQL healthcheck and condition: service_healthy to prevent the PHP container from starting before MySQL is ready.


Conclusion

A PHP-FPM and Nginx Docker setup takes a few more configuration files than the Apache equivalent, but produces an environment that more closely reflects how most PHP production deployments are structured. The separation of Nginx and PHP-FPM also makes each service independently configurable and replaceable.

Once this foundation is in place, adding services — Redis for caching, a queue worker, a search engine — follows the same pattern: a new entry in docker-compose.yml, a network connection, and any required environment variables.


Running into a specific Nginx or PHP-FPM configuration issue with this setup? Share the error in the comments.

Topics
phpwebdevdocker
Interserver Hosting#1 VALUEAffordable, reliable hosting from $2.50/mo99.9% uptimeSponsored

Discussion

Join the discussion

Sign in to share your thoughts and engage with the community.

Sign In
Loading comments…

Continue Reading

More Articles

View all
Cover image for: AI for DevOps: Tools That Are Already Changing the Game
Jun 17, 20256 MIN READ min read

AI for DevOps: Tools That Are Already Changing the Game

How artificial intelligence is transforming CI/CD pipelines, monitoring, and incident response—today.

Cover image for: React Authentication with JWT: A Step-by-Step Guide
Oct 17, 20257 MIN READ min read

React Authentication with JWT: A Step-by-Step Guide

Learn how to implement secure JWT authentication in React. From login to route protection and API calls, this guide covers everything you need to know.

Cover image for: The 3-Device Rule: How to Simplify Your Digital Life and Stop Overbuying Tech
Aug 5, 20255 MIN READ min read

The 3-Device Rule: How to Simplify Your Digital Life and Stop Overbuying Tech

Tired of juggling too many devices? Learn the 3-device rule that helps you streamline your digital life, reduce clutter, and focus on what really matters.

Cover image for: Best Web Hosting of 2026 (Honest Picks From Real-World Use)
Jan 1, 20267 MIN READ min read

Best Web Hosting of 2026 (Honest Picks From Real-World Use)

Choosing the right web hosting in 2026 isn't just about price. A breakdown of the best providers, focusing on reliability, performance, and support.

|Made with · © 2026|TermsPrivacy
AboutBlogContact

Free, open-source tools for developers and creators · Community driven