A self-hosted homelab running on a repurposed notebook, built as a functional development infrastructure and a hands-on portfolio for roles in Linux system administration and cybersecurity.
This repository documents my home server setup: Docker Compose stacks, maintenance scripts, and architectural decisions. The project serves a dual purpose — real personal utility (development databases, container management, monitoring) and demonstrable portfolio evidence of infrastructure skills.
Philosophy: Direct control over every layer. No managed platforms, no unnecessary abstraction. Understand the why behind every command.
| Component | Details |
|---|---|
| Model | Samsung NP350XAA (headless) |
| CPU | Intel Core i5-8250U |
| RAM | ~3.7 GB usable (constrained env) |
| Storage | 1 TB SSD |
| Partition | LVM (55 GB root · 831 GB /home) |
| Encryption | LUKS2 full-disk encryption |
Running headless on Debian 13 (Trixie). SSH access via Tailscale VPN.
All services are orchestrated with Docker Compose
(v5.1.2 plugin — docker compose, not docker-compose).
homelab/
├── docker-compose/
│ ├── compose.dev.yml # Active dev stack
│ └── docker-compose.yml # Legacy stack (archived)
└── scripts/
└── update-all.sh # Full system update script
| Service | Image | Port | Role |
|---|---|---|---|
| PostgreSQL | postgres:16 |
5432 |
Primary relational database |
| Redis | redis:7 |
6379 |
Cache / message broker |
| Portainer CE | portainer/portainer-ce |
9443 |
Container management UI |
All containers run with restart: unless-stopped and use named volumes
for data persistence.
The server also provides a pre-configured environment for development projects:
| Tool | Version |
|---|---|
| Java | 21 LTS |
| Python | 3.13 |
| Go | 1.24 |
| Node.js | LTS |
- VPN: Tailscale (primary remote access layer)
- Firewall: UFW
- Management UI: Cockpit (port
9090) - Client SSH alias:
homelab→ Tailscale IP (Fedora client machine)
External access is handled entirely through Tailscale — no open ports on the public internet.
Single-command full system update. Run with:
update-allCovers:
| Layer | Tool | Behavior on missing tool |
|---|---|---|
| OS packages | apt |
— |
| Python tools | pipx upgrade-all |
warns and skips |
| Node.js | npm update -g |
warns and skips |
| Go binaries | go install @latest per bin |
warns and skips |
| Docker images | docker pull |
warns and skips |
| Compose stack | docker compose pull && up -d |
warns and skips |
The script uses set -euo pipefail with || warn fallbacks — unexpected errors
stop execution but missing optional tools are skipped gracefully.
- Named Docker volumes are used for all persistent service data
- Bind mounts were intentionally avoided — they caused UID/GID conflicts
in past deployments (Apache running as
www-data, UID 33) - Portainer volume declared as
external: trueto preserve configuration state across stack redeployments
K3s was evaluated and removed. At this hardware scale (~3.7 GB usable RAM), K3s alone consumed ~600 MB with no justified benefit. Docker Compose was chosen for:
- RAM efficiency — critical in a memory-constrained environment
- Direct control — no Kubernetes abstraction layer between operator and containers
- Portfolio signal — demonstrates deliberate architectural reasoning over cargo-culting orchestration tools
Despite the operational overhead (manual passphrase entry on boot), LUKS2 encryption was kept because:
- Protects data-at-rest on a physically accessible device
- Demonstrates security-conscious infrastructure design
- Relevant to a cybersecurity-focused portfolio
Bind mounts caused 403 Forbidden errors in past deployments due to UID/GID
mismatches between the host and container processes. Named volumes delegate
ownership to Docker, resolving permission issues cleanly.
| Problem | Root Cause | Solution |
|---|---|---|
Apache 403 Forbidden |
Bind mount UID/GID conflict | Switched to named Docker volumes |
| Port conflict on deploy | Pre-existing nginx on port 8080 | Audit ports with ss -tlnp before deploying |
docker-compose not found |
Plugin version uses space | Use docker compose (v5.1.2 plugin) |
| Script silently stopping | set -euo pipefail + find returning exit 1 on missing dir |
Guard directory existence before find |
| Containers not persisting data | Missing volume declarations on docker run |
Migrated to Compose with explicit named volumes |
- Migrate from K3s to Docker Compose
- Configure Tailscale VPN access
- Deploy dev stack (PostgreSQL, Redis, Portainer)
- Migrate containers from
docker runto Compose with named volumes - Write
update-allmaintenance script - Configure HTTPS via Tailscale for the full stack
- Set up automated backups for named volumes
- Harden UFW rules and document firewall policy
- Add monitoring/observability layer (Uptime Kuma or Prometheus stack)
- OS: Debian 13 (Trixie), headless — hostname
DebianHomeServer - Containerization: Docker + Compose plugin v5.1.2
- VCS: Git + GitHub (SSH key authentication via ed25519)
This is a personal homelab for learning and portfolio purposes. Configuration values (IPs, credentials, domain names) are intentionally omitted or anonymized in this repository. Never commit secrets to version control.
Built and maintained by Matheus Costa de Jesus · LinkedIn