Cloud & Infrastructure

Docker basics for web developers

With Docker, unify the development and deployment environment of web applications. In this guide, with a real PHP example, learn step by step how to build images, containers, and volumes.

Cloud & Infrastructure

Why is Docker Essential for Web Developers?

If you've ever heard or said the phrase "it works on my machine," you've exactly faced the problem that Docker was built to solve. Docker allows you to package software along with all its dependencies — libraries, programming language versions, environment variables, and even the base operating system — into a package called an Image. You then run this image as a Container that behaves identically in any environment, from a personal laptop to a cloud server.

For web developers, this means the end of the hassle of installing and configuring MySQL, PHP, Nginx, or Node.js on different operating systems. Instead of direct installation, you just write a docker-compose.yml file and bring up the entire application infrastructure with a single command. In this article, with a real example of a PHP application, you'll learn the core concepts of Docker from scratch: building images with a Dockerfile, running containers, managing data with volumes, and finally lightweight orchestration with Docker Compose.

Prerequisites and Installing Docker

We assume you are familiar with basic Linux and command-line concepts. For installation, refer to the official Docker documentation. On Windows and macOS, install Docker Desktop; on Linux, install the Docker Engine package. After installation, verify the installation with the following commands:

docker --version
docker run hello-world

The second command runs a test container, and if everything is correct, a welcome message will be printed.

The Concept of Image vs. Container

Think of an Image as a template or snapshot: it is read-only and includes the base operating system, application code, and settings. A Container is a running instance of that image that has its own writable layer. You can run multiple separate containers from a single image; just like baking several identical cakes from one mold.

Building Your First Dockerfile for a PHP App

Let's build a simple PHP application with a welcome page and a connection to MySQL. First, create the project structure:

my-php-app/
├── src/
│   └── index.php
├── Dockerfile
└── docker-compose.yml

We'll write the content of src/index.php like this:

<?php
$host = getenv('DB_HOST') ?: 'db';
$user = getenv('DB_USER') ?: 'root';
$pass = getenv('DB_PASS') ?: 'secret';
$db   = getenv('DB_NAME') ?: 'app_db';

try {
    $pdo = new PDO("mysql:host=$host;dbname=$db;charset=utf8mb4", $user, $pass);
    $stmt = $pdo->query("SELECT 'Connected to MySQL successfully!' AS message");
    echo "<h1>" . $stmt->fetch()['message'] . "</h1>";
} catch (PDOException $e) {
    http_response_code(500);
    echo "<h1>DB Connection Failed: </h1>" . htmlspecialchars($e->getMessage());
}

Now, let's create the Dockerfile. This file contains the instructions for building the image:

# Use the official PHP image with FPM and mysqli
FROM php:8.3-fpm

# Install necessary extensions
RUN docker-php-ext-install pdo_mysql mysqli

# Copy application code into the container
COPY src/ /var/www/html/

# Set the working directory
WORKDIR /var/www/html

# Non-root user for better security
RUN useradd -m appuser && chown -R appuser:appuser /var/www/html
USER appuser

# Default FPM port
EXPOSE 9000

Each line in the Dockerfile creates a layer in the image. Docker caches these layers; so the order of instructions matters. Place instructions that change less frequently (like installing extensions) higher up to save time in subsequent builds.

Commonly Used Dockerfile Instructions

  • FROM: Specifies the base image. Always use official and specific versions, not latest.
  • RUN: A command executed during the image build (like installing a package).
  • COPY: Copies files from the host system into the image.
  • WORKDIR: Sets the default directory for subsequent instructions.
  • EXPOSE: Only documents the port; it doesn't actually open the port.
  • CMD or ENTRYPOINT: The default command when the container runs.

Running Containers and Managing Volumes

To build the image from the Dockerfile and run the container, execute the following commands:

# Build the image named my-php-app with tag v1
docker build -t my-php-app:v1 .

# Run the container in the background
docker run -d --name my-app -p 8080:80 my-php-app:v1

