A default should reduce unnecessary decisions. It should not prevent necessary ones.
The more services I deployed, the less I wanted every application installed directly into the host operating system.
That is how Docker became my default.
Not because containers make every service simple. They do not automatically make an application secure, portable, backed up, or easy to recover. What they gave me was a consistent way to describe the moving parts: the image, configuration, storage, networks, ports, dependencies, and runtime behavior.
That structure became more valuable than the container itself.
I wanted the host to remain understandable
A native installation can spread application state across package repositories, service definitions, system users, configuration directories, scheduled tasks, logs, libraries, and version-specific dependencies.
That is manageable when the host has one purpose. It becomes harder when the same system carries ten unrelated services with ten different installation methods.
Containers moved more of that application-specific structure into a visible deployment definition. The host still needs an operating system, Docker Engine, storage, networking, drivers, updates, and monitoring. Docker does not eliminate host administration.
It does, however, help me answer a basic question:
What belongs to this application, and what belongs to the host?
The answer is not always perfect, but it is usually clearer than it was after months of one-off native installations.
A Compose file became part of the documentation
Docker Compose is designed to define an application’s services, networks, volumes, configuration, and secrets in a YAML application model. That does not make a Compose file complete documentation, but it gives the documentation a reliable technical center. Docker’s current Compose reference describes that application model directly.
A useful Compose definition can show:
- Which image and version provide the service
- Which ports are published to the host
- Which networks the service can reach
- Which volumes or bind mounts contain persistent state
- Which environment values configure runtime behavior
- Which dependencies affect startup
- Which health check represents readiness
- Which restart behavior is expected
That is far better than relying on a shell history or a memory of commands typed months earlier.
It also makes questionable decisions easier to see. A floating image tag, an unnecessary published port, an overly broad mount, a privileged container, or a secret embedded directly in the file is harder to ignore when the deployment is declarative.
Compose is not automatically good documentation. A cryptic, undocumented file can reproduce a bad design very efficiently. The value comes from treating the file as an operational contract rather than merely a way to launch containers.
Persistence is the real design work
The phrase “containers are disposable” is useful only when the important state is not disposable with them.
Before deploying a service, I need to separate at least five categories:
- Application configuration
- Databases and mutable application state
- User-created or irreplaceable data
- Cache, transcodes, and other reproducible temporary data
- Secrets and credentials
Docker volumes are persistent data stores managed by the container engine, while bind mounts connect a container to a specific host path. Both can be valid, but they create different operational responsibilities. Docker’s current documentation also warns that bind mounts are tied to the host’s directory structure and are writable by default unless configured otherwise. (Compose volumes, bind mounts)
That means the storage decision is not finished when the application starts.
I still need to know:
- What must be backed up?
- What must be restored together?
- What permissions does the container require?
- What happens if an expected mount is missing?
- Can the application initialize an empty directory and make that mistake look valid?
- Can I rebuild the service without guessing where its state lived?
My earlier article, The Container Path Problem That Looked Like Permissions, covers why a path can look correct at one layer while pointing somewhere entirely different at another. Storage Is More Than Capacity expands the same idea beyond Docker: storage is an availability and recovery dependency, not merely a number of terabytes.
Docker makes persistence visible. It does not design persistence correctly for me.
Networking became easier to declare and easier to misunderstand
Compose creates a default network for an application unless I define something more specific. Services on a shared Compose network can reach one another by service name, while services that do not share a network are isolated from direct communication through that network. Docker documents this behavior in the Compose networks reference.
That gives me useful control. Internal services do not need every port published to the rest of the LAN. A frontend can share one network with an application while the application shares a separate network with its database.
But every additional network boundary creates another place where assumptions can fail.
A service can be:
- Reachable from another container but not from the host
- Reachable from the host but blocked by a host firewall
- Reachable on the local network but not through a reverse proxy
- Healthy inside its container while listening on the wrong interface
- Routed through another container while losing the management path I expected
The fix is not to flatten everything into one trusted network. The fix is to identify the exact path being tested.
“The network is down” is rarely a useful diagnosis. I want to know the source, destination, name-resolution path, port, network namespace, published mapping, firewall boundary, and application listener involved.
Started is not the same as ready
One of the easiest Compose mistakes is assuming startup order proves application readiness.
Docker’s current documentation is explicit: Compose can start dependencies in order, but it does not automatically wait for a dependency to become ready. A dependency marked with condition: service_healthy can wait for a defined health check; the short form of depends_on only establishes startup order. (Startup order, depends_on reference)
That distinction matters for databases, identity providers, message queues, and anything else that takes time to become usable after its process starts.
It also changed how I interpret container status.
A running container tells me that a process is running. It does not prove that:
- The application completed initialization
- The database is reachable
- Required storage is mounted
- Authentication works
- A reverse proxy can reach the service
- The user-facing workflow succeeds
Health checks help when they test something meaningful. A check that confirms only that a process exists can still report healthy while the actual service is unusable.
The final test remains the outcome the service exists to provide.
Docker did not eliminate maintenance
Containers made replacement easier, which can create the false impression that updates are automatically low-risk.
They are not.
A new image may change configuration requirements, remove a deprecated option, migrate a database, alter file ownership, introduce a new dependency, or make rollback incompatible with modified application state.
That is why I treat container updates as controlled changes rather than routine restarts. My full process is in The Checklist I Use Before Updating a Container, but the important sequence is straightforward:
- Identify the exact service and intended version.
- Review release notes when the service or data matters.
- Confirm that persistent state is protected.
- Pull and recreate the intended service deliberately.
- Review startup logs and health state.
- Test the user-facing result.
- Preserve a recovery path appropriate to any data migration.
A restart is also not a substitute for recreation. Docker’s current CLI documentation notes that docker compose restart does not apply changes made to the Compose configuration, including changed environment values. The service must be recreated for those changes to take effect.
The ease of replacing a container should reduce recovery time, not reduce caution.
Containers are not an automatic security boundary
Docker provides isolation mechanisms, but configuration determines how much of that boundary remains useful.
Broad writable bind mounts, unnecessary Linux capabilities, privileged mode, exposed management sockets, weak secrets, and overly connected networks can give a container far more authority than the application requires.
Docker’s security guidance recommends removing capabilities that are not explicitly required. Its bind-mount documentation warns that a process in a container can modify host files through writable mounts. Docker also offers rootless mode, which runs both the daemon and containers without root privileges, although whether it fits depends on workload and host requirements. (Docker Engine security, rootless mode)
My baseline questions are therefore:
- Does this port need to be published?
- Does this mount need to be writable?
- Does the whole directory need to be mounted?
- Does the process need to run as root?
- Does the container require added capabilities?
- Does it need access to the Docker socket?
- Which other services must it be able to reach?
- Where are its secrets stored and how are they rotated?
“Runs in Docker” is a deployment fact, not a security conclusion.
I still need a recovery model
A Compose file can recreate container definitions. It cannot recreate data that was never backed up, credentials that were never recorded safely, an external dependency that disappeared, or an image that is no longer available.
A recoverable deployment needs more than YAML:
- A known source for the image
- A deliberate version strategy
- Protected persistent state
- A record of required secrets and configuration
- Documented external dependencies
- A tested restoration sequence
- Validation criteria after restoration
That is why The Definitive Guide to Building a Homelab from Scratch treats recovery and documentation as design requirements rather than cleanup work.
Reproducibility is not the ability to run docker compose up on the original host. It is the ability to explain what must exist before that command can produce a trustworthy service again.
When Docker is not my answer
Docker is my default because defaults reduce unnecessary design variation. It is not a rule because the wrong abstraction creates more operational cost than it removes.
I reconsider containers when:
- The supported vendor deployment is native and the container image is unofficial or abandoned.
- Hardware access becomes fragile or poorly documented.
- The service requires so much host integration that the boundary becomes mostly fictional.
- A virtual machine provides a clearer compatibility, isolation, or recovery model.
- The added storage or network layers make a simple service harder to diagnose.
- The team responsible for the system cannot confidently maintain the container platform.
A native package can be the correct answer. A virtual machine can be the correct answer. A managed service can be the correct answer.
The best deployment model is the one that can be understood, updated, monitored, secured, and recovered by the people who actually own it.
The checklist I use before making Docker the default
Before I commit a service to a container deployment, I want clear answers to these questions:
Image
- Is the image official or maintained by a source I trust?
- Is the version pinned deliberately?
- Are release notes and upgrade instructions available?
State
- What must persist?
- What is disposable?
- What must be backed up together?
- What happens when a mount is absent or empty?
Access
- Which ports must be published?
- Which services must share a network?
- Which mounts can be read-only?
- Which user, group, and capabilities are required?
Operation
- What indicates readiness?
- What does the health check actually prove?
- How will logs be reviewed?
- How will updates be validated?
Recovery
- Can I recreate the service from documentation?
- Can I restore the important state?
- Can I identify dependencies outside Compose?
- Can I prove the recovered service works?
If those answers are weak, Docker has not solved the deployment. It has only moved the uncertainty into a container.
What I would do differently
I would design persistence, readiness, and recovery before the first successful launch.
My earliest deployments usually started with “make it run.” Volumes, backup boundaries, health checks, and update procedures came later. That worked until a migration, failed mount, or broken dependency revealed how much important behavior had remained implicit.
Docker became my default because it helps me describe services consistently.
The benefit does not come from the container label. It comes from using that structure to make dependencies visible, access deliberate, updates controlled, and recovery testable.
That is why Docker remains my default.
And why it will never become my rule.
Sources and further reading
- Docker Compose file reference
- Define and manage volumes in Compose
- Bind mounts
- Define and manage networks in Compose
- Control startup and shutdown order in Compose
- Docker Engine security
- Docker rootless mode
Security note
This article describes general homelab architecture and operational lessons. Real hostnames, addresses, account identifiers, container names, registry locations, internal paths, mount points, network names, credentials, secrets, device mappings, and environment-specific configuration are intentionally omitted or generalized.
AI transparency
AI assisted with structure, copy editing, privacy review, and verification against current Docker documentation. The deployment preferences, failure patterns, and operational conclusions are based on my own homelab experience.