Mission summary.
A hardened private infrastructure node built from a donated mini PC.
eal-core01 is the first self-hosted infrastructure node for Endurance Aero Labs — a private cloud for engineering files, coursework, and project documentation, reachable from my Windows PC, iPhone, and iPad without ever touching the public internet.
It started as a free, retired HP EliteDesk 800 G3 Mini. The interesting part of the project was not "stand up a file server" — it was doing it under real constraints: a tight budget, a single internal drive bay, and a hard rule that the machine must never be exposed to the open internet or to untrusted Wi-Fi and LAN clients.
The v1 architecture answers those constraints directly: an internal SSD for live storage, a Dockerized Nextcloud stack for daily file access, Tailscale for a private encrypted network, Tailscale Serve for HTTPS, and a deny-by-default firewall with the application backend bound to localhost. The deliverable is a documented build — storage layout, access-control model, firewall policy, containerization, and a staged backup plan — not just a box that happens to serve files.
Constraints & requirements.
Real-world constraints drove the architecture more than raw specs.
- REQ-001 · Tailnet Access. The eal-core01 infrastructure node shall provide user-facing cloud services only to devices authenticated to the authorized Tailnet.
- REQ-002 · Public Exposure Prevention. The eal-core01 infrastructure node shall not require public router port forwarding, Tailscale Funnel, or public DNS exposure for normal user access.
- REQ-003 · Untrusted Network Isolation. The eal-core01 infrastructure node shall prevent unauthenticated devices on the local area network, open Wi-Fi networks, or the public internet from directly accessing the Nextcloud web service.
- REQ-004 · Client File Access. The eal-core01 infrastructure node shall provide file access through standard Nextcloud clients on Windows, IOS, and Linux
- REQ-005 · Localhost Backend Binding. The Nextcloud web backend shall bind to
127.0.0.1:8080so that the raw application port is not directly exposed on the local area network or Tailnet. - REQ-006 · Live Data Partitioning. The eal-core01 infrastructure node shall store active Nextcloud user data on a dedicated
/srvdata partition separate from the operating-system root partition. - REQ-007 · Containerized Service Deployment. The eal-core01 infrastructure node shall deploy Nextcloud, MariaDB, Redis, and Nextcloud cron as Docker Compose services.
- REQ-008 · DNS Filtering Service. The eal-core01 infrastructure node shall provide DNS-level filtering for Tailnet-connected client devices using AdGuard Home.
- REQ-009 · Backup Role Separation. The external 4 TB drive shall be used as a backup target and shall not be treated as primary live storage until backup execution and restore verification are completed.
- REQ-010 · Verification Evidence. The eal-core01 infrastructure node shall maintain verification evidence for service status, firewall posture, access-path behavior, storage layout, client connectivity, DNS filtering behavior, and backup/restore status.
System architecture.
The v1 access path: authorized device to localhost-bound backend.
127.0.0.1:8080 — so the raw Nextcloud service is never exposed on the LAN,
the public internet, or even a direct Tailnet HTTP port.
Decision package.
Each decision scored against cost, failure modes, access risk, and backup needs.
The guiding tradeoff was a reliable, recoverable v1 over an expensive multi-drive NAS. Every row below is a decision I can defend, with the open item that still has to close before I'd call it done.
| Decision Area | Selected Baseline | Why It Won | Open Closure Item |
|---|---|---|---|
| Operating System | Ubuntu Server 24.04 LTS | Best fit for a general-purpose node running Docker, Tailscale, and Nextcloud on mini-PC hardware, with long-term support. | Evaluate Ubuntu Pro + unattended security upgrades once backup is live. |
| Storage Layout | Internal SSD live + external HDD backup | Keeps live data on fast, reliable internal storage instead of a USB drive, while still using cheap high-capacity HDD for backup. | Mount WD Black 4 TB as backup target; verify restore. |
| Remote Access | Tailscale overlay only | Encrypted private access with zero router port forwarding and no public DNS exposure. | Keep the Tailnet device list controlled; never enable Funnel. |
| Web Frontend | Tailscale Serve HTTPS | Valid HTTPS inside the Tailnet while the Docker backend stays bound to localhost. | Maintain trusted-domain / proxy config if the hostname changes. |
| NAS Platform | TrueNAS / OMV deferred | Considered, but the single-bay mini PC favored flexible Docker services over a ZFS storage appliance. | Revisit if a multi-bay chassis is acquired. |
These are architecture-level decisions, not vendor lock-ins. The goal was to reduce risk before committing to a build.
Access control.
What's intentionally open, what's intentionally closed, what's still hardening.
The security boundary is a private overlay network, not a public firewall ruleset.
Nextcloud is reachable only by devices authorized in the Tailnet. The browser-facing service is
HTTPS via Tailscale Serve; the Docker backend listens only on 127.0.0.1:8080. Defense
comes from not being reachable in the first place, with the firewall as a second layer.
- No public port forwarding. The router has no inbound rules for this service.
- No Tailscale Funnel. Deliberately avoided — Funnel would publish the service to the open internet.
- Deny-by-default firewall. UFW drops incoming traffic by default; only
tailscale0is allowed. - Localhost-only backend. Docker binds Nextcloud to
127.0.0.1:8080, blocking direct LAN or Tailnet HTTP access to the raw app. - Reverse-proxy correctness. Trusted proxies, forwarded headers, HTTPS overwrite, and official host/CLI URLs configured so the proxied app behaves correctly and securely.
- Data-directory ownership. The Nextcloud data directory is owned by the container web user, preventing casual edits from the normal Linux account.
Live data & backup plan.
Why live storage and backup storage are deliberately separate roles.
The 2 TB SSD is the live device, partitioned deliberately: a root partition for the OS,
Docker, applications, and logs, and a separate /srv partition for served data so
user uploads can never fill the OS partition. Active files live under
/srv/nextcloud-data.
The 4 TB external HDD is reserved strictly for backup and restore validation. I'm intentionally not using it as extra live capacity — live storage without backup just builds a bigger single point of failure. The closure gate for this project is a working, tested restore, not just a working server.
- Live: internal SSD,
/srv/nextcloud-data - App / config:
/opt/eal/nextcloud - Backup target: WD Black 4 TB, dedicated backup volume
- Backup scope: Nextcloud data, Compose files, env file, app volume, MariaDB dump, restore notes
Build & test log.
What was verified before treating the node as real storage.
OS installed with deliberate partition layout
Fresh Ubuntu Server on the 2 TB SSD: EFI boot, root, and a dedicated /srv
live-data partition — separation enforced at install time, not retrofitted.
Container stack deployed & verified
Docker and Compose installed; MariaDB, Redis, the Nextcloud app, and the cron container brought up and confirmed healthy.
CLOSEDTailnet-only HTTPS established
Configured Tailscale Serve for a Tailnet-only HTTPS URL and moved the raw Docker backend to localhost-only binding — closing off LAN and direct-port access.
CLOSEDClients connected & upload path confirmed
Windows, iPhone, and iPad clients connected — Windows using virtual files to avoid pulling the
full dataset onto limited local storage. Verified a test upload landed under the
/srv-backed data directory.
Access-control behavior verified
Confirmed the HTTPS URL works on-Tailnet and times out when a device leaves the Tailnet — proving the access boundary holds, not just that the happy path works.
CLOSEDBackup & restore verification
Mount the 4 TB drive, write a backup procedure, run the first backup, and prove a restore. Until a restore is demonstrated, the node is "working," not "trustworthy."
OPENWhat I actually did.
End-to-end ownership, framed as transferable engineering skill.
I owned this node end-to-end: hardware selection under budget, OS install and partitioning, container deployment, Tailnet-only HTTPS, firewall hardening, client onboarding, and the backup architecture. More importantly, I treated a hobby-sounding task like an engineering project — requirements first, decisions documented, behavior verified.
- Systems engineering. Turned a vague "private cloud" idea into requirements, constraints, tradeoffs, and a staged architecture with explicit closure gates.
- Linux administration. Ubuntu Server, SSH, UFW, partitioning, permissions, and service directory layout.
- Containerization. Nextcloud with MariaDB, Redis, and cron via Docker Compose.
- Network security. Overlay-only access, no port forwarding, no Funnel, localhost-bound backend, reverse-proxy correctness.
- Operational discipline. Live/backup separation and a verification-before-trust posture — including the open restore gate I refuse to skip.
The honest framing: this is operational v1, not a finished product. Its value is that it's a documented, defensible build — the same systems-engineering habits I apply to avionics work, demonstrated on infrastructure I run every day.
End of brief. This node is operational v1 — backup and restore verification is the next closure gate.