With -p 8080:80, you map port 8080 on the host system to port 80 in the container. Now open http://localhost:8080 in your browser.

Volumes; Data Persistence

Containers are inherently stateless; any change to the container's filesystem is lost when the container is removed. For persistent data — such as uploaded files or databases — we use Volumes. There are two main types:

  • Named Volume: Managed by Docker and stored in /var/lib/docker/volumes.
  • Bind Mount: Directly connects a folder from the host system to the container. Great for development because code changes take effect immediately.

Example of a Bind Mount for development:

docker run -d --name my-app-dev \
  -p 8080:80 \
  -v $(pwd)/src:/var/www/html \
  my-php-app:v1

Now, any change to src/index.php is applied in the container without needing to rebuild the image. This makes the development workflow much faster.

Common Mistake: Forgetting a volume for the database. If you run MySQL without a volume, all data will be deleted with every docker-compose down. Always define a volume for data-driven services.

Connecting Multiple Containers with Docker Compose

A real application isn't just PHP; it also needs MySQL, Redis, or Nginx. Instead of running each container separately and connecting them manually, we use Docker Compose. We'll write the docker-compose.yml file like this:

services:
  web:
    build: .
    ports:
      - "8080:80"
    volumes:
      - ./src:/var/www/html
    environment:
      DB_HOST: db
      DB_USER: root
      DB_PASS: secret
      DB_NAME: app_db
    depends_on:
      - db

  db:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: app_db
    volumes:
      - db_data:/var/lib/mysql
    ports:
      - "3306:3306"

volumes:
  db_data:

Now, with a single command, you bring up the entire infrastructure:

docker-compose up -d

Docker creates a private network between the services; so the web container can connect to MySQL using the service name db. This is exactly the value we set as DB_HOST in the PHP code.

Commonly Used Compose Commands

  • docker-compose up -d: Build and run all services in the background.
  • docker-compose logs -f web: View live logs of the web service.
  • docker-compose exec web bash: Enter the shell of a running container.
  • docker-compose down: Stop and remove containers and the network (volumes are preserved).
  • docker-compose down -v: Complete removal along with volumes — use with caution.

Common Mistakes and Troubleshooting Tips

Here are some common issues and their solutions:

1. Port is already in use

If you see the error port is already allocated, either another service is using that port or a previous container is still running. Check with:

docker ps
docker stop <container_id>

2. Cannot connect to the database

Make sure DB_HOST is exactly equal to the service name in Compose (here db), not localhost. Inside the Docker network, localhost refers to the container itself, not the host.

3. Large images and slow builds

Use lightweight images like php:8.3-fpm-alpine. Also, create a .dockerignore file so that folders like vendor or node_modules are not included in the build context:

vendor/
node_modules/
.git/
*.log

4. Code changes are not applied

If you're not using a Bind Mount, you need to rebuild the image after every code change: docker-compose up -d --build. In development, be sure to define the volume as shown in the example above.

Improving Image Security and Performance

A few professional tips for production:

  • Always use a specific version tag, not latest. This ensures reproducibility.
  • Use a non-root user in the container (like USER appuser in the Dockerfile).
  • Store sensitive environment variables like passwords in a .env file and don't add it to git. In Compose, use env_file.
  • To reduce image size, use multi-stage builds; especially for Node.js or Go applications.

Summary and Next Steps

In this article, you learned how to bring up a PHP app along with MySQL in minutes using Docker. The key concepts — image, container, volume, and Docker Compose — are the foundation of any Docker project. Now you can repeat this pattern for any language or framework.

For the next step, try these exercises: add an Nginx service as a reverse proxy to your Compose setup, or add a Redis container for caching. If you're looking for cloud environments to run your containers, ServerNet's cloud services can be a suitable option for deployment. But the most important thing is to get started right now and write your first Dockerfile.

ServerNet Support

ServerNet engineering & editorial team — specialists in infrastructure, networking and web hosting.

Contact
Share:

Comments 0

No comments yet — be the first!

Leave a comment