Development

Dockerfiles Aren't Dead, They're Just Evolving

Dockerfiles Aren't Dead, They're Just Evolving

The Dockerfile has been declared obsolete often enough to qualify as a recurring industry ritual. A new build tool appears, a platform promises zero-config deployment, or a team gets tired of debugging image layers, and suddenly the Dockerfile is the problem.

It is not. The Dockerfile remains one of the clearest ways to describe how an application becomes a runnable artifact. What is changing is the standard we should expect from it. A modern Dockerfile is less a list of shell commands and more a deliberately designed boundary between source code, build tooling, runtime dependencies, and production operations.

Containers still need an explicit contract

PHP applications may be deployed through managed platforms, Kubernetes, virtual machines, or a simple Docker Compose stack. The infrastructure differs, but a repeatable artifact is still valuable. It gives local development, CI, staging, and production a shared definition of the application runtime.

A Dockerfile is particularly useful when it answers important operational questions directly:

  • Which PHP version and extensions does the application require?
  • What must happen before Composer dependencies can be installed?
  • Which files belong in the final runtime image?
  • Which process starts when the container runs?
  • What configuration is supplied at runtime rather than baked into the image?

If those answers are hidden in a CI script, a platform dashboard, and a developer wiki, deployment may feel easy until something fails. An explicit build definition is not bureaucracy. It is a maintainability tool.

The important evolution is from “works” to “intentional”

Early Dockerfiles often copied the entire repository into an image, installed everything as root, and shipped build tools alongside the application. That can work, but it creates slow builds, oversized images, unnecessary attack surface, and confusing production behavior.

Modern image construction separates concerns. Build dependencies belong in a builder stage. Runtime images should contain only what the application needs to serve requests. Composer caches should be used without turning them into part of the delivered artifact. Secrets should be provided at runtime or through the build system’s secret mechanisms, never copied into an image layer.

Consider a conventional PHP application using Composer and PHP-FPM:

FROM composer:2 AS vendor

WORKDIR /app

COPY composer.json composer.lock ./
RUN composer install \
    --no-dev \
    --no-interaction \
    --no-progress \
    --prefer-dist \
    --optimize-autoloader

COPY . .
RUN composer dump-autoload --no-dev --classmap-authoritative

FROM php:8.3-fpm-alpine

WORKDIR /var/www/html

COPY --from=vendor /app /var/www/html

RUN addgroup -S app && adduser -S app -G app \
    && chown -R app:app /var/www/html

USER app

CMD ["php-fpm"]

This is not a universal template. Native extensions, Node-based asset builds, queue workers, and framework cache warmup may require different stages and commands. The point is the shape of the solution: build artifacts first, then copy only the required result into a smaller runtime image.

Layer ordering is an engineering decision

Docker’s build cache rewards stable inputs. Composer manifests usually change less frequently than application code, so copying composer.json and composer.lock before the rest of the source lets dependency installation remain cached when only a controller, migration, or test changes.

That small ordering choice matters in CI. It shortens feedback loops without sacrificing reproducibility, provided the lock file is committed and honored. For production builds, composer install is normally the appropriate operation because it installs the resolved dependency set. Replacing it with composer update would make image builds unexpectedly choose newer packages and undermine repeatable deployments.

The same discipline applies to operating-system packages. Install only the packages needed for the build or runtime, and keep that choice visible. A database client may be useful for a migration job image, but it does not automatically belong in every web image.

One image does not have to mean one process

A common oversimplification is that every application needs a separate image for every process. Usually, the more useful rule is this: create one application image that can run one primary process per container.

The same PHP codebase may produce a web container, a queue worker, and a scheduler container. They can share the same image while using different commands:

services:
  web:
    image: example-app
    command: php-fpm

  worker:
    image: example-app
    command: php artisan queue:work

  scheduler:
    image: example-app
    command: php artisan schedule:work

This keeps dependencies and deployed code consistent while allowing each process to be scaled, restarted, and observed independently. It also avoids the fragile practice of using a process supervisor merely to keep unrelated long-running services inside one container.

Configuration belongs outside the image

An image should be portable across environments. Database credentials, API tokens, environment-specific endpoints, and signing keys should not be copied in during the build. Once a secret enters an image layer, removing the file later does not reliably remove it from the image history.

Configuration should instead be injected at runtime through the deployment environment’s configuration and secret facilities. The application should validate essential configuration on startup, fail clearly when it is absent, and expose health checks that reflect whether the process is actually ready to receive work.

There is a practical distinction here. A web process may be alive before its database connection is usable. A worker may be running but unable to reach its queue. Readiness and liveness should be designed around the behavior each process must provide, not added as generic boilerplate.

Dockerfiles are architecture documents

The best Dockerfiles reveal useful truths about a system. They make dependencies explicit, separate build-time work from runtime behavior, and show where operational responsibility begins. When the Dockerfile becomes hard to understand, that can be a signal that the application’s boundaries need attention too.

Tools will continue to generate Dockerfiles, abstract them behind buildpacks, and optimize their layers automatically. Those are useful advances. But abstractions do not eliminate the underlying decisions about reproducibility, dependencies, processes, configuration, and security.

Dockerfiles are not dead. They are growing up alongside the systems they package. Treat them as production code: small, reviewable, reproducible, and honest about what your application needs. That is far more durable than any argument about whether the file itself is fashionable.

Blog author portrait

Mihajlo

I’m Mihajlo — a developer driven by curiosity, discipline, and the constant urge to create something meaningful. I share insights, tutorials, and free services to help others simplify their work and grow in the ever-evolving world of software and AI.