Development

Dockerize Your Backend: Beyond Basic Containers for Resilient Systems

Dockerize Your Backend: Beyond Basic Containers for Resilient Systems

A container that starts successfully is not necessarily a backend that is ready to serve traffic. That distinction is where many Docker setups fall short. A basic Dockerfile can package an application, but a resilient backend also needs clear runtime boundaries, predictable configuration, graceful dependency handling, and a path to production operations.

Docker is most valuable when it turns “works on my machine” into a repeatable system contract. Your application should bring its runtime requirements with it, while infrastructure concerns such as secrets, persistent data, health checks, and deployment policy remain explicit rather than accidental.

Build an application image, not a development folder

A backend image should contain exactly what it needs to run: the right PHP extensions, application code, and production dependencies. It should not rely on files mounted from a developer’s machine or on packages installed interactively after startup.

For a PHP-FPM application using PostgreSQL, a multi-stage build keeps Composer out of the final runtime image while preserving a straightforward build process.

FROM composer:2 AS vendor

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

FROM php:8.3-fpm-alpine

RUN docker-php-ext-install pdo_pgsql

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

USER www-data
CMD ["php-fpm"]

This approach makes dependency installation reproducible when the lock file is committed. It also avoids shipping Composer and its build-time tooling in the running image. In a real application, add a .dockerignore file so local dependencies, logs, test artifacts, and environment files do not become part of the build context.

Be deliberate about what changes invalidate a build layer. Copying composer.json and composer.lock before the rest of the source allows Docker to reuse the dependency layer when only application code changes. That is a small design choice with a meaningful effect on local iteration and CI speed.

Separate configuration from the image

An image should be portable across environments. Database addresses, credentials, queue endpoints, logging levels, and feature settings belong in runtime configuration, not hard-coded PHP files or baked image layers.

A development Compose file can make those dependencies visible and easy to start together.

services:
  app:
    build: .
    environment:
      DB_DSN: "pgsql:host=db;port=5432;dbname=app"
      DB_USER: "app"
      DB_PASSWORD: "development-password"
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: development-password
    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 named volume protects database data from routine container replacement. The health check improves startup ordering, but it is not a substitute for application resilience. A database can become unavailable after the backend has already started, during a restart, network interruption, maintenance event, or failover.

For production, do not treat the example password as a secrets strategy. Supply secrets through the platform’s approved secret mechanism and restrict who can read them. Environment variables are convenient, but their exposure characteristics depend on the runtime, process inspection permissions, logs, and deployment tooling.

Design for dependencies that are temporarily unavailable

Backend services should distinguish between a permanent configuration failure and a transient dependency failure. Retrying a malformed DSN will not help. Retrying a database connection during startup may be entirely reasonable, especially when the infrastructure starts independently from the application.

Keep retries bounded, add delay, and fail clearly once the budget is exhausted. Unbounded retries hide broken deployments and can prevent an orchestrator from recognizing a failed service.

<?php

function connectDatabase(): PDO
{
    $attempts = 5;

    for ($attempt = 1; $attempt <= $attempts; $attempt++) {
        try {
            return new PDO(
                $_ENV['DB_DSN'],
                $_ENV['DB_USER'],
                $_ENV['DB_PASSWORD'],
                [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_TIMEOUT => 5,
                ]
            );
        } catch (PDOException $exception) {
            if ($attempt === $attempts) {
                throw new RuntimeException(
                    'Database connection failed after retries.',
                    0,
                    $exception
                );
            }

            usleep($attempt * 200000);
        }
    }

    throw new LogicException('Unreachable retry state.');
}

This is intentionally modest. Retries must be chosen carefully for each operation. Retrying an idempotent connection attempt is different from retrying a payment request or a database write whose outcome is unknown. For request-path work, use timeouts, preserve idempotency where possible, and avoid turning a slow dependency into a pile-up of waiting PHP workers.

Health checks should represent useful readiness

A process check answers, “Is PHP-FPM running?” A readiness check answers, “Can this instance safely accept traffic?” They are related but not identical.

Expose a lightweight endpoint that verifies only the dependencies required to serve your core request path. Avoid making it perform expensive queries or call every optional integration. If an application cannot reach its primary database, returning an unhealthy response may be appropriate. If an analytics service is temporarily unavailable, taking the entire API out of rotation may be counterproductive.

Put the health endpoint behind your reverse proxy and configure the deployment platform to use it. Docker can restart a failed process, but only the surrounding platform can usually remove an unhealthy instance from traffic, replace it, or scale it. Containers are a packaging primitive; resilience is a system property.

Make observability and shutdown part of the design

Containers are ephemeral. Write logs to standard output and standard error so the runtime can collect them. Include request identifiers where your application architecture supports them, log failures with enough context to diagnose them, and never log credentials or raw sensitive payloads.

Shutdown deserves equal care. When a container receives a termination signal, the application should stop accepting new work, allow in-flight requests a bounded period to finish, and close connections cleanly. Long-running workers need explicit signal handling and a safe way to resume jobs without duplicating side effects.

Finally, test the operational behavior, not only the happy path. Start from a clean machine with docker compose up --build. Restart the database while the application is running. Confirm that a rebuilt image contains no local development artifacts. Confirm that data survives an application container replacement but that a deliberately removed database volume does not. These checks reveal whether the architecture matches the diagram in your head.

The goal is not to make Docker configuration elaborate. It is to make important assumptions visible: what the service needs, how it fails, how it recovers, and how it is observed. Once those answers live in the image, configuration, and deployment design, a container stops being a convenient wrapper and becomes a dependable unit of backend delivery.

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.