Skip to main content

Command Palette

Search for a command to run...

Containerized: My Repository Structure & Design Decisions

Designing a Docker repo that explains itself

Updated
5 min read
Containerized: My Repository Structure & Design Decisions
N
Senior-level Fullstack Web Developer with 10+ years experience, including 2 years of Team Lead position. Specializing in responsive design and full-stack web development across the Vue.js and .NET ecosystems. Skilled in Azure/AWS cloud infrastructure, focused on DevOps techniques such as CI/CD. Experienced in system design, especially with software architecture patterns such as microservices, BFF (backend-for-frontend). Hands-on with Agile practices in team leading, and AI-assisted coding.

Series: Containers, Actually: Building Real Local Dev Environments
ACT III — Real Implementation: My Humhub Stack
Previous: Containerized: Architecture Overview of my Humhub Stack
Next: Step-by-Step: Bootstrapping my Containerized Environment

Disclaimer (NDA Notice)
This article is based on a real production-adjacent repository used in an internal project.
Due to NDA and organizational constraints, some names, paths, credentials, and configuration values are simplified, anonymized, or omitted.

The structure, reasoning, trade-offs, and Docker patterns are real and intentional.
Treat this as a design blueprint, not a copy-paste template.

This article answers a deceptively important question:

Why is the repository shaped the way it is?

Most Docker tutorials explain what files exist.
Few explain why they are separated, why some things are duplicated, or why convenience is sometimes rejected on purpose.

That “why” is the difference between a maintainable stack and a fragile one.


The Prime Directive: Structure Reflects Responsibility

Before looking at folders, establish the rule that governs everything:

Repository structure should mirror system boundaries, not developer convenience.

If your repository layout does not reflect:

  • runtime boundaries

  • ownership boundaries

  • lifecycle boundaries

then Docker will feel arbitrary and brittle.


High-Level Folder Layout

Here is a simplified but realistic repository structure:

repo-root/
├─ docker/
│  ├─ nginx/
│  │  └─ default.conf
│  ├─ php/
│  │  ├─ Dockerfile
│  │  └─ php.ini
│  ├─ mysql/
│  │  └─ init.sql
│  └─ elasticsearch/
│     └─ elasticsearch.yml
│
├─ app/
│  ├─ src/
│  ├─ public/
│  ├─ composer.json
│  └─ composer.lock
│
├─ docker-compose.yml
├─ .env.example
├─ .gitignore
└─ README.md

This structure encodes several deliberate decisions.


Why Configuration Lives Outside the App

A common mistake is putting Docker config inside the application folder.

This repository avoids that.

Why?

Because application code and infrastructure config have different lifecycles.

  • App code changes frequently

  • Infrastructure changes deliberately

  • App developers shouldn’t accidentally “refactor” infrastructure

That’s why:

  • /app contains only application concerns

  • /docker contains only runtime concerns

This separation prevents accidental coupling.


Dockerfiles: Purpose-Built, Not Generic

PHP Dockerfile (Example)

FROM php:8.2-fpm

RUN apt-get update \
 && apt-get install -y \
    libzip-dev \
    libpng-dev \
 && docker-php-ext-install pdo_mysql zip gd

COPY php.ini /usr/local/etc/php/php.ini

WORKDIR /var/www/html

Why this Dockerfile is minimal

This Dockerfile:

  • Installs only runtime dependencies

  • Does not install dev tools

  • Does not copy application code

Why?

Because:

  • Code is bind-mounted during local dev

  • Image rebuilds should be cheap

  • The image represents capability, not state


Dockerfiles vs Shared Images: The Trade-Off

You may ask:

Why not use a shared base image across projects?

That’s a valid option—but not always the right one.

Shared Images: Pros

  • Faster builds

  • Centralized updates

  • Strong standardization

Shared Images: Cons

  • Hidden coupling

  • Harder debugging

  • Version drift across teams

This repo chooses local Dockerfiles because:

  • The team owns the runtime

  • Debugging clarity matters more than build speed

  • Local dev should mirror production constraints explicitly

