I've always believed home-grown projects can be both fun and genuinely useful. Over the past few weeks I turned that belief into SamNet Cloud—a self-hosted Nextcloud running on a hardened Raspberry Pi 4 with a clean, modern front end and a self-service signup flow. In this post I'll walk through what I built, how it works, why certain choices mattered, and a few lessons I learned along the way.
What I set out to build
I wanted a small, dependable file cloud for friends and side projects—no trackers, sane defaults, HTTPS everywhere, and a UX that doesn't feel "hobby-grade." The non-negotiables:
- Security by default: LUKS full-disk encryption, enforced HTTPS, and Redis for file locking.
- Clean signup: human check, email verification, and rate-limited resends.
- Good UX: responsive landing page, sleek register form, clear instructions, professional emails.
- Low maintenance: Raspberry Pi + Nginx + PHP-FPM + Nextcloud, automated backups every 72 hours.
The stack at a glance
- Hardware: Raspberry Pi 4 (SSD, encrypted with LUKS).
- Core apps: Nextcloud (Docker), Nginx (reverse proxy), PHP-FPM for the signup app.
- Data: MariaDB + Redis.
- Security: Let's Encrypt TLS, strict headers, SPF/DMARC, and simple rate limits.
- Frontend: Hand-crafted HTML/CSS with a "glass" UI, fully responsive.
- Email: msmtp (Gmail/SMTP relay) for verification + a polished welcome email.
UX first: landing + registration
I wanted the homepage at cloud.samnet.dev to feel like a product, not a lab. It's a two-column layout: a hero card with "Create account / Sign in / Instructions" on the left, and feature cards on the right. Below the hero I showcase other SamNet projects (Speed Test, Monitoring) with icons and concise descriptions. It scales cleanly down to mobile.
A tiny taste of the hero markup
<svg class="cloud" viewBox="0 0 48 48" aria-hidden="true">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#6ea8fe"/><stop offset="1" stop-color="#b86bff"/>
</linearGradient></defs>
</svg>
<h1>SamNet Cloud</h1>
Private Nextcloud for your projects—encrypted at rest, HTTPS in transit…
[Create account](/register/)
[Sign in](/login)
<button class="btn" id="openGuide">Instructions</button>
The Instructions button opens an accessible modal with a short, emoji-aided getting-started guide (desktop, mobile apps, WebDAV, 2FA tips). On mobile the dialog caps its height and scrolls internally so it never overflows.
The self-signup flow (with guardrails)
The register page mirrors the product style but focuses on clarity: username/email/password, reCAPTCHA, and a minimal ToS modal. On submit, a six-digit code goes to the user's email, and the UI switches to a verification form with a Resend button that respects a cooldown (45s in my latest change).
Here's a condensed version of the PHP logic:
// 1) Start signup: validate inputs, verify reCAPTCHA, capacity check
if ($stage === 'signup') {
$user = clean_user($_POST['user'] ?? '');
$mail = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
$pass = $_POST['pass'] ?? '';
// …validate + call Google verify endpoint…
// Save pending state + send code
$_SESSION['pending'] = ['user'=>$user,'pass'=>$pass,'mail'=>$mail,'expires'=>time()+1800];
$_SESSION['last_resend'] = time();
$code = random_int(100000, 999999);
$_SESSION['code'] = password_hash((string)$code, PASSWORD_DEFAULT);
$html = email_shell('Verify your email', email_verification_html($code, 30));
send_mail($mail, 'Your verification code', $html, "Code: $code");
$stage = 'verify';
}
// 2) Resend (rate-limited)
if ($stage === 'resend') {
$last = $_SESSION['last_resend'] ?? 0;
if (time() - $last >= 45) {
$code = random_int(100000, 999999);
$_SESSION['code'] = password_hash((string)$code, PASSWORD_DEFAULT);
$_SESSION['last_resend'] = time();
send_mail($_SESSION['pending']['mail'], 'Your new verification code',
email_shell('Your new verification code', email_verification_html($code, 30)),
"Code: $code");
}
$stage = 'verify';
}
// 3) Verify and create user via Nextcloud OCS API
if ($stage === 'verify_submit') {
$ok = password_verify(trim($_POST['code'] ?? ''), $_SESSION['code'] ?? '');
if ($ok) {
$u = $_SESSION['pending']['user'];
$p = $_SESSION['pending']['pass'];
$m = $_SESSION['pending']['mail'];
// Create account
ocs('POST','/ocs/v1.php/cloud/users?format=json', ['userid'=>$u,'password'=>$p,'email'=>$m]);
// Set default quota
ocs('PUT', "/ocs/v1.php/cloud/users/".rawurlencode($u)."?format=json",
['key'=>'quota','value'=> (string)($DEFAULT_QUOTA_MB*1024*1024) ]);
// Send welcome email
send_mail($m, 'Welcome to SamNet Cloud',
email_shell('Welcome to SamNet Cloud', email_welcome_html($u, $NC_BASE, $DEFAULT_QUOTA_MB)),
"Welcome, $u! Sign in: $NC_BASE");
}
}
A few details I'm glad I built in:
- Sessions with expiry for the pending signup.
- Hashed codes (never store the raw OTP).
- Resend cooldown with a client-side countdown and a server-side check.
- Clear errors and one success prompt (no double banners).
Nginx gotchas & the "/register" slash
The reverse proxy terminates TLS, serves the landing page, and exposes Nextcloud. The signup app lives under /register/ using alias. The small but important bit: redirect the no-slash path to a slash, and make sure the PHP location preserves the aliased path.
# Redirect /register -> /register/
location = /register {
return 301 $scheme://$host/register/;
}
# Serve the app (index + fallback)
location /register/ {
alias /var/www/register/;
index index.php;
try_files $uri $uri/ /register/index.php?$args;
}
# PHP for aliased directory
location ~ ^/register/(.+\.php)$ {
alias /var/www/register/;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/register/$1;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}
I also added strict security headers, HSTS, and raised proxy timeouts for large file operations.
Email that feels like a product email
Plain text gets the job done, but styled emails make the project feel real. I built a table-based, responsive template with:
- Branded header (SamNet Cloud / SamNet IT Services)
- Clear title and content blocks
- Buttons that align in all major clients
- A "Highlights" section with icon bullets
- Links to explore other projects
- A note pointing to SamNet SecOps AI assistant for quick help
Verification emails are short and to the point. The welcome email thanks users, explains security choices (LUKS, HTTPS, Redis, backups), provides quick-start links, and encourages 2FA.
On the backend, msmtp works as a lean sendmail replacement. I set SPF/DMARC on the domain so messages land in Inbox rather than spam.
Icons, SEO & share cards
To tidy up the browser experience and improve visibility, I generated a small icon set (16–512 PNGs + favicon.ico and an Apple touch icon) and wired a site.webmanifest for the PWA basics. I also added OpenGraph/Twitter tags and a share image so links to cloud.samnet.dev look great on Slack/LinkedIn.
What I learned building this
- Small polish adds a lot: aligned inputs, consistent button sizes, gentle shadows, and a real email template make the difference between "lab toy" and "product."
- Nginx
aliasnuances matter: getting PHP +alias+try_filesright saves hours of head-scratching. - Rate-limiting UX is both security and kindness. The resend timer prevents spam and sets expectations.
- Table-based email is still king: it's not glamorous, but it survives Gmail, Outlook, Apple Mail, and mobile clients.
- Write your ToS like a human: short, clear, and honest about best-effort service on a tiny box.
- Self-hosted ≠ insecure: a Pi with LUKS, TLS, good headers, and backups can be a solid little platform.
Try it—and tell me what you think
If you want a quiet, privacy-first place to stash files and collaborate, SamNet Cloud is open. Install the Nextcloud mobile/desktop apps to sync, and explore my Speed Test and Monitoring projects from the home page. Questions? The SamNet SecOps AI assistant is there to help, and I'm always up for feedback.
Home-made projects can absolutely be useful and beautiful—and this one now powers my day-to-day.
☁️ Visit SamNet Cloud