21 KiB
🏠 Home Server — Docker Compose Stack
Self-hosted infrastructure running Home Assistant, Gitea, and WireGuard VPN behind Nginx Proxy Manager with Let's Encrypt SSL and IONOS DynDNS.
| Service | Domain | Internal Port | Purpose |
|---|---|---|---|
| Nginx Proxy Manager | — (admin: :81) |
80, 443, 81 | Reverse proxy + SSL |
| Home Assistant | ha.doerflingers.com |
8123 | Smart home |
| Gitea | git.doerflingers.com |
3000 (web), 22 (SSH) | Git hosting |
| WireGuard | home.doerflingers.com |
51820/UDP | VPN |
Table of Contents
- Hardware Requirements
- Prerequisites — Ubuntu 24.04 Setup
- IONOS DynDNS Setup
- FritzBox Port Forwarding
- Project Setup
- Launch the Stack
- Nginx Proxy Manager Configuration
- Service-Specific Setup
- WireGuard Client Setup
- Backup & Restore
- Migration to New Hardware
- Troubleshooting
- Architecture Overview
1. Hardware Requirements
This stack is designed to run on modest hardware. Here's the assessment for the target laptop:
| Resource | Available | Used (est.) | Status |
|---|---|---|---|
| CPU | i5-3210M (2C/4T, 2.5 GHz) | ~5-15% idle | ✅ |
| RAM | 3.7 GB + 3.7 GB Swap | ~1.3 GB | ✅ |
| Disk | 500 GB HDD | ~5 GB base + data | ✅ |
All services coexist without interference. HTTP traffic is multiplexed by hostname through the reverse proxy, WireGuard uses a separate UDP port.
2. Prerequisites — Ubuntu 24.04 Setup
Since this is a fresh Ubuntu 24.04 install, we need to install all required packages from scratch.
2.1 System Update
sudo apt update && sudo apt upgrade -y
2.2 Install Essential Packages
sudo apt install -y \
ca-certificates \
curl \
gnupg \
lsb-release \
python3 \
python3-pip \
python3-venv \
git \
cron
2.3 Install Docker Engine
Follow the official Docker installation for Ubuntu. Do not use the docker.io snap package.
# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine + Compose plugin
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
2.4 Post-Installation — Run Docker Without sudo
sudo usermod -aG docker $USER
newgrp docker
Verify the installation:
docker --version
docker compose version
2.5 Prevent Laptop Suspend on Lid Close
Since this is a laptop acting as a server, you want it to stay on when the lid is closed:
sudo sed -i 's/#HandleLidSwitch=suspend/HandleLidSwitch=ignore/' /etc/systemd/logind.conf
sudo sed -i 's/#HandleLidSwitchExternalPower=suspend/HandleLidSwitchExternalPower=ignore/' /etc/systemd/logind.conf
sudo systemctl restart systemd-logind
2.6 (Optional) Enable WireGuard Kernel Module
WireGuard often needs the kernel module pre-loaded:
sudo apt install -y wireguard-tools
sudo modprobe wireguard
# Make it persistent across reboots
echo "wireguard" | sudo tee /etc/modules-load.d/wireguard.conf
3. IONOS DynDNS Setup
Your domains are registered at IONOS. Since your home internet has a dynamic IP address, you need DynDNS to keep the DNS records updated automatically.
3.1 Install the IONOS DynDNS Client
# Create a virtual environment (required on Ubuntu 24.04 which uses PEP 668)
sudo python3 -m venv /opt/dyndns-venv
sudo /opt/dyndns-venv/bin/pip install domain-connect-dyndns
3.2 Register Each Domain
You need to run the setup command once per domain/subdomain. Each will give you a URL to open in your browser to authorize IONOS.
# Domain for Home Assistant
sudo /opt/dyndns-venv/bin/domain-connect-dyndns setup --domain ha.doerflingers.com
# → A URL is printed. Open it in your browser.
# → Log in to IONOS, click "Erlauben" (Allow).
# → Copy the code shown and paste it back into the terminal.
# Domain for Gitea
sudo /opt/dyndns-venv/bin/domain-connect-dyndns setup --domain git.doerflingers.com
# → Same process: open URL, authorize, paste code.
# Domain for Home / VPN
sudo /opt/dyndns-venv/bin/domain-connect-dyndns setup --domain home.doerflingers.com
# → Same process: open URL, authorize, paste code.
3.3 Test the Update
sudo /opt/dyndns-venv/bin/domain-connect-dyndns update --all
You should see output like:
Read ha.doerflingers.com config.
IP x.x.x.x found in A record
New IP: x.x.x.x
A record up to date.
3.4 Set Up Automatic Updates via Cron
The cron job runs every 5 minutes to check for IP changes and update DNS if needed.
# Edit the root crontab
sudo crontab -e
Add this line at the bottom:
*/5 * * * * /usr/bin/flock -n /tmp/dyndns-update.lck /opt/dyndns-venv/bin/domain-connect-dyndns update --all >> /var/log/dyndns-update.log 2>&1
Note: We use every 5 minutes instead of every 1 minute — it's unlikely your IP changes more frequently, and it reduces system load. The
flockprevents overlapping runs.
3.5 Verify Cron is Running
sudo systemctl enable cron
sudo systemctl start cron
# Check it's scheduled
sudo crontab -l
4. FritzBox Port Forwarding
You need to forward specific ports from your FritzBox router to the laptop.
4.1 Find the Laptop's Local IP Address
ip addr show | grep "inet " | grep -v 127.0.0.1
Note the IP address (e.g., 192.168.178.XX).
Tip: In FritzBox, go to Heimnetz → Netzwerk → [Your Laptop] and enable "Diesem Netzwerkgerät immer die gleiche IPv4-Adresse zuweisen" (always assign the same IP) to prevent the IP from changing.
4.2 Configure Port Forwarding
In the FritzBox admin UI (http://fritz.box), go to Internet → Freigaben → Portfreigaben and add the following rules:
| Service | Protocol | External Port | Internal Port | Internal IP | Required |
|---|---|---|---|---|---|
| HTTP | TCP | 80 | 80 | 192.168.178.XX | ✅ Yes (Let's Encrypt HTTP-01 challenge) |
| HTTPS | TCP | 443 | 443 | 192.168.178.XX | ✅ Yes (all web services) |
| WireGuard | UDP | 51820 | 51820 | 192.168.178.XX | ✅ Yes (VPN) |
| Gitea SSH | TCP | 2222 | 2222 | 192.168.178.XX | ⚡ Optional (for git clone via SSH) |
⚠️ Security Note: Do NOT forward port 81. The Nginx Proxy Manager admin UI should only be accessible from your local network (or via WireGuard VPN).
5. Project Setup
5.1 Clone or Copy This Repository
# If using git
git clone <your-repo-url> ~/home-server
cd ~/home-server
# Or simply copy the files to your laptop
# and navigate to the directory
cd ~/home-server
5.2 Create Your Environment File
cp .env.example .env
Edit .env with your preferred editor:
nano .env
Required changes:
| Variable | What to set |
|---|---|
TZ |
Your timezone (default: Europe/Berlin) |
WG_SERVERURL |
home.doerflingers.com (already set) |
WG_PEERS |
Comma-separated list of your VPN client names |
GITEA_SECRET_KEY |
Run openssl rand -hex 32 and paste result |
GITEA_INTERNAL_TOKEN |
Run openssl rand -hex 32 and paste result |
5.3 Set Script Permissions
chmod +x scripts/backup.sh scripts/restore.sh
6. Launch the Stack
6.1 Start All Services
docker compose up -d
This will pull all images (first run may take 5-10 minutes on a slow connection) and start the containers.
6.2 Verify Everything is Running
docker compose ps
Expected output:
NAME STATUS PORTS
gitea Up 0.0.0.0:2222->22/tcp, 3000/tcp
homeassistant Up 8123/tcp
nginx-proxy-manager Up (healthy) 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp, 0.0.0.0:81->81/tcp
wireguard Up 0.0.0.0:51820->51820/udp
6.3 Check Logs (if something goes wrong)
# All services
docker compose logs -f
# Specific service
docker compose logs -f homeassistant
docker compose logs -f gitea
docker compose logs -f wireguard
docker compose logs -f nginx-proxy-manager
7. Nginx Proxy Manager Configuration
7.1 Access the Admin UI
Open your browser and go to:
http://<laptop-local-ip>:81
Default credentials:
| Field | Value |
|---|---|
admin@example.com |
|
| Password | changeme |
You will be prompted to change these on first login. Do it immediately.
7.2 Add Proxy Host for Home Assistant
-
Go to Hosts → Proxy Hosts → Add Proxy Host
-
Fill in the Details tab:
Field Value Domain Names ha.doerflingers.comScheme httpForward Hostname / IP homeassistantForward Port 8123Websockets Support ✅ Enable (required for HA) -
Go to the SSL tab:
Field Value SSL Certificate Request a new SSL Certificate Force SSL ✅ Enable HTTP/2 Support ✅ Enable Email for Let's Encrypt your-email@example.com Agree to ToS ✅ Yes -
Click Save.
7.3 Add Proxy Host for Gitea
Same process, with these values:
| Field | Value |
|---|---|
| Domain Names | git.doerflingers.com |
| Scheme | http |
| Forward Hostname / IP | gitea |
| Forward Port | 3000 |
| Websockets Support | ✅ Enable |
SSL tab: same as above (request new cert, force SSL, HTTP/2).
7.4 Add Proxy Host for Home / Landing Page (Optional)
If you want home.doerflingers.com to point somewhere (e.g., a landing page or redirect to Home Assistant):
| Field | Value |
|---|---|
| Domain Names | home.doerflingers.com |
| Scheme | http |
| Forward Hostname / IP | homeassistant |
| Forward Port | 8123 |
Or use a Redirection Host to redirect home.doerflingers.com → ha.doerflingers.com.
Note:
home.doerflingers.comis primarily used as the WireGuard server URL. It doesn't need a proxy host unless you want it to serve a web page too.
8. Service-Specific Setup
8.1 Home Assistant — First Launch
- Navigate to
https://ha.doerflingers.com(orhttp://<laptop-ip>:8123from LAN). - The onboarding wizard will guide you through:
- Creating your admin account
- Setting your home location
- Discovering devices on your network
- Home Assistant is ready to use!
Note on device discovery: In container mode (non-host network), some discovery protocols (mDNS, SSDP) may not work. You can add integrations manually via Settings → Devices & Services → Add Integration.
8.2 Gitea — First Launch
-
Navigate to
https://git.doerflingers.com. -
The initial configuration page will appear. Most settings are pre-configured via environment variables. Verify:
Setting Value Site Title Your choice Repository Root Path /data/git/repositoriesSQLite Database Path /data/gitea/gitea.dbSSH Server Domain git.doerflingers.comSSH Port 2222Gitea Base URL https://git.doerflingers.com/ -
Create your admin account in the Administrator Account Settings section.
-
Click Install Gitea.
9. WireGuard Client Setup
9.1 Locate Client Configurations
After the WireGuard container starts, it automatically generates config files for each peer defined in WG_PEERS:
# List generated peer configs
ls wireguard/config/peer_*/
# View a specific peer's config
cat wireguard/config/peer_phone/peer_phone.conf
Each peer folder also contains a QR code image for easy mobile setup:
# Display QR code in the terminal (for mobile scanning)
docker exec wireguard /app/show-peer phone
9.2 Install WireGuard on Your Client
| Platform | App |
|---|---|
| Android | WireGuard on Play Store |
| iOS | WireGuard on App Store |
| Windows | WireGuard official installer |
| macOS | WireGuard on App Store or brew install wireguard-tools |
| Linux | sudo apt install wireguard-tools |
9.3 Import the Configuration
- Mobile: Scan the QR code shown by
docker exec wireguard /app/show-peer <peername> - Desktop: Copy the
peer_<name>.conffile to the client and import it in the WireGuard app
9.4 Test the VPN Connection
- Activate the WireGuard tunnel on your client.
- Try accessing
https://ha.doerflingers.com— it should work even from a mobile network. - You can also access the Nginx Proxy Manager admin UI at
http://192.168.178.XX:81through the VPN.
10. Backup & Restore
10.1 Create a Backup
# Backup all service data to ./backups/
./scripts/backup.sh
# Or specify a custom backup directory
./scripts/backup.sh /mnt/usb-drive/backups
The script creates a timestamped archive like home-server-backup_20260220_080000.tar.gz.
Tip: For automated backups, add a cron job:
# Daily backup at 3 AM 0 3 * * * /home/YOUR_USER/home-server/scripts/backup.sh /mnt/backup-drive/ >> /var/log/home-server-backup.log 2>&1
10.2 Restore from Backup
# Stop all services first!
docker compose down
# Restore
./scripts/restore.sh backups/home-server-backup_20260220_080000.tar.gz
# Restart services
docker compose up -d
11. Migration to New Hardware
Moving everything to a new server is straightforward thanks to Docker:
-
On the old server:
docker compose down ./scripts/backup.sh -
Copy to new server:
scp backups/home-server-backup_*.tar.gz user@new-server:/tmp/ scp -r . user@new-server:~/home-server/ # or just clone the repo -
On the new server:
# Install prerequisites (Section 2 of this README) cd ~/home-server ./scripts/restore.sh /tmp/home-server-backup_*.tar.gz # Review .env and adjust if needed (e.g., new timezone) docker compose up -d -
Update DynDNS — Run the DynDNS setup again on the new server (Section 3).
-
Update FritzBox — If the new server has a different local IP, update the port forwarding rules (Section 4).
12. Troubleshooting
Container won't start
docker compose logs <service-name>
Home Assistant shows 400 Bad Request
This means the reverse proxy is not trusted. Verify that homeassistant/configuration.yaml contains the correct trusted_proxies subnet (172.20.0.0/24) matching the Docker network in docker-compose.yml.
# Find the actual Docker network subnet
docker network inspect proxy-network | grep Subnet
Let's Encrypt certificate fails
- Port 80 must be forwarded in FritzBox (HTTP-01 challenge requires it).
- DNS for the domain must already point to your IP. Check:
dig +short ha.doerflingers.com - Wait a few minutes after setting up DynDNS for DNS propagation.
WireGuard clients can't connect
- Verify port
51820/UDPis forwarded in FritzBox. - Check if the WireGuard kernel module is loaded:
lsmod | grep wireguard - View WireGuard logs:
docker compose logs wireguard
Gitea SSH clone doesn't work
- Verify port
2222/TCPis forwarded in FritzBox. - Clone syntax:
git clone ssh://git@git.doerflingers.com:2222/user/repo.git
High memory usage
# Check per-container memory usage
docker stats --no-stream
If running low on RAM, consider:
- Reducing
WG_PEERScount - Moving Gitea to a lighter database (SQLite is already the lightest)
- Adding more swap:
sudo fallocate -l 4G /swapfile2 && sudo chmod 600 /swapfile2 && sudo mkswap /swapfile2 && sudo swapon /swapfile2
Laptop suspends when lid is closed
See Section 2.5 — make sure HandleLidSwitch=ignore is set.
13. Architecture Overview
┌─────────────────────────────────────────────────────────────────────────┐
│ INTERNET │
│ │
│ ha.doerflingers.com ──┐ │
│ git.doerflingers.com ─┤──→ Your Public IP (DynDNS-updated) │
│ home.doerflingers.com ┘ │
└────────────────────────────────┬────────────────────────────────────────┘
│
┌───────────┴───────────┐
│ FritzBox │
│ Port Forwarding: │
│ 80 → Laptop:80 │
│ 443 → Laptop:443 │
│ 51820/UDP → :51820 │
│ 2222 → Laptop:2222 │
└───────────┬───────────┘
│
┌───────────┴───────────┐
│ Ubuntu 24.04 Laptop │
│ (Docker Host) │
└───────────┬───────────┘
│
┌──────────────────┼──────────────────┐
│ Docker Network │
│ (proxy-network) │
│ 172.20.0.0/24 │
│ │
│ ┌─────────────────────────────┐ │
│ │ Nginx Proxy Manager │ │
│ │ :80, :443, :81 │ │
│ │ SSL termination │ │
│ │ Let's Encrypt certs │ │
│ └──────┬──────┬───────────────┘ │
│ │ │ │
│ ┌────┘ └────┐ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Home │ │ Gitea │ │
│ │ Assistant│ │ :3000 │ │
│ │ :8123 │ │ :22→2222│ │
│ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────────────────────┐ │
│ │ WireGuard VPN │ │
│ │ :51820/UDP │ │
│ │ Subnet: 10.13.13.0/24 │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
Data Persistence
All service data is stored in local directories (bind mounts), making backups and migration simple:
| Service | Data Directory | Contains |
|---|---|---|
| Nginx Proxy Manager | ./nginx-proxy-manager/data/ + ./nginx-proxy-manager/letsencrypt/ |
Proxy configs, SSL certs |
| Home Assistant | ./homeassistant/ |
Configuration, automations, database |
| Gitea | ./gitea/data/ |
Repositories, database, config |
| WireGuard | ./wireguard/config/ |
Server + peer keys, configs |
Quick Reference
# Start all services
docker compose up -d
# Stop all services
docker compose down
# Restart a single service
docker compose restart homeassistant
# View logs
docker compose logs -f
# Check resource usage
docker stats
# Update all images to latest
docker compose pull
docker compose up -d
# Backup
./scripts/backup.sh
# Restore
docker compose down
./scripts/restore.sh backups/<backup-file>.tar.gz
docker compose up -d