Correctness beats convenience here.


Volumes & Persistence Strategy

Persistence is where Docker setups silently fail.

Example: MySQL Volume

mysql:
  image: mysql:8
  volumes:
    - mysql-data:/var/lib/mysql

Why volumes are named explicitly

Named volumes:

  • Survive container rebuilds

  • Are isolated from host filesystem quirks

  • Make data lifecycle intentional

This is a deliberate refusal to use bind mounts for databases.

Why?

Because:

  • Bind mounts leak host behavior into containers

  • File permission issues become non-deterministic

  • Performance varies wildly on Windows

Databases deserve correctness, not convenience.


Bind Mounts: Used Carefully

Application code is bind-mounted:

app:
  volumes:
    - ./app:/var/www/html

Why this is acceptable:

  • Code is ephemeral

  • Reload speed matters

  • Losing code state is impossible (Git exists)

The rule remains consistent:

Mutable human work → bind mount
Durable system state → volume


Environment Variables: Explicit, Not Magical

This repository uses a .env file—but cautiously.

.env.example

APP_ENV=local
DB_HOST=mysql
DB_NAME=app
DB_USER=app
DB_PASSWORD=secret
REDIS_HOST=redis
QUEUE_HOST=rabbitmq

Why .env.example exists

  • Documents required configuration

  • Prevents secrets from leaking

  • Forces developers to opt in to local values

Actual .env files are:

  • Git-ignored

  • Local-only

  • Treated as replaceable


Why Environment Variables Aren’t Everything

Environment variables are powerful—but insufficient alone.

This repo avoids:

  • Overloading .env with complex config

  • Encoding architecture decisions as variables

  • Making runtime behavior implicit

Environment variables configure values, not structure.

Structure belongs in:

  • docker-compose

  • Dockerfiles

  • code defaults


docker-compose as an Architectural Document

Here’s a trimmed example:

services:
  web:
    image: nginx:alpine
    depends_on:
      - app

  app:
    build: ./docker/php
    environment:
      DB_HOST: mysql
      REDIS_HOST: redis

  mysql:
    image: mysql:8
    volumes:
      - mysql-data:/var/lib/mysql

This file tells you:

  • What exists

  • Who depends on whom

  • What persists

  • What communicates

It is not a startup script.
It is a system declaration.


Convenience vs Correctness: A Real Trade-Off

Convenient (but dangerous)

  • One container with everything

  • Database bind-mounted from C:\

  • Global Node installed everywhere

  • Runtime configuration hidden in scripts

Correct (but deliberate)

  • Separate containers

  • Explicit volumes

  • Explicit networking

  • Explicit versions

This repo consistently chooses correctness, even when it costs setup time.

Why?

Because convenience debt compounds faster than tech debt.


Local Dev vs Production Parity (With Honesty)

This setup aims for structural parity, not identical parity.

Local differs from production in:

  • Scale

  • Security hardening

  • Monitoring depth

But matches production in:

  • Service separation

  • Runtime versions

  • Communication patterns

  • Failure modes

That’s the sweet spot.

Perfect parity is impossible.
Structural parity is achievable—and valuable.


What You Should Learn From This Repo

The goal is not to copy this structure.

The goal is to ask:

  • What boundaries exist in my system?

  • What state deserves protection?

  • What can be disposable?

  • Where do humans interact with the system?

If your repository answers those questions by its shape, Docker will stop feeling complicated.


What Comes Next

With architecture and structure understood, the next step is execution:

  • Bootstrapping the environment

  • Building images

  • Running services

  • Verifying health

  • Working day-to-day

That’s where theory meets muscle memory.

Structure is not ceremony.
It’s how systems explain themselves.

Containers, Actually: Building Real Local Dev Environments

Part 10 of 18

This series explores full-stack local development with Docker—from core concepts and best practices to a real Windows implementation. The goal is to understand how things run, why they work, and how to build reproducible production environment.

Up next

Containerized: Architecture Overview of my Humhub Stack

Understanding the real system before running it