Docker for Laravel, Spring Boot, React, MySQL & Kafka
What containers are, images vs containers, writing Dockerfiles for Laravel and Spring Boot, a docker-compose stack with MySQL, Redis and Kafka for local development, and production practices.
Running example: an e-commerce stack (Laravel API, queue worker, React front end, MySQL, Redis, Kafka)
1. Containers explained
A container packages an application with its runtime (PHP 8.3 + extensions, or Java 21), libraries and configuration, and runs it isolated from other processes, sharing the host's kernel. "Works on my machine" problems disappear because every developer, CI and production run the same image.
An image is the read-only template, built from a Dockerfile, layer by layer. A container is a running instance of an image. A registry (Docker Hub, AWS ECR, GitHub Container Registry) stores images. docker compose runs several containers together for development; Kubernetes or ECS run them in production.
| Virtual machine | Container | |
|---|---|---|
| Includes | Full guest OS | App + libraries only |
| Start time | Minutes | Seconds |
| Size | GBs | MBs |
| Isolation | Strong (hypervisor) | Process-level (namespaces, cgroups) |
2. Dockerfile for a Laravel API
Multi-stage build: dependencies installed in one stage, slim runtime in the next
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-interaction
FROM php:8.3-fpm-alpine
RUN docker-php-ext-install pdo_mysql opcache bcmath && apk add --no-cache icu-dev && docker-php-ext-install intl
WORKDIR /var/www
COPY --from=vendor /app/vendor ./vendor
COPY . .
RUN php artisan config:cache && php artisan route:cache && php artisan view:cache \
&& chown -R www-data:www-data storage bootstrap/cache
USER www-data
EXPOSE 9000
CMD ["php-fpm"]
Copying composer.json before the rest of the code lets Docker reuse the cached vendor layer when only application code changes, so builds take seconds instead of minutes. Never COPY .env into an image: secrets are injected at runtime as environment variables. Add .env, node_modules, vendor and storage/logs to .dockerignore.
3. Spring Boot and React images
Spring Boot
FROM eclipse-temurin:21-jdk AS build
WORKDIR /src
COPY . .
RUN ./gradlew bootJar --no-daemon
FROM eclipse-temurin:21-jre-alpine
COPY --from=build /src/build/libs/payments.jar /app.jar
USER 1000
EXPOSE 8080
CMD ["java", "-XX:MaxRAMPercentage=75", "-jar", "/app.jar"]
React: build static files, serve with nginx
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
4. docker-compose: the whole stack locally
docker-compose.yml
services:
api:
build: .
env_file: .env
depends_on: [mysql, redis, kafka]
web:
image: nginx:1.27-alpine
ports: ["8080:80"]
volumes: ["./docker/nginx.conf:/etc/nginx/conf.d/default.conf", "./public:/var/www/public"]
worker:
build: .
command: php artisan queue:work redis --queue=payments,emails,default --tries=3
depends_on: [api]
scheduler:
build: .
command: sh -c "while true; do php artisan schedule:run; sleep 60; done"
mysql:
image: mysql:8.0
environment: { MYSQL_DATABASE: shop, MYSQL_ROOT_PASSWORD: secret }
volumes: ["mysql-data:/var/lib/mysql"] # data survives container restarts
redis:
image: redis:7-alpine
kafka:
image: bitnami/kafka:3.7
environment:
KAFKA_CFG_NODE_ID: 1
KAFKA_CFG_PROCESS_ROLES: broker,controller
KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
volumes:
mysql-data:
Daily commands
docker compose up -d --build # build and start everything
docker compose ps # what is running
docker compose logs -f worker # follow queue worker logs
docker compose exec api php artisan migrate
docker compose exec mysql mysql -uroot -psecret shop
docker compose down # stop (add -v to delete volumes, i.e. the database)
Inside the compose network, services reach each other by service name: DB_HOST=mysql, REDIS_HOST=redis, KAFKA_BROKERS=kafka:9092. "localhost" inside a container means that container itself, the most common beginner mistake.
5. Production practices
| Practice | Why |
|---|---|
| One process per container (api, worker, scheduler separate) | Scale queue workers independently of web traffic during a sale |
| Pin image versions (mysql:8.0.36, not latest) | Reproducible builds |
| Run as non-root user | Limits damage if the app is compromised |
| Secrets from the platform (Kubernetes secrets, AWS Secrets Manager) | Never baked into images |
| Health checks (/up in Laravel 11, /actuator/health in Spring) | Orchestrator restarts unhealthy containers |
| Logs to stdout/stderr | Collected centrally (CloudWatch, ELK) |
| Managed MySQL/Kafka in production | Backups, failover and upgrades handled for you |
Note for shared hosting (cPanel/GoDaddy): Docker is not available there, so this project deploys with Git + cron. Docker becomes relevant when you move to a VPS, AWS, or Kubernetes.
6. Interview questions
Image vs container?
An image is the immutable template; a container is a running instance with its own writable layer.
Why multi-stage builds?
Build tools (Composer, JDK, Node) stay in the build stage, so the final image is small and has less attack surface.
How do containers talk to each other in compose?
Over a shared network using service names as hostnames.
Where does MySQL data live?
In a named volume; containers are disposable, volumes persist.
