Razvoj

Beyond Dockerfiles: Composing Your System's Future with Declarative Infrastructure

Iznad Dockerfileova: Oblikovanje budućnosti vašeg sustava deklarativnom infrastrukturom

A Dockerfile is a useful promise: given the same inputs, build the same application image. But an application image is not a running system. It does not describe which services must start together, where state lives, how configuration enters the process, or what should happen when one dependency is unavailable.

That gap is where many backend teams accumulate fragile setup scripts, undocumented commands, and environments that work only because someone remembers the correct sequence. Declarative infrastructure is the practical alternative: describe the desired system in versioned configuration, then let tooling converge on that description.

Think in systems, not containers

For a PHP service, the Dockerfile should usually focus on one concern: producing a runnable PHP application image. It can install extensions, copy application code, and define a process. The composition layer should describe the relationships around that process: a database, a queue or cache, a reverse proxy, persistent volumes, networks, and environment-specific settings.

This distinction matters because it keeps responsibilities legible. When a database connection fails, the question becomes “what does the declared system say?” rather than “which setup command did we forget to run?”

A small Compose configuration can make those relationships explicit:

services:
  app:
    build: .
    environment:
      APP_ENV: development
      DATABASE_URL: postgresql://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "8080:8080"

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 10

volumes:
  postgres-data:

The important value is not that this file is short. It is that a new developer can inspect it and answer concrete questions: which hostname should PHP use, which port is exposed to the host, and which data survives a container replacement?

Declare the contract, then design for reality

Declarative configuration is often misunderstood as a guarantee that dependencies are always ready. It is not. A health check can help coordinate startup, but networks fail, databases restart, credentials rotate, and a service may become unavailable after the application has started.

Your application still needs operational behavior. PHP code that opens a database connection should use bounded timeouts. Queue consumers should make retry decisions deliberately. HTTP clients should retry only operations that are safe to repeat, and should avoid turning a slow downstream service into an amplified traffic surge.

This is where infrastructure declarations and application design reinforce each other. The configuration states that an application depends on PostgreSQL. The code accepts that PostgreSQL is a remote dependency with failure modes. Neither layer replaces the other.

Make state visible

State is the first place where containerized systems become misleading. A container filesystem is an implementation detail, not a database strategy. If PostgreSQL data, uploaded files, generated reports, or durable queue data matter after replacement, their storage must be explicitly designed.

  • Use named volumes for local development data that should persist across container recreation.
  • Use managed or deliberately operated persistent storage in production.
  • Keep backups, retention, restoration procedures, and access controls separate from the application image.
  • Do not treat a bind mount used for convenient local editing as a production deployment pattern.

The same principle applies to schema changes. A migration is a state transition, not merely a startup side effect. Running migrations automatically can be appropriate in a tightly controlled single-instance environment, but concurrent application instances can race. Production deployment should make ownership and ordering of migrations explicit.

Configuration is an interface

Environment variables are popular because they separate deploy-time values from image contents. They are helpful, but they are still an interface and deserve the same care as an HTTP API. Give variables stable names, document their meaning, validate required values at startup, and avoid silently falling back to unsafe defaults.

A PHP application can fail early with a clear message when a required setting is absent, rather than discovering the problem only when its first request arrives. For example, a configuration bootstrap can require a database URL and reject an empty value before constructing a connection.

$databaseUrl = getenv('DATABASE_URL');

if ($databaseUrl === false || $databaseUrl === '') {
    throw new RuntimeException('DATABASE_URL must be configured.');
}

Do not place long-lived production secrets directly in a committed Compose file. A declaration should reveal the shape of configuration without exposing credentials. Local development credentials may be acceptable when clearly scoped and disposable; production secret delivery needs controls appropriate to the deployment environment.

Keep development convenient without making it fictional

A development environment should remove unnecessary friction, but it should not hide the system’s important boundaries. If production uses a separate database service, local development should normally do the same. If the application depends on Redis for a cache or queue, a no-op replacement may be useful for a focused unit test, but it should not be the only way the team exercises the application.

Compose profiles or separate override files can help distinguish optional local tools from core dependencies. The goal is not to reproduce every production detail on a laptop. It is to make the essential topology, configuration, and failure assumptions testable before deployment.

Compose is a model, not necessarily the destination

Docker Compose is especially effective for local development, integration testing, and small deployments. Larger environments may use another scheduler or a managed platform. That does not invalidate the model. The transferable lesson is to describe desired services, connectivity, configuration, health expectations, and persistent resources as code.

Avoid copying configuration mechanically between environments. A local published port exists for developer access; an internal production database often should not publish a port at all. A local volume makes data easy to inspect; production storage needs durability and recovery properties. Shared concepts should remain consistent, while environment-specific implementation stays explicit.

The durable outcome

Dockerfiles make application builds repeatable. Declarative infrastructure makes the surrounding system understandable. Together, they reduce the amount of operational knowledge that lives only in chat messages and individual memory.

The best test is simple: can another engineer clone the repository, read the configuration, start the core system, and understand its boundaries without a guided tour? When the answer is yes, you have done more than containerize an application. You have composed a system that is easier to change, operate, and trust.

Portret autora bloga

Mihajlo

Ja sam Mihajlo — programer vođen znatiželjom, disciplinom i stalnom željom da stvorim nešto smisleno. Dijelim uvide, tutorijale i besplatne usluge kako bih pomogao drugima da pojednostave svoj rad i rastu u svijetu softvera i umjetne inteligencije koji se neprestano razvija.