In 2024 I set up a home server around a refurbished Dell OptiPlex 7050 Micro. The machine cost about $140; the SSD it came with had bad sectors, so I replaced it for roughly $35. I already had a Synology NAS with two 4 TB drives in RAID for my photo storage and backups, and a UPS to ride through the short power interruptions common where I live.

I wanted more control over my files, media, passwords, and reading list. Docker made getting those services running straightforward. Deciding how much of my daily life should depend on that machine was the harder engineering question.

Revision, September 2026: The original article included my large Compose file. I have replaced it with this retrospective because a runnable configuration is not necessarily a safe recommendation. The setup above is historical; the changes and checks below describe how I would evaluate it, not a claim that I have implemented or tested every improvement.

Start with the cost of failure

The attraction was a small, inexpensive machine that could run several useful services. The tradeoff was making one host a dependency for all of them.

A media library and a password manager do not have the same recovery requirements. A dashboard outage is inconvenient; losing the only copy of family photos is not an acceptable experiment. I would classify services by the data they hold, the consequence of downtime, and the recovery steps they require before adding more containers.

For each service, the useful questions are:

  • What is the authoritative data, and where does it live?
  • Which dependencies must be available before it can start?
  • What access does it have to other services and to the host?
  • How would I recover it if the SSD failed tonight?

That is a more useful inventory than a list of container names.

Make network boundaries explicit

My original stack published many ports to the host. In Docker, a published port with no host IP normally binds on all host interfaces. That does not prove it is reachable from the internet: routing, the host firewall, and router configuration also matter. It does mean the service may be reachable from networks where I did not intend to offer it.

The design I would use is to expose only the entry points that actually need clients. Databases and internal APIs do not need published host ports simply to communicate with another container on a shared Docker network. Management interfaces deserve a separate access decision, not the same exposure as the application they administer.

Inside a Compose network, use the service name and container port, not a container’s current IP address. Compose provides service-name discovery; an IP can change when a container is recreated. Networks should also follow trust boundaries rather than connecting every service to every other service by default.

A reverse proxy can centralize TLS and routing. It does not, by itself, add application authorization, secure an exposed admin interface, or make a vulnerable upstream service safe.

See Docker’s documentation on Compose networking and port publishing for the behavior behind these choices.

Treat credentials and host access as part of the design

The old example included default passwords and a Plex claim-token value. They should not have been in a published configuration. Removing a value from an article does not invalidate it or remove it from repository history; any credential that remains valid needs to be rotated or revoked separately.

A safe setup should require unique credentials before a service becomes accessible, rather than asking the reader to remember to replace defaults later. Keep credentials out of version control. Where an application supports file-based secrets, Compose secrets can provide a scoped file mount, but they are not automatically an encrypted secret-management system. The source files and host still need protection. Moving a password into an environment variable also does not make it secret from everyone with container-management access.

The Docker socket is another important boundary. My stack mounted it into management and monitoring tools. Access to a rootful Docker daemon’s API can effectively grant control of the host. Mounting the socket read-only does not make the API itself read-only. A dashboard should receive that access only if its function genuinely requires it and the consequences are understood.

Docker documents these boundaries in its secrets guidance and daemon security discussion.

Separate availability from recoverability

My NAS used RAID, and the UPS helped with brief power loss. Those solve different problems from backup and recovery.

RAID is not a backup. Redundancy can help keep storage available after a drive failure; it does not provide an independent recovery copy after accidental deletion, corruption, ransomware, or loss of the NAS. I would want versioned backups, a copy outside the same failure domain, and a tested restore process. Credentials or keys needed to restore encrypted data must be recoverable too.

A database volume is not automatically a consistent backup just because its files were copied. Use the database’s supported backup mechanism, or a coordinated snapshot procedure with understood consistency guarantees. If an application also stores uploaded files separately, plan how to restore those alongside the database.

A UPS is time to act, not unlimited availability. Its value depends on the actual load and battery condition. For a longer outage, the system needs a graceful-shutdown path and an understood restart order. Restoring power is not the same as restoring a usable application.

The meaningful test is not whether a backup job reports success. It is whether I can recover the service and its data on another disk or host, without depending on the failed system for the instructions or keys.

Updates need a recovery path

Many images in my original file used mutable tags such as latest. That makes the deployed version harder to identify and a later pull less predictable.

Pinning an explicit version or digest makes a deployment more reproducible, but it is not a security-update strategy. A pinned vulnerable image stays vulnerable. I would keep a deliberate update process: read the release notes, identify migrations, take an appropriate backup, update, and check behavior from a client.

Rolling the image back may not roll the data back. If an application changes its database schema or file format, recovery can require a compatible data restore rather than simply selecting the previous image.

Likewise, restart: unless-stopped can restart a process; it cannot establish that the database is ready, that authentication works, or that the files the user needs are intact. Health checks and startup dependencies need to reflect the service’s actual requirements.

Checks I would run before depending on the server

This is a verification plan, not a report of completed tests. I would use disposable data or an isolated restore environment for destructive scenarios.

ScenarioWhat I would verify
Reach the host from another networkOnly intended entry points are reachable; administrative interfaces and databases are not accidentally exposed.
Recreate a containerClients reconnect through service names rather than relying on an old container IP.
Restart the hostDependencies recover, authentication works, and an actual client can read and write expected data.
Restore onto a clean disk or hostBackups include the required database, files, configuration, and keys; the recovered data is usable.
Rehearse an application updateExpected behavior still works, and the recovery procedure accounts for data migrations.
Exercise the power-loss procedure safelyShutdown happens before battery exhaustion, and restart behavior is understood.
Lose the dashboard or monitoring hostI can still diagnose and recover the underlying services without that interface.

What I would keep

I still like the premise: an inexpensive machine, a small set of useful services, and the freedom to understand how they work. The mistake is treating the ease of adding a container as evidence that it is cheap to own.

My recommendation is to start with fewer services and make one complete recovery path work before adding more. Self-hosting gives you control over the system. It also gives you responsibility for the boundaries and failure modes that a hosted service used to manage for you.