Development

Dockerfiles are Dead: Embrace Declarative Infrastructure for Better Systems

Dockerfiles are Dead: Embrace Declarative Infrastructure for Better Systems

Dockerfiles are not literally dead. They still build a remarkable number of production images, and a well-written one is often the most portable way to describe a container build.

But they are increasingly the wrong place to express an entire system.

A Dockerfile is an imperative recipe: start from this base image, run these commands in this order, copy these files, set this working directory. That is useful for packaging an application. It becomes fragile when it quietly absorbs responsibility for operating-system packages, PHP extensions, database assumptions, development tooling, secrets, process supervision, and deployment behavior.

Better systems separate those concerns and describe desired state declaratively.

The hidden cost of the build recipe

Imperative build steps create state by replaying commands. The result depends on details that are easy to overlook: the selected base-image tag, package repository contents, shell behavior, cache layers, and whether a command is safe to run twice.

Consider a familiar PHP image:

FROM php:8.3-fpm
RUN apt-get update && apt-get install -y libzip-dev
RUN docker-php-ext-install pdo_mysql zip
COPY . /var/www/app
RUN composer install --no-dev --optimize-autoloader

This is concise, but it mixes several lifecycles. PHP runtime dependencies change differently from application source. Composer dependencies should be cacheable independently of a source edit. The application’s database connection should not be embedded in the image. And a deployment may need to know more than “an image was built successfully.”

The Dockerfile has become a miniature configuration-management system without the guardrails of one.

Declarative means describing the target, not narrating the journey

Declarative infrastructure starts with the state the system should have: an application service exposes HTTP, a worker consumes queued jobs, a database owns persistent data, and configuration arrives from the environment or a managed secret store. The platform determines how to reconcile that state.

For container workloads, a deployment manifest can express the operational contract more clearly than build commands can:

services:
  api:
    image: registry.example.com/catalog-api:1.4.0
    environment:
      APP_ENV: production
      DATABASE_URL: ${DATABASE_URL}
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: catalog
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  postgres-data:

This example is not a complete production deployment, and it should not be treated as one. It does show the useful boundary: the application image is an immutable artifact, while service relationships, runtime configuration, ports, and storage are declared outside it.

In a larger environment, the same idea may be represented through an orchestration platform, infrastructure-as-code definitions, managed database configuration, and policy controls. The syntax matters less than the separation of intent from procedural setup.

What should remain in an image build

“Embrace declarative infrastructure” does not mean put everything in YAML. An image still needs a reproducible way to package application code and runtime dependencies. The practical goal is to make that build small, deterministic, and narrowly scoped.

  • Pin meaningful base-image versions and review base-image updates deliberately.
  • Install only runtime libraries and PHP extensions required by the application.
  • Copy dependency manifests before application source so dependency installation can be cached appropriately.
  • Build frontend assets or optimized Composer autoload files during the build when they belong to the release artifact.
  • Run one clear application process per container where possible, rather than embedding a general-purpose supervisor to compensate for missing service design.

A multi-stage build is often a good compromise. Composer can run in a builder stage, while the final PHP runtime receives only the application and installed dependencies. The operational environment then decides how many replicas run, where logs go, what configuration is injected, and how the service is restarted.

Move mutable concerns out of the Dockerfile

The most valuable migration is usually not replacing a Dockerfile. It is identifying what does not belong in it.

Configuration and secrets

Configuration varies by environment; secrets should vary without becoming image layers or source files. Pass them at runtime through the deployment mechanism your environment supports. A rebuilt image should not be required merely because a database host, queue name, or API credential changes.

For a PHP application, validate required configuration at startup and fail clearly when it is missing. A container that starts but points at the wrong database is more dangerous than one that refuses to start.

Database lifecycle

Database schema migrations are not just another startup command. They change shared, persistent state and need ordering, visibility, and failure handling. Run them as an explicit release step or a controlled job. Make migrations safe to retry where possible, and do not assume that a new application instance can safely serve traffic before the schema transition is compatible with it.

This is especially important for rolling deployments. Favor expand-and-contract changes: add a nullable column or new table first, deploy code that can work with both forms, backfill separately if needed, then remove old structures in a later release.

Health and readiness

A process running is not proof that an API is ready. A useful readiness check verifies the minimum dependencies needed to serve the request path, without turning every health check into a slow integration test. Keep liveness checks focused on whether the process is wedged; use readiness to control whether traffic should reach it.

Declarative systems still need engineering judgment

Declarations can obscure complexity just as effectively as shell scripts can. A large manifest full of copied defaults, ambiguous image tags, and undocumented overrides is not an improvement. Nor is introducing a complex orchestration stack for a small application that can be operated reliably with a simple service definition.

The test is pragmatic: can a developer or operator answer what will run, which version will run, what it depends on, where its data lives, and how it fails? If the answer requires reading three Dockerfiles, a shell script, a CI configuration, and a wiki page, the system has lost clarity.

Keep build definitions for building. Keep runtime declarations for running. Keep infrastructure definitions for provisioning. Keep release procedures explicit when they modify shared state.

The Dockerfile becomes smaller, and the system becomes stronger

The future is not a dramatic funeral for Dockerfiles. It is a demotion to their proper role: a packaging detail within a broader, declarative system.

That shift makes PHP services easier to reproduce, databases safer to evolve, deployments easier to reason about, and incidents less dependent on someone remembering the exact sequence of commands that created a working environment once. The best infrastructure is not the one with the most automation. It is the one whose intended state is obvious, reviewable, and repeatable.

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.