Docker Is Not Just Packaging: Why Mixing Host and Container Workflows Breaks Your App

Docker Is Not Just Packaging: Why Mixing Host and Container Workflows Breaks Your App
Photo by Mika Baumeister / Unsplash

There is a subtle but very common misunderstanding when people start using Docker. They think Docker is just a way to “wrap” an already working application. So they keep doing their usual workflow on the host machine, then simply copy the result into a container. It feels convenient. It works… until it doesn’t.

The real purpose of Docker is not packaging. It is reproducibility.


The Invisible Problem: Host vs Container Reality

Let’s start with a scenario that looks harmless:

composer install   # run on host
docker build .

At first glance, nothing seems wrong. But under the surface, you are already creating a split-brain environment.

Your host might be running:

  • PHP 8.3
  • macOS or Windows
  • A specific set of extensions

Your container might be:

  • PHP 8.2
  • Debian or Alpine Linux
  • Missing or different extensions

Now imagine your vendor/ folder is generated based on the host environment and then copied into a container that has a different runtime contract. This is where things become unreliable in a way that is hard to debug.

The failure won’t always be obvious. Sometimes it works in development and breaks only in production. Sometimes only specific features fail. Sometimes Composer silently installs different dependency versions due to platform differences.

That’s what makes this issue dangerous. It is not loud. It is quietly inconsistent.


Why This Breaks: Composer Is Not Just Downloading Packages

Many developers underestimate what Composer actually does.

Composer resolves dependencies based on:

  • PHP version (platform.php)
  • Installed extensions (ext-*)
  • OS-level compatibility
  • Constraints in composer.json and composer.lock

So when you run composer install on the host, you are telling Composer:

“Resolve dependencies for THIS machine.”

But then you run the app in a container that says:

“Actually, I’m a different machine.”

That mismatch is the root cause.


The Correct Mental Model

Instead of thinking:

“Docker runs my app”

You should think:

“Docker defines the only environment where my app is valid”

That means the container must be able to:

  • Install dependencies
  • Build the application
  • Run the application

All without relying on the host

This is the key idea: self-contained reproducibility.


A Clean Approach: Build From Container Perspective

A proper setup moves dependency installation into the container build process.

Here is a simple but correct example:

FROM composer:2 AS builder

WORKDIR /app
COPY composer.json composer.lock ./

RUN composer install \
    --no-dev \
    --prefer-dist \
    --no-interaction \
    --optimize-autoloader

COPY . .

FROM php:8.3-fpm

WORKDIR /var/www/html
COPY --from=builder /app /var/www/html

In this approach:

  • Composer runs inside a controlled environment
  • Dependencies match the runtime PHP version
  • The final image is clean and predictable

Also, your .dockerignore should prevent accidental leakage from host:

vendor
node_modules
.git
.env

This ensures you are not silently importing host artifacts.


The Subtle Trap: “It Works on My Machine”

This phrase exists because of exactly this problem.

When your build depends on the host:

  • Your laptop becomes part of the system
  • Your OS becomes part of the dependency tree
  • Your installed tools affect the result

That means your app is no longer portable. It is environment-coupled.

Docker is supposed to eliminate that. But if you mix host and container workflows, you reintroduce the same problem in a more confusing way.


When It’s Actually Okay to Use the Host

Now, this is where nuance matters.

Running things on the host is not always wrong. In fact, in local development, it is often practical.

For example:

  • Running composer install locally for faster iteration
  • Mounting source code as a volume
  • Using IDE-integrated tools

These are valid trade-offs. But they should be treated as development shortcuts, not production truth.

The key rule is:

Production images must not depend on host-generated artifacts.

You can be flexible in development, but strict in build and deployment.


A Realistic Development vs Production Split

A mature workflow often looks like this:

In development:

composer install
docker-compose up

In production:

docker build .
docker run ...

The difference is intentional.

Development prioritizes speed and convenience.

Production prioritizes consistency and reliability.

Confusing the two is where most teams get into trouble.


Another Thing People Miss: Native Dependencies

This problem becomes even worse when native extensions or binaries are involved.

Examples:

  • PHP extensions like ext-intl, ext-gd
  • Node modules with native bindings
  • OS-specific binaries

If these are built on the host and copied into a container, they may:

  • Fail at runtime
  • Crash silently
  • Produce inconsistent behavior

This is especially common when building on macOS and running on Linux containers.


The Deeper Insight: Docker as a Contract

The most important shift is this:

Docker is not just a tool. It is a contract.

It defines:

  • Which PHP version is used
  • Which extensions exist
  • Which dependencies are valid

When you run commands outside that contract, you are essentially saying:

“I trust another environment more than my container.”

That defeats the purpose.


Finally

Your original intuition is correct, but the refined version is more powerful:

The problem is not running things on the host. The problem is mixing host and container outputs in an uncontrolled way.

Once you understand that, everything becomes clearer.

You can still be pragmatic. You can still optimize your workflow. But you will know exactly where the boundaries are.

And that’s the difference between using Docker… and actually understanding Docker.

Support Us

Share to Friends