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, notlatest.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.CMDorENTRYPOINT: 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.
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 appuserin the Dockerfile). - Store sensitive environment variables like passwords in a
.envfile and don't add it to git. In Compose, useenv_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.
Comments 0
No comments yet — be the first!