Insights into Company
← Backend Concepts in Depth topics

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 vs container
Virtual machineContainer
IncludesFull guest OSApp + libraries only
Start timeMinutesSeconds
SizeGBsMBs
IsolationStrong (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

Checklist
PracticeWhy
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 userLimits 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/stderrCollected centrally (CloudWatch, ELK)
Managed MySQL/Kafka in productionBackups, 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.