Nextcloud & Quality of Life: Part 7 of “Building a Resilient Home Server” Series
So I've got monitoring working, the disaster recovery ISO is solid, and everything's declarative. Now it's time to tackle something that's been bugging me: dependence on cloud services.
Don't get me wrong - Dropbox, iCloud, OneDrive - they're convenient. But you're trusting someone else with your files, paying monthly subscriptions, and hoping they don't change their terms or pricing. Plus, there's the whole "your data is their product" thing.
I'd rather own my data. And with a home server already running, why not?
Enter Nextcloud - self-hosted cloud storage that you control. Your files, your hardware, your rules.
But first - a quick detour that'll make your life way better.
The nh Detour (Part 7.5)
Look, nixos-rebuild is Slow
If you've been following along, you've noticed that nixos-rebuild switch takes forever. Even on fiber, downloads crawl at 1-2 MB/s because LZMA decompression is eating your CPU alive.
And watching a wall of text scroll by with zero progress indication? That's some 2005-era UX right there.
Enter nh
nh (NixOS Helper) wraps nixos-rebuild with actual quality-of-life features:
- Progress bars for builds and downloads
- Color-coded output that doesn't make your eyes bleed
- Optimized substituter settings
- Build summaries showing what actually changed
I'll keep using standard nixos-rebuild commands in the blog for accessibility, but for your own sanity? Use nh.
Installation
Most people add packages directly to configuration.nix. In my setup, they're in modules/system.nix, but the principle is the same:
Add to your packages list:
Nix
environment.systemPackages = with pkgs; [
# ... existing packages ...
nh # Makes rebuilds not painful
];
Apply:
Bash
sudo nixos-rebuild switch
Usage (Non-Flake Setup)
Since I'm not using flakes, nh needs to know where my configuration lives. The way my configs are structured in the repo, here's the full command:
Bash
nh os switch -f '<nixpkgs/nixos>' -- -I nixos-config=/etc/nixos/configuration.nix
What this does:
- -f '<nixpkgs/nixos>' - Use the nixpkgs channel (non-flake)
- -- -I nixos-config=/etc/nixos/configuration.nix - Pass through the config path
For the rest of this post, I'll use the shorthand nixos-rebuild switch for readability, but know that's the full nh command you'll actually run.
Other useful commands:
Bash
# Build and set as next boot (doesn't activate now)
nh os boot -f '<nixpkgs/nixos>' -- -I nixos-config=/etc/nixos/configuration.nix
# Build and activate temporarily (reverts on reboot)
nh os test -f '<nixpkgs/nixos>' -- -I nixos-config=/etc/nixos/configuration.nix
# Search for packages
nh search vim
# Clean old generations and garbage collect
nh clean all --keep 5 # Keep last 5 generations
nh clean user ppb1701 --keep 3 # Clean specific user's home-manager generations
nh clean profile /nix/var/nix/profiles/system --keep 5 # Clean specific profile
Full cleanup example:
Bash
# Clean system generations, keep last 5
nh clean all --keep 5
# Or clean specific user's home-manager generations
nh clean user ppb1701 --keep 3
# Or target a specific profile
nh clean profile /nix/var/nix/profiles/system --keep 5
During my ISO build marathon, I discovered that --log-format bar-with-logs and additional substituters made builds tolerable. nh bakes these improvements in by default.
External Drive Setup
I've got a 6TB external drive for Nextcloud data. Keeps user files separate from the system, makes backups cleaner.
Formatting and Getting the UUID
First, identify your drive:
Bash
lsblk
Find your drive (mine was /dev/sdb), then format it:
Bash
sudo mkfs.ext4 -L nextcloud-data /dev/sdb
Get the UUID for your config:
Bash
sudo blkid /dev/sdb
This outputs something like:
Plaintext
/dev/sdb: UUID="15d81dda-90c6-4993-9770-92841665e7b5" TYPE="ext4" LABEL="nextcloud-data"
Copy that UUID - you'll need it for the config.
Declarative Mounting
Add to modules/system.nix:
Nix
# Create the mount point
systemd.tmpfiles.rules = [
"d /mnt/nextcloud-data/data 0750 nextcloud nextcloud -"
];
# Mount the drive
fileSystems."/mnt/nextcloud-data" = {
device = "/dev/disk/by-uuid/[YOUR_UUID_HERE]";
fsType = "ext4";
options = [ "defaults" "nofail" ]; # nofail prevents boot issues if drive is missing
};
Why mount by UUID? Device names like /dev/sdb can change between reboots. UUIDs are stable.
The nofail option means the system will boot even if the drive isn't connected - important for disaster recovery scenarios.
Apply:
Bash
sudo nixos-rebuild switch
Verify it's mounted:
Bash
df -h /mnt/nextcloud-data
Nextcloud: Taking Back Your Files
So here's the thing about cloud storage - it's convenient until it isn't. Monthly fees that keep creeping up, arbitrary file size limits, and the nagging feeling that someone's data mining your vacation photos for ad targeting.
I've got a 6TB external drive sitting here. Might as well use it.
Installing Nextcloud
Nextcloud's NixOS module is surprisingly complete. It handles PostgreSQL setup, Nginx configuration, and even Let's Encrypt certificates automatically.
I keep all my service configs in modules/services.nix, so that's where this goes:
Bash
sudo micro /etc/nixos/modules/services.nix
Add this block:
Nix
# ═══════════════════════════════════════════════════════════════════════════
# NEXTCLOUD
# ═══════════════════════════════════════════════════════════════════════════
services.nextcloud = {
enable = true;
package = pkgs.nextcloud32;
hostName = "nextcloud.home";
datadir = "/mnt/nextcloud-data";
database.createLocally = true;
config = {
dbtype = "pgsql";
adminpassFile = "/etc/nixos/private/nextcloud-admin-pass";
};
# PHP settings for large files
phpOptions = {
"upload_max_filesize" = "16G";
"post_max_size" = "16G";
"memory_limit" = "512M";
};
extraApps = with config.services.nextcloud.package.packages.apps; {
inherit calendar contacts notes tasks;
};
extraAppsEnable = true;
https = false; # HTTP only for local network
settings = {
"overwriteprotocol" = "http"; # Tell Nextcloud it's HTTP
trusted_domains = [
"nextcloud.home"
"localhost"
"nextcloud.vpn" # For Tailscale access
];
trusted_proxies = [
"100.64.0.0/10" # Tailscale CGNAT range
];
};
};
# Configure Nextcloud to use port 8280 to avoid conflict with AdGuard Home
services.nginx.virtualHosts."nextcloud.home".listen = [
{ addr = "0.0.0.0"; port = 8280; }
{ addr = "[::]"; port = 8280; }
];
A few things to note:
- datadir = "/mnt/nextcloud-data" - all user files go on the external drive
- database.createLocally = true - NixOS handles PostgreSQL setup
- adminpassFile - password lives in /etc/nixos/private/, not in the config
- phpOptions - bumped up for large file uploads (16GB max)
- https = false - HTTP only for the local network
- port = 8280 - avoids conflict with AdGuard Home on port 80
- overwriteprotocol = "http" - tells Nextcloud to stop trying to redirect to HTTPS
The Admin Password
Nextcloud needs an admin password, but I'm not hardcoding that in the config. Create the secrets directory if it doesn't exist:
Bash
sudo mkdir -p /etc/nixos/private
echo "your-secure-password-here" | sudo tee /etc/nixos/private/nextcloud-admin-pass
sudo chmod 600 /etc/nixos/private/nextcloud-admin-pass
And make sure Git ignores it:
Bash
echo "private/" >> /etc/nixos/.gitignore
DNS Setup
Nextcloud needs a hostname. Add a DNS rewrite in AdGuard Home:
- Filters → DNS rewrites
- Add: nextcloud.home → 192.168.1.100 (your server's IP)
Fire It Up
Bash
sudo nixos-rebuild switch
This will:
- Install Nextcloud and PostgreSQL
- Create the database
- Configure Nginx on port 8280
- Start everything
Give it a minute, then open http://nextcloud.home:8280 in your browser.
Login with admin and the password you set earlier.
You now own your cloud storage.
The Port 80 Problem
Here's where things got interesting. I applied the config, everything built successfully, services started... and I couldn't login to Nextcloud.
The issue: Nextcloud defaults to port 80. AdGuard Home is already sitting on port 80. Nginx couldn't bind to the port, so Nextcloud was unreachable.
The fix: Configure Nginx to use port 8280 instead (already shown in the config above).
The HTTP/HTTPS Decision
I went with HTTP only for the local network. Why? Because:
- It's all behind AdGuard Home and Tailscale
- No need for certificate management headaches
- Simpler to troubleshoot
- Fast
That overwriteprotocol = "http" setting is critical - it tells Nextcloud "you're HTTP, stop trying to redirect to HTTPS."
Monitoring Integration
Time to make sure I know when Nextcloud goes down.
Prometheus Exporter
Nextcloud has a built-in Prometheus exporter. Here's how it's configured in modules/monitoring.nix:
Nix
exporters = {
nextcloud = {
enable = true;
url = "http://nextcloud.home:8280";
username = "root";
passwordFile = "/etc/nixos/private/nextcloud-admin-pass";
port = 9205;
};
};
scrapeConfigs = [
{
job_name = "nextcloud";
static_configs = [{
targets = [ "localhost:9205" ];
}];
}
];Blackbox HTTP Check (via Tailscale)
The interesting part - I monitor Nextcloud via its Tailscale IP to verify remote access works:
Nix
{
job_name = "nextcloud-http";
scrape_interval = "30s";
metrics_path = "/probe";
params.module = [ "http_2xx" ];
static_configs = [{
targets = [ "http://${secrets.tailscaleIP}:8280" ];
}];
relabel_configs = [
{
source_labels = [ "__address__" ];
target_label = "__param_target";
}
{
source_labels = [ "__param_target" ];
target_label = "instance";
}
{
target_label = "__address__";
replacement = "localhost:9115";
}
];
}This checks Nextcloud through Tailscale, not just localhost - proves remote access is working.
Grafana Dashboard
Import the official Nextcloud dashboard:
- Go to http://localhost:3000
- Dashboards → Import
- Dashboard ID: 9632
- Select Prometheus data source
- Import
Alertmanager Rules (And The False Positives)
Here's where it got fun. Initial alerts were too aggressive:
Nix
- alert: NextcloudDown
expr: probe_success{job="nextcloud-http"} == 0
for: 20m
labels:
severity: critical
annotations:
summary: "Nextcloud is unreachable"
description: "Nextcloud HTTP check has failed for 20 minutes"
The problem: Nextcloud can be slow to respond during file uploads, background jobs, or maintenance tasks. A 2-minute timeout was way too aggressive - constant false positives.
The fix: Bumped it to 20 minutes. I still get the occasional false positive (about every 6-8 hours), but it's rare now. Nextcloud just needs breathing room sometimes.
Note to self: As I'm writing this, I realized the alert message still says "15 minutes" when the actual timeout is 20 minutes. Oops. That's what I get for copy-pasting and not updating everything. Fixed in the config above, need to apply it to the actual server.
Apply:
Bash
sudo nixos-rebuild switch
Remote Access: The Problem
So Nextcloud is running locally, but what about when I'm not home?
The usual suspects:
- Port forwarding - Poke holes in your firewall, expose services to the internet, pray nobody finds them
- Dynamic DNS - Hope your IP doesn't change at 3am when you need access
- Traditional VPN - Certificate management, complex configs, breaks when you look at it wrong
All of these options range from "annoying" to "security nightmare."
But I already have the perfect solution installed: Tailscale. (See Part 3 for the initial setup.)
Tailscale creates a mesh VPN between all your devices - no port forwarding, no certificates, no headaches. The server gets a stable IP address that works from anywhere, and all traffic is encrypted end-to-end.
Configuring Nextcloud for Tailscale
Nextcloud needs to know it can be accessed via Tailscale. I already have the trusted proxies configured for the Tailscale CGNAT range, but let's add a specific hostname for remote access.
The config shown earlier already includes:
Nix
settings = {
trusted_domains = [
"nextcloud.home"
"localhost"
"nextcloud.vpn" # For Tailscale access
];
trusted_proxies = [
"100.64.0.0/10" # Tailscale CGNAT range
];
};DNS for Remote Access
Add a DNS rewrite in AdGuard Home for the Tailscale hostname:
- nextcloud.vpn → 100.x.x.x (your server's Tailscale IP)
Now I can access Nextcloud from anywhere via http://nextcloud.vpn:8280.
Test it: Turn off WiFi on your phone, use cellular data, and open http://nextcloud.vpn:8280. You should see the Nextcloud login page, securely tunneled through Tailscale.
Verification
Verify everything is working:
Bash
# Check all services are running
systemctl status nextcloud nginx postgresql
# Verify Nextcloud is accessible locally
curl -I http://nextcloud.home:8280
# Check Prometheus is scraping Nextcloud metrics
curl http://localhost:9205/metrics
# Verify blackbox exporter can reach Nextcloud via Tailscale
curl "http://localhost:9115/probe?target=http://$(tailscale ip -4):8280&module=http_2xx"
Test the web portals:
- On your network: http://nextcloud.home:8280
- Off your network (via Tailscale): http://nextcloud.vpn:8280
Login with admin and your password. Upload a test file, verify it shows up on the external drive at /mnt/nextcloud-data/data/.
For system maintenance and troubleshooting, see the documentation in the repo: https://github.com/ppb1701/nixos-config/tree/main/docs
Next Time
Part 8: Vaultwarden - Self-hosted password management with Bitwarden compatibility. Because if I'm taking back control of my data, passwords are next on the list.
Then I'll tackle backup automation to make sure I never lose anything.
Building a resilient home server, one declarative config at a time.
Questions? Reach out on Mastodon: @ppb1701@ppb.social
Main Server (nixos): Codeberg
Second Server (nixos2): Codeberg
ISO can be gotten here.