Finally Understood Why Self-Hosting Felt Hard
Finally Understood Why Self-Hosting Felt Hard
Introduction
Self-hosting has become increasingly popular among DevOps engineers, sysadmins, and tech enthusiasts who want control over their data and services. Yet many people find themselves overwhelmed, frustrated, and ultimately giving up on their self-hosting journey. I was one of those people who struggled for years, trying to set up various services like Immich, Nextcloud, and Plex, only to face endless configuration issues, networking problems, and compatibility headaches.
The realization that changed everything for me wasn’t about mastering a specific tool or technology—it was understanding that the true complexity of self-hosting lies in the foundational infrastructure rather than the applications themselves. This comprehensive guide will walk you through the exact insights that transformed my approach to self-hosting, helping you build a robust, scalable, and maintainable homelab that actually works.
Whether you’re a seasoned DevOps professional or someone just starting their self-hosting journey, this guide will provide you with the architectural understanding and practical knowledge needed to overcome the common pitfalls that make self-hosting feel so difficult. We’ll cover everything from networking fundamentals to container orchestration, security best practices, and operational excellence.
Understanding the True Complexity of Self-Hosting
The initial attraction to self-hosting often comes from the desire to run specific applications like Immich for photo management, Nextcloud for file storage, or Home Assistant for smart home automation. However, this application-centric approach quickly reveals its limitations when you encounter issues like:
- Services that work perfectly on your local network but fail when accessed remotely
- Applications that refuse to communicate with each other despite being on the same machine
- Security vulnerabilities that leave your data exposed
- Performance degradation as you add more services
- Configuration files that become increasingly complex and unmanageable
The fundamental issue is that self-hosting isn’t just about running applications—it’s about building and maintaining an entire infrastructure stack. This includes networking, security, storage, monitoring, backups, and orchestration, all of which need to work together seamlessly.
The Infrastructure-First Mindset
The shift from an application-centric to an infrastructure-first mindset is what finally made self-hosting manageable for me. Instead of trying to install Immich directly, I started by asking: “What infrastructure do I need to support this application and the ones that will follow?”
This approach led me to design a modular, scalable infrastructure that could support dozens of services without becoming unmanageable. The key insight was that once you have solid infrastructure foundations, adding new services becomes trivial—you’re essentially just deploying another container with minimal configuration.
Common Misconceptions About Self-Hosting
Many people approach self-hosting with misconceptions that lead to frustration:
The “Easy Setup” Myth: Many applications advertise “one-click” or “simple setup” installations, but these rarely work in complex home network environments.
The “I’ll Figure It Out” Approach: Diving into installations without understanding the underlying infrastructure leads to a patchwork of solutions that don’t work well together.
The “Free Software” Expectation: While the software itself may be free, the time and expertise required to properly set up and maintain a self-hosted environment has significant costs.
The “I Can Do It All Myself” Syndrome: Trying to handle every aspect of infrastructure, security, and operations without leveraging existing tools and best practices.
Understanding these misconceptions helps you approach self-hosting with realistic expectations and a proper strategy.
Prerequisites for Successful Self-Hosting
Before diving into specific installations, it’s crucial to establish the right foundation. Here are the prerequisites that will set you up for success:
Hardware Requirements
The hardware you choose will significantly impact your self-hosting experience. While you can start small, understanding your requirements upfront prevents painful upgrades later.
Minimum Viable Setup:
- CPU: Modern multi-core processor (Intel i5 or AMD Ryzen 5 equivalent)
- RAM: 8GB DDR4 (16GB recommended for multiple services)
- Storage: 256GB SSD for OS and containers, plus additional storage for data
- Network: Gigabit Ethernet connection
- Power: Reliable UPS for power protection
Recommended Production Setup:
- CPU: Intel i7 or AMD Ryzen 7+ with AES-NI support
- RAM: 32GB DDR4 (64GB for heavy workloads)
- Storage: 1TB NVMe SSD for OS/containers, multiple HDDs/SSDs for data storage (RAID configuration recommended)
- Network: Gigabit Ethernet with dual ports for redundancy
- Power: Enterprise-grade UPS with sufficient runtime
Software Stack Requirements
A successful self-hosting environment requires a carefully selected software stack:
Operating System:
- Linux (Ubuntu Server LTS 22.04+ recommended for stability)
- Docker Engine 20.10+ with Docker Compose v2.20+
- Containerd as the container runtime
- ZFS or Btrfs for storage management (optional but recommended)
Network Services:
- WireGuard or OpenVPN for secure remote access
- Traefik or Nginx for reverse proxying
- DNS server (CoreDNS or dnsmasq)
- Firewall management (UFW or iptables)
Monitoring and Management:
- Prometheus + Grafana for monitoring
- Portainer or Docker Compose UI for management
- Automated backup solutions
- Log aggregation (ELK stack or similar)
Network Infrastructure Considerations
Your network setup is critical for self-hosting success. Consider these factors:
Network Segmentation:
- Separate VLANs for different service types (IoT, personal data, public services)
- Proper firewall rules between segments
- Guest network isolation for visitors
Static IP Addressing:
- Reserve static IPs for your self-hosted services
- Configure proper DNS records for internal resolution
- Set up dynamic DNS for external access if needed
Port Forwarding and DMZ:
- Carefully configure port forwarding for external access
- Consider using a DMZ for services exposed to the internet
- Implement proper firewall rules to limit exposure
Security Foundation
Security should be built into your infrastructure from the start, not added as an afterthought:
Authentication and Authorization:
- Implement centralized authentication (LDAP or similar)
- Use role-based access control (RBAC)
- Enable two-factor authentication where possible
Encryption:
- Use HTTPS everywhere with valid certificates (Let’s Encrypt)
- Encrypt sensitive data at rest
- Implement secure communication between services
Backup and Disaster Recovery:
- Regular automated backups of all critical data
- Off-site backup storage for disaster recovery
- Documented recovery procedures
Building Your Self-Hosting Infrastructure
With the prerequisites established, let’s build a solid infrastructure foundation. This section covers the core components that will make adding new services straightforward.
Network Configuration with WireGuard
WireGuard has revolutionized secure networking for self-hosting. Here’s how to set it up properly:
1
2
3
4
5
6
7
8
9
10
# Install WireGuard on Ubuntu/Debian
sudo apt update
sudo apt install wireguard
# Generate server keys
wg genkey | sudo tee /etc/wireguard/server-private.key
wg genkey | sudo tee /etc/wireguard/server-public.key
# Create the WireGuard configuration
sudo nano /etc/wireguard/wg0.conf
1
2
3
4
5
6
7
8
9
10
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <server-private-key>
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey = <client-public-key>
AllowedIPs = 10.0.0.2/32
Key Benefits of WireGuard:
- Simpler configuration than OpenVPN
- Better performance and lower latency
- Built-in roaming support
- Strong cryptography with modern algorithms
- Easy to set up on mobile devices
Reverse Proxy Configuration with Traefik
A reverse proxy is essential for managing multiple services and providing HTTPS:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# docker-compose.yml for Traefik
version: '3.8'
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/traefik.yml
- ./acme.json:/acme.json
networks:
- web
networks:
web:
external: true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# traefik.yml
api:
dashboard: true
entryPoints:
http:
address: ":80"
https:
address: ":443"
certificatesResolvers:
letsEncrypt:
acme:
email: your@email.com
storage: acme.json
httpChallenge:
entryPoint: http
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
watch: true
exposedByDefault: false
Docker Compose Structure
Organizing your Docker Compose files properly prevents configuration chaos:
1
2
mkdir -p /opt/docker
cd /opt/docker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/opt/docker/
├── traefik/
│ ├── docker-compose.yml
│ ├── traefik.yml
│ └── acme.json
├── services/
│ ├── immich/
│ │ ├── docker-compose.yml
│ │ └── .env
│ ├── nextcloud/
│ │ ├── docker-compose.yml
│ │ └── .env
│ └── portainer/
│ ├── docker-compose.yml
│ └── .env
└── shared/
├── nginx/
│ ├── conf.d/
│ └── logs/
└── data/
├── immich/
├── nextcloud/
└── portainer/
Storage Management with ZFS
Proper storage management prevents data loss and improves performance:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Install ZFS
sudo apt install zfsutils-linux
# Create ZFS pool
sudo zpool create -O compression=lz4 -O atime=off data /dev/sdb
# Create datasets for different services
sudo zfs create -o mountpoint=/opt/docker/shared/data/immich data/immich
sudo zfs create -o mountpoint=/opt/docker/shared/data/nextcloud data/nextcloud
sudo zfs create -o mountpoint=/opt/docker/shared/data/portainer data/portainer
# Set up automatic snapshots
sudo apt install zfs-auto-snapshot
sudo dpkg-reconfigure zfs-auto-snapshot
Service Configuration and Deployment
With the infrastructure in place, deploying services becomes straightforward. Let’s configure a few common services to demonstrate the approach.
Immich Photo Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# /opt/docker/services/immich/docker-compose.yml
version: '3.8'
services:
db:
image: postgres:15-alpine
container_name: immich-db
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- /opt/docker/shared/data/immich/postgres:/var/lib/postgresql/data
networks:
- immich
redis:
image: redis:7-alpine
container_name: immich-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- /opt/docker/shared/data/immich/redis:/data
networks:
- immich
app:
image: altran1502/immich-server:latest
container_name: immich-app
restart: unless-stopped
environment:
- DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@db:5432/${DB_NAME}
- REDIS_URL=redis://redis:6379
- UPLOAD_PATH=/app/upload
- CACHE_PATH=/app/cache
- THUMBNAIL_PATH=/app/thumbnail
- DOCKER=true
- PORT=3001
- LOG_LEVEL=info
volumes:
- /opt/docker/shared/data/immich/upload:/app/upload
- /opt/docker/shared/data/immich/cache:/app/cache
- /opt/docker/shared/data/immich/thumbnail:/app/thumbnail
networks:
- immich
depends_on:
- db
- redis
worker:
image: altran1502/immich-server:latest
container_name: immich-worker
restart: unless-stopped
environment:
- DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@db:5432/${DB_NAME}
- REDIS_URL=redis://redis:6379
- UPLOAD_PATH=/app/upload
- CACHE_PATH=/app/cache
- THUMBNAIL_PATH=/app/thumbnail
- DOCKER=true
- PORT=3002
- LOG_LEVEL=info
- WORKER=true
volumes:
- /opt/docker/shared/data/immich/upload:/app/upload
- /opt/docker/shared/data/immich/cache:/app/cache
- /opt/docker/shared/data/immich/thumbnail:/app/thumbnail
networks:
- immich
depends_on:
- db
- redis
nginx:
image: altran1502/immich-nginx:latest
container_name: immich-nginx
restart: unless-stopped
ports:
- "3000:3000"
environment:
- IMMICH_SERVER_PORT=3001
volumes:
- /opt/docker/shared/data/immich/upload:/app/upload
- /opt/docker/shared/data/immich/cache:/app/cache
- /opt/docker/shared/data/immich/thumbnail:/app/thumbnail
networks:
- immich
depends_on:
- app
- worker
labels:
- "traefik.enable=true"
- "traefik.http.routers.immich.rule=Host(`photos.yourdomain.com`)"
- "traefik.http.routers.immich.tls.certresolver=letsEncrypt"
- "traefik.http.services.immich.loadbalancer.server.port=3000"
networks:
immich:
driver: bridge
Nextcloud File Storage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# /opt/docker/services/nextcloud/docker-compose.yml
version: '3.8'
services:
db:
image: mariadb:10.11
container_name: nextcloud-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: nextcloud
MYSQL_USER: nextcloud
MYSQL_PASSWORD: ${NEXTCLOUD_DB_PASSWORD}
volumes:
- /opt/docker/shared/data/nextcloud/mysql:/var/lib/mysql
networks:
- nextcloud
app:
image: nextcloud:25-fpm-alpine
container_name: nextcloud-app
restart: unless-stopped
environment:
- MYSQL_HOST=db
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- MYSQL_PASSWORD=${NEXTCLOUD_DB_PASSWORD}
- NEXTCLOUD_DATA_DIR=/var/www/html/data
- NEXTCLOUD_TRASHBIN_RETENTION_OBLIGATION=false
- NEXTCLOUD_UPDATE_AUTO=true
volumes:
- /opt/docker/shared/data/nextcloud/html:/var/www/html
- /opt/docker/shared/data/nextcloud/data:/var/www/html/data
- /opt/docker/shared/data/nextcloud/config:/var/www/html/config
- /opt/docker/shared/data/nextcloud/themes:/var/www/html/themes
- /opt/docker/shared/data/nextcloud/apps:/var/www/html/custom_apps
networks:
- nextcloud
depends_on:
- db
labels:
- "traefik.enable=true"
- "traefik.http.routers.nextcloud.rule=Host(`files.yourdomain.com`)"
- "traefik.http.routers.nextcloud.tls.certresolver=letsEncrypt"
- "traefik.http.services.nextcloud.loadbalancer.server.port=9000"
cron:
image: nextcloud:25-fpm-alpine
container_name: nextcloud-cron
restart: unless-stopped
volumes:
- /opt/docker/shared/data/nextcloud/html:/var/www/html
- /opt/docker/shared/data/nextcloud/data:/var/www/html/data
- /opt/docker/shared/data/nextcloud/config:/var/www/html/config
- /opt/docker/shared/data/nextcloud/themes:/var/www/html/themes
- /opt/docker/shared/data/nextcloud/apps:/var/www/html/custom_apps
networks:
- nextcloud
command: /usr/local/bin/entrypoint.sh cron
depends_on:
- app
nginx:
image: nextcloud:25-fpm-alpine
container_name: nextcloud-nginx
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- /opt/docker/shared/data/nextcloud/html:/var/www/html
- /opt/docker/shared/data/nextcloud/data:/var/www/html/data
- /opt/docker/shared/data/nextcloud/config:/var/www/html/config
- /opt/docker/shared/data/nextcloud/themes:/var/www/html/themes
- /opt/docker/shared/data/nextcloud/apps:/var/www/html/custom_apps
networks:
- nextcloud
depends_on:
- app
labels:
- "traefik.enable=true"
- "traefik.http.routers.nextcloud-public.rule=Host(`files.yourdomain.com`)"
- "traefik.http.routers.nextcloud-public.tls.certresolver=letsEncrypt"
- "traefik.http.services.nextcloud-public.loadbalancer.server.port=8080"
networks:
nextcloud:
driver: bridge
Advanced Configuration and Optimization
Once you have basic services running, you can optimize your infrastructure for better performance, security, and manageability.
Security Hardening
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Configure UFW firewall
sudo apt install ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow from 10.0.0.0/24 to any port 22
sudo ufw enable
# Create a non-root user for Docker operations
sudo groupadd docker
sudo usermod -aG docker $USER
# Configure fail2ban for SSH protection
sudo apt install fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
Performance Optimization
1
2
# Enhanced Docker daemon configuration
sudo nano /etc/docker/daemon.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2",
"storage-opts": [
"overlay2.override_kernel_check=true"
],
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 64000,
"Soft": 64000
}
},
"experimental": false