My self-hosting graveyard is longer than the list of survivors
I didn't set up my first server for anything this post is about (for reasons of my own, that part stays a secret under seven seals:D), but it very quickly found a perfectly legitimate and much more boring purpose: I needed a place to deploy my own pet projects - for tests, for demos to friends, to poke at something live instead of localhost - without running into Vercel and Render with their free tiers, strict per-service limits and subscriptions that grow faster than my projects do
A VPS has only one limit, the hardware, and as it turned out, that's much easier to negotiate with than someone else's price list ;)
Then I noticed that most of the time this hardware just sits idle, and I asked myself a fair question: what exactly am I paying for? Right around then I stumbled on the awesome-selfhosted repo, and down the rabbit hole I went… :D
Right now there are three machines, not counting my main PC: a small rented VPS in Europe with 3 gigabytes of memory and 30 gigabytes of disk, which everything that has to be reachable from the internet goes through; a home server that lives at my family's place, because electricity and internet are simply cheaper there; and a small VPS for learning and doing scary things by hand (so I don't end up restoring the production box from backups), with 1 gigabyte of RAM and about 10 gigabytes of disk
The home media server runs a Ryzen 3 PRO 3200G, 12 gigabytes of memory, a 480-gigabyte system SSD and two hard drives: a 4 TB WD Gold for everything valuable and an 8 TB Toshiba N300 for media
They're called tank and junk, and I don't think I need to explain which is which :D
Tank needs a second drive to mirror it, so the backups don't live on a single disk, but storage prices are such that it's easier to buy a Chevrolet Camaro than a new hard drive that size, and good deals on the used market are rare
The current one was a lucky find: about $80 for a WD Gold WD4002FYYZ with just over 200 power cycles and a couple of years of service (and of course I ran the tests, the drive is livelier than plenty of modern ones)
In all honesty: of those 12 gigabytes, the system currently sees less than ten, because the CMOS battery on the motherboard is dead, so after every power cut it forgets my settings and once again generously hands two gigabytes to the integrated graphics, which a server with no monitor has roughly zero use for
For about two months now I've been promising myself every weekend to swap the battery and take that memory back, but, as you can probably tell, I'm a lazy ass, and my weekends are taken up by more important things (lying around, doomscrolling Instagram reels, playing with my cat, sinking ten hours into Civilization V... you know how it is:D)
The front door
The VPS mostly hosts what glues everything else together and keeps it safe, plus a couple of things I use every day
My blog: about a hundred megabytes
Homepage: a start page with a tile for every service
Out of the box it's boring, and I wanted something pretty, so mine has glass, a background and my own palette, and I use it quite often, because its UI and UX really are great
Uptime Kuma: monitoring
Strictly speaking, having a server check whether it's still alive is a so-so idea, and once mine was down for half an hour without anyone noticing, but it keeps a proper eye on the containers, and that's why I keep it (the obvious fix is to run it on the home server and watch the VPS from outside, it's on the list)
Beszel: CPU, memory and disk graphs for every machine
Right now it just sits there: Kuma and Termix cover pretty much everything I need from it, so Beszel is like that distant relative you used to be really close to and now call once a year :D
Termix: SSH right in the browser
Nexterm lived exactly one day before it, and Termix stayed because it won on every count at once: features, easy setup, convenience, looks and snippets, and I honestly don't know how I managed servers from my phone before them
Memos: notes where I write down absolutely everything, and it has completely replaced Saved Messages in Telegram for me
I'm a fan of minimalism and simplicity, and I don't see the point of an app where I'll never touch most of the features, and most importantly, it's self-hosted, btw
The idea of bolting AI onto it for tags and summaries is sitting in the backlog: there are higher-priority projects right now, but SOMEDAY I'll automate this part of my life:D
MeshCentral: remote access to my main PC
All my code and Claude Code live on it, and I often work away from home, so I needed access from anywhere, right in the browser, without installing anything on the machine I'm connecting from
Donetick: household chores for the family, and right now it's on pause, because I still have questions about the interface and some of the features
But it comes with my favorite story: I forked its web app to translate it, and it turned into a full i18n integration and some real help for the maintainer, so now I'm listed as a contributor in donetick/frontend
Vaultwarden: a password manager
I used Google Password Manager for years, and that was perfectly fine right up until I started running my own services and suddenly realized I was seriously worried about where and how my data was stored, and since I'm running a server anyway, it made sense to keep my passwords there too
Pocket ID: passkey sign-in to almost all my services, no passwords
When you have twenty services, typing a password into each one gets old within half an hour, and I also wanted my self-written MCP servers to sit behind my own security layer, not behind some third-party platform I'd have to take at its word
Backrest: nightly backups that go to the cloud and then get pulled home
Knock on wood, I haven't had to restore from them yet, apart from a test run, and I picked it because I like seeing what's actually happening: I enjoy working in the console, but a UI is still more familiar and more pleasant
IT-Tools: a set of small converters and generators
It can stay :D
And two of my own pet projects: MindAmp, mind maps with an LLM layer that speeds up the work and boosts the research part with agents, no third-party subscriptions needed, and VigilAMP, a registry of my projects and their current status that keeps me from getting lost in what I'm working on and what stage it's at
All of this fits into one and a half gigabytes of memory, with about as much left in reserve, and the disk is 85% full
Yes, I know, and no, I'm not doing anything about it today:/
The warehouse
The home server is for the heavy stuff: things that take up a lot of space, and things I'd rather not keep on rented hardware
The rule here is simple: I test a service on myself first and only then bring the family in, which is why some of these are still just mine
Nextcloud with OnlyOffice: my files and documents, a replacement for Google Drive, which is now just a backup
People often call Nextcloud heavy, and the numbers I'd read were scary: 1-1.2 GB of RAM against 300-400 MB for Seafile, so I was all set to go with Seafile
Then I decided not to take it on faith and ran both side by side on the home server
At idle, the whole Seafile stack (Seafile, MariaDB, memcached) sat at about 450 MiB, and Nextcloud with Postgres and Redis at about 270
Right now the Nextcloud stack, cron included, idles at around 240 MiB, and with OnlyOffice on top it's about 415, roughly 4% of the 9.6 GB my dead battery lets me have
OnlyOffice is the only container with a cap (2 GB), because it spins up a worker for every open document and easily climbs into the hundreds of MB while someone's editing
These are idle numbers
Previews and big syncs make Nextcloud heavier, and my setup is gentle: SSD, LAN only, not much data
Still, I haven't run into any problems, it's stable and pretty spry, and on my hardware the memory argument fell apart, so Nextcloud stayed and Seafile went (I could be wrong, ofc, but I'm too lazy to test it under load, and I don't use it that much anyway: it's basically a glorified file dump with a few nice apps that let me get to my files from anywhere)
Immich: photos, a great Google Photos replacement, and it's still at the "me first" stage before I bring the family in
BookLore: a book library, and I chose it for FB2 support, because my whole library is in that format
So far I'm the only one using it
BookLore has since gone into maintenance mode, and its developer named BookOrbit as the official successor, so right now I'm trying it out next to BookLore, but I don't have an opinion on it yet
Jellyfin: movies and TV shows
It's fully open and free, does exactly what it needs to, and these days we watch almost everything I own through it at home
SearXNG: a metasearch engine, set up for privacy and security, like most of my services
I've barely used it seriously yet
Navidrome: my music library, and I listen to it every day: all my music goes through it now, no subscriptions, no limits
On my PC I listen through Psysonic, and on my phone through Solound
The graveyard
The list of things I've removed is longer than the list of things that run, and every one of the deceased had its own cause of death
And these are just the ones worth a eulogy: in total around fifty services got installed and then deleted
Some got a second chance, like Dashy and Karakeep, and Windmill filled the disk to 100%
Ghost and Astro with Keystatic were my first two shots at my blog, and each lasted about two days
Ghost brought a crowded communal apartment of other people's services, and Keystatic kept its admin on Vite's dev server, the one you're not supposed to expose to the internet
So I ended up writing my own
Finding a browser terminal took longer than I'd like
ttyd dropped the connection every few minutes
Cockpit worked, but I only needed it for the terminal and files, and dragging in a whole server control panel for that is overkill
Nexterm was lightweight, but it was still in beta
Termix won
Seafile was supposed to be the lightweight one and lost to Nextcloud on memory (the numbers are up in the warehouse)
Termix also finished off Portainer: it turned out I'd rather manage containers straight from the terminal than click through yet another panel
Karakeep, bookmarks with previews, turned out to be three containers, 3.5 GB of images and 700 MB of memory
Without the built-in Chrome for previews it's a completely different product, and keeping something like that on a VPS with three gigabytes is a luxury I can't afford
Trilium was out because of its cluttered interface and broken built-in AI
Minimalism, remember? Notes went to Memos in the end
Anki Sync Server lasted two days: I set it up for my wife, my wife gave up on AnkiDroid, and there was nothing left to sync
And Inbox Zero never even made it to installation: I wrote a whole spec for it and then decided I didn't need it (and there were security questions too, but that stays between us:D)
My rule is this: every new service gets compared to what already works, and if it doesn't click quickly and isn't critical, it gets removed without hours of debugging, which is exactly why 3 gigabytes is still enough
Why all this
Partly for control: my photos and files sit on disks I can touch with my own hands, and my passwords at least live on a server I run myself
But mostly for learning: I write backend in Python and I'm learning everything around the code, and the servers are my sandbox for that, from networking and Docker to backups, monitoring and security
And when something breaks at three in the morning, it's my fault and my lesson ¯\_(ツ)_/¯
So what's in your graveyard, and what killed it?
10 comments
I Replaced Google Photos With a $60 Board in My Closet
---
title: I Replaced Google Photos With a $60 Board in My Closet
published: true
description: Google wanted ₹1,300/year to keep my own photos. So I pointed a domain at an ARM board, installed Nextcloud on bare metal, and got my library back — with nginx, PostgreSQL, a USB drive, and zero open ports.
tags: selfhosting, nextcloud, privacy, linux
canonical_url: clinerds.com
cover_image: clinerds.com
---
Google Photos was great until it wasn't. The free tier died years ago, my "Storage almost full" notification had become a weekly ritual, and the idea of paying rent forever to access my own memories started to genuinely annoy me. So last weekend I did what any reasonable person with a spare ARM board and trust issues would do: I built my own Google Photos.
The whole thing runs on a 4-core ARM board with 8 GB of RAM sitting in my closet. Photos auto-upload from my phone, I get a timeline view, a map, sharing links, the works. Total cost of ownership: the electricity a 5-watt board draws. Here's exactly how I did it — the native way and the Docker way — with the mistakes included, because that's where the learning is.
!Hero — the CLINERDS cloud
Too long; just run it: the whole native setup is also packaged as an interactive script — it asks you a handful of questions (domain, nginx or apache, postgres or mariadb, data dir) and does the rest. Grab setup-nextcloud.sh, read it (always read scripts before piping them into your life), then chmod +x setup-nextcloud.sh && ./setup-nextcloud.sh. The post below is the "what and why" behind everything the script does.
The stack (and why)
- Nextcloud — the obvious pick. It's the only self-hosted suite with a mature Android app that does camera auto-upload properly.
- nginx + PHP-FPM instead of Apache. Nextcloud ships .htaccess files and "officially" prefers Apache, but their docs publish a full nginx config, and nginx uses a fraction of the RAM. On a small board that matters.
- PostgreSQL instead of MySQL/MariaDB. Nextcloud's own docs recommend it, file locking behaves better under concurrency, and you skip the whole utf8mb4 / row-format dance MariaDB makes you do.
- A 128 GB USB drive for data. SD cards die under database + thumbnail workloads. Don't store your photos on the boot card.
- Cloudflare Tunnel for HTTPS and remote access. No port forwarding, no exposing my home IP, free valid TLS.
Part 1: The native install
This is the path I actually ran. Everything below is copy-paste real.
Packages
sudo apt update
sudo apt install -y nginx postgresql redis-server \
php-fpm php-pgsql php-gd php-curl php-zip php-xml php-mbstring \
php-intl php-bcmath php-gmp php-imagick php-redis php-apcu \
unzip ffmpeg
(If you're team MySQL: swap postgresql for mariadb-server and php-pgsql for php-mysql. Everything else is identical. I'll show both database setups below.)
First gotcha, straight from my terminal history: on a freshly booted Ubuntu, unattended-upgrades holds the dpkg lock hostage for a few minutes and apt just fails. Either wait it out or:
while sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do sleep 10; done
Database
PostgreSQL:
sudo -u postgres psql
CREATE USER nextcloud WITH PASSWORD 'pick-a-real-password';
CREATE DATABASE nextcloud OWNER nextcloud TEMPLATE template0 ENCODING 'UTF8';
Or MariaDB, if that's your religion:
sudo mariadb
CREATE DATABASE nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER 'nextcloud'@'localhost' IDENTIFIED BY 'pick-a-real-password';
GRANT ALL PRIVILEGES ON nextcloud.* TO 'nextcloud'@'localhost';
FLUSH PRIVILEGES;
One thing to check before you pick a PHP version: Nextcloud is picky about it. There's a compatibility matrix on their wiki — check it before apt installs whatever your distro's default is. My Ubuntu shipped PHP 8.5, which Nextcloud 34/35 supports. Lucky. An older release would have needed a PPA dance.
The data directory (on the USB drive)
My drive already had a partition, so I just mounted it. If yours is fresh, format it first (sudo mkfs.ext4 -L nextcloud-data /dev/sda1 — check the device name three times before running that).
sudo mkdir -p /mnt/usb
sudo mount /dev/sda1 /mnt/usb
sudo mkdir /mnt/usb/nextcloud-data
sudo chown www-data:www-data /mnt/usb/nextcloud-data
sudo chmod 750 /mnt/usb/nextcloud-data
Make it survive reboots. The nofail flag is important — without it, the board won't boot if the drive is unplugged:
echo "UUID=$(sudo blkid -s UUID -o value /dev/sda1) /mnt/usb ext4 defaults,nofail 0 2" | sudo tee -a /etc/fstab
Nextcloud itself
cd /tmp
wget https://download.nextcloud.com/server/releases/latest.zip
sudo unzip -q latest.zip -d /var/www/
sudo chown -R www-data:www-data /var/www/nextcloud
PHP tuning — /etc/php/8.5/fpm/conf.d/99-nextcloud.ini (drop the same file into cli/conf.d/ too, or occ will behave differently from the web app and confuse you later):
memory_limit = 512M
upload_max_filesize = 10G
post_max_size = 10G
max_execution_time = 360
output_buffering = Off
opcache.enable = 1
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 10000
opcache.memory_consumption = 256
opcache.save_comments = 1
opcache.revalidate_freq = 60
apc.enable_cli = 1
The nginx vhost — /etc/nginx/sites-available/nextcloud. This is adapted from Nextcloud's official nginx template; the non-obvious bits are the .well-known redirects (CalDAV/CardDAV break without them) and the front_controller_active param:
upstream php-handler {
server unix:/run/php/php8.5-fpm.sock;
}
server {
listen 80;
listen [::]:80;
server_name cloud.example.com;
root /var/www/nextcloud;
server_tokens off;
client_max_body_size 10G;
client_body_timeout 360s;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "no-referrer" always;
fastcgi_hide_header X-Powered-By;
index index.php index.html /index.php$request_uri;
location = /robots.txt { allow all; log_not_found off; access_log off; }
location ^~ /.well-known {
location = /.well-known/carddav { return 301 /remote.php/dav/; }
location = /.well-known/caldav { return 301 /remote.php/dav/; }
location /.well-known/acme-challenge { try_files $uri $uri/ =404; }
return 301 /index.php$request_uri;
}
location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) { return 404; }
location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; }
location ~ \.php(?:$|/) {
rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php$request_uri;
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
set $path_info $fastcgi_path_info;
try_files $fastcgi_script_name =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $path_info;
fastcgi_param HTTPS off;
fastcgi_param modHeadersAvailable true;
fastcgi_param front_controller_active true;
fastcgi_pass php-handler;
fastcgi_intercept_errors on;
fastcgi_request_buffering off;
fastcgi_max_temp_file_size 0;
fastcgi_read_timeout 360s;
}
location ~ \.(?:css|js|mjs|svg|gif|png|jpg|ico|wasm|map)$ {
try_files $uri /index.php$request_uri;
add_header Cache-Control "public, max-age=15778463";
access_log off;
}
location /remote { return 301 /remote.php$request_uri; }
location / {
try_files $uri $uri/ /index.php$request_uri;
}
}
Two things that bit people before you: one, add mjs to /etc/nginx/mime.types (application/javascript js mjs;) or parts of the web UI silently break. Two, if you'd rather use Apache2 — fine, the zip ships with a working .htaccess, just a2enmod rewrite headers env dir mime and point a vhost at /var/www/nextcloud. I still think nginx is worth it.
Enable and light it up:
sudo ln -s /etc/nginx/sites-available/nextcloud /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
sudo systemctl restart php8.5-fpm
The actual install
Skip the web wizard. The CLI installer is faster and scriptable:
sudo -u www-data php /var/www/nextcloud/occ maintenance:install \
--database pgsql --database-host 127.0.0.1 \
--database-name nextcloud --database-user nextcloud \
--database-pass 'pick-a-real-password' \
--admin-user admin --admin-pass 'a-better-password-than-this' \
--data-dir /mnt/usb/nextcloud-data
(--database mysql for the MariaDB crowd.)
Then the post-install config everyone forgets:
cd /var/www/nextcloud
sudo -u www-data php occ config:system:set trusted_domains 1 --value=cloud.example.com
sudo -u www-data php occ config:system:set memcache.local --value='\OC\Memcache\APCu'
sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis'
sudo -u www-data php occ config:system:set redis host --value=127.0.0.1
sudo -u www-data php occ config:system:set redis port --value=6379 --type=integer
sudo -u www-data php occ config:system:set default_phone_region --value=IN
sudo -u www-data php occ background:cron
And the cron job that actually runs background tasks (without this, thumbnails and cleanup silently never happen):
echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" | sudo crontab -u www-data -
Sanity check: curl http://<server-ip>/status.php should return JSON with "installed":true.
Part 2: The Docker way (if you hate fun)
If the above looked like a lot — fair. Here's the same thing as one docker-compose.yml:
services:
db:
image: postgres:16
restart: always
volumes:
- db:/var/lib/postgresql/data
environment:
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: pick-a-real-password
redis:
image: redis:alpine
restart: always
app:
image: nextcloud:apache # or nextcloud:fpm + an nginx container
restart: always
ports:
- 8080:80
volumes:
- nextcloud:/var/www/html
- /mnt/usb/nextcloud-data:/var/www/html/data
environment:
POSTGRES_HOST: db
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: pick-a-real-password
REDIS_HOST: redis
NEXTCLOUD_TRUSTED_DOMAINS: cloud.example.com
depends_on:
- db
- redis
volumes:
db:
nextcloud:
docker compose up -d and you're at the same place. Swap postgres:16 for mariadb:11 if you prefer. Honest take: Docker is easier to start, but the native install is easier to understand when something breaks at 2 AM — and something always breaks at 2 AM. Pick your poison knowingly.
Part 3: HTTPS without opening ports
My ISP puts me behind NAT anyway, so port forwarding was never on the table. Cloudflare Tunnel solved it in about four commands:
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64.deb
sudo dpkg -i cloudflared-linux-arm64.deb
cloudflared tunnel login # gives you a URL, open it, authorize your domain
cloudflared tunnel create mycloud
cloudflared tunnel route dns mycloud cloud.example.com
/etc/cloudflared/config.yml:
tunnel: <tunnel-id-from-the-create-command>
credentials-file: /etc/cloudflared/<tunnel-id>.json
ingress:
- hostname: cloud.example.com
service: http://localhost:80
- service: http_status:404
sudo cloudflared service install
sudo systemctl enable --now cloudflared
Valid HTTPS, real cert, zero open inbound ports. Then tell Nextcloud about its public identity:
sudo -u www-data php occ config:system:set overwriteprotocol --value=https
sudo -u www-data php occ config:system:set overwritehost --value=cloud.example.com
sudo -u www-data php occ config:system:set overwrite.cli.url --value=https://cloud.example.com
sudo -u www-data php occ config:system:set trusted_proxies 0 --value=127.0.0.1
That last one matters — without it, every visitor in your logs shows up as 127.0.0.1 and your brute-force protection can't tell attackers apart.
Since I own the domain anyway, I also themed the login page — logo, colors, the lot. This is what greets me now:
!Themed login page
Part 4: Locking it down
Since it's internet-facing, minimum viable paranoia:
# Firewall: LAN only for SSH and HTTP. The world only enters via the tunnel.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 192.168.1.0/24 to any port 22 proto tcp
sudo ufw allow from 192.168.1.0/24 to any port 80 proto tcp
sudo ufw --force enable
# fail2ban for SSH and Nextcloud logins
sudo apt install -y fail2ban
The Nextcloud jail needs a custom filter because the log is JSON — /etc/fail2ban/filter.d/nextcloud.conf:
[Definition]
_groupsre = (?:(?:,?\s*"\w+":(?:"(?:\\.|[^"])+"|\w+))*)
failregex = ^\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Login failed:
datepattern = ,?\s*"time"\s*:\s*"%%Y-%%m-%%dT%%H:%%M:%%S%%z"
/etc/fail2ban/jail.local:
[sshd]
enabled = true
maxretry = 4
bantime = 1h
[nextcloud]
enabled = true
port = 80,443
filter = nextcloud
logpath = /mnt/usb/nextcloud-data/nextcloud.log
maxretry = 5
bantime = 1h
Plus inside Nextcloud itself: enable the password_policy app (12+ chars, block breached passwords), turn on TOTP 2FA for your account, and enable the Suspicious Login app. Then run sudo -u www-data php occ setupchecks — it grades your homework. Keep going until it stops complaining.
Part 5: The phone part (the actual point of all this)
Install the Nextcloud Android app, type your domain, log in, and enable Auto upload for the camera folder. Then install the Memories app on the server (occ app:install memories, or from the web UI) — that's what gives you the Google-Photos-style timeline, map view, and "on this day" nostalgia hits. I also added Calendar, Contacts, Notes and Tasks; with DAVx⁵ from F-Droid, my phone's native calendar and contacts now sync to my closet instead of Mountain View.
What it looks like
All of these are from the actual box. I made a throwaway account with stock photos for the screenshots — my real library stays private, which is rather the point of the whole exercise.
!Dashboard — dark theme, recommended files
!Files — folders and uploads
!Photos app — timeline grid
!Memories — the Google Photos-style timeline
Was it worth it?
Honest answer: as a file-syncing appliance, Google is still smoother. Their search is magic, their apps are polished, and nobody calls you when their server breaks at 2 AM.
But my photos live on a drive I can unplug and hold in my hand. Nobody's training anything on them. Nobody's holding them for ransom at ₹1,300 a year. And when I pull a photo from three years ago on my phone, it's served out of my closet by a board that draws less power than the LED on my TV.
Some things you do because they're practical. Some you do because they're yours.
---
Full configs from this build are in the post — everything ran on Ubuntu (ARM64) with Nextcloud 35, nginx 1.28, PostgreSQL 18 and Redis 8. If you build this and something explodes, the comments are open.
Appendix: the full setup script
Everything in this post, automated. Also available as a downloadable file — read it first, then chmod +x setup-nextcloud.sh && ./setup-nextcloud.sh.
#!/usr/bin/env bash
#
# setup-nextcloud.sh — self-hosted Nextcloud on bare metal, no Docker, no snap.
#
# i run this on the little ARM board in my closet, but it works on any
# ubuntu/debian-ish x86 box too. it asks you a few questions, then sets up:
#
# nginx or apache + php-fpm + postgres or mariadb + redis
# nextcloud itself, cron, caching, and (optionally) a cloudflare tunnel
# so you get real https without opening ports on your router.
#
# read it before you run it. that's just good hygiene.
#
# chmod +x setup-nextcloud.sh && ./setup-nextcloud.sh
#
set -euo pipefail
# -- pretty output ------------------------------------------------------------
c_cyan=$'\e[36m'; c_green=$'\e[32m'; c_yellow=$'\e[33m'; c_red=$'\e[31m'; c_off=$'\e[0m'
say() { echo "${c_cyan}==>${c_off} $*"; }
ok() { echo "${c_green} ok${c_off} $*"; }
warn() { echo "${c_yellow} !!${c_off} $*"; }
die() { echo "${c_red} xx${c_off} $*" >&2; exit 1; }
# -- sanity checks --------------------------------------------------------------
[[ $EUID -eq 0 ]] && die "don't run me as root directly. run as your normal user; i'll sudo when needed."
command -v apt-get >/dev/null || die "this script speaks apt. debian/ubuntu only, sorry."
sudo -v || die "need sudo."
# fresh-booted ubuntu boxes love holding the dpkg lock hostage for a bit
say "waiting for any background apt runs to finish"
while sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do sleep 5; done
# -- questions ------------------------------------------------------------------
echo
say "a few questions before we start"
read -rp " public domain for the cloud (e.g. cloud.example.com, blank = lan only): " NC_DOMAIN
NC_DOMAIN=${NC_DOMAIN:-$(hostname -I | awk '{print $1}')}
read -rp " web server [nginx/apache] (default nginx): " WEB
WEB=${WEB:-nginx}
read -rp " database [postgres/mariadb] (default postgres): " DB
DB=${DB:-postgres}
read -rp " data directory (default /var/nextcloud-data): " DATA_DIR
DATA_DIR=${DATA_DIR:-/var/nextcloud-data}
read -rp " admin username (default admin): " ADMIN_USER
ADMIN_USER=${ADMIN_USER:-admin}
read -rsp " admin password: " ADMIN_PASS; echo
[[ -z "$ADMIN_PASS" ]] && die "empty password? nice try."
DB_PASS=$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)
# warn gently if the data dir is about to live on the root sd card
ROOT_SRC=$(findmnt -n -o SOURCE /)
DATA_SRC=$(findmnt -n -o SOURCE -T "$DATA_DIR" 2>/dev/null || echo "$ROOT_SRC")
if [[ "$DATA_SRC" == "$ROOT_SRC" && "$ROOT_SRC" == *mmcblk* ]]; then
warn "$DATA_DIR will sit on the SD card ($ROOT_SRC)."
warn "sd cards die young under database + thumbnail workloads."
read -rp " continue anyway? [y/N]: " YOLO
[[ "${YOLO,,}" == "y" ]] || die "mount a usb drive first, point the data dir at it, rerun me."
fi
# -- packages -------------------------------------------------------------------
say "installing packages"
sudo apt-get update -qq
PHP_PKGS="php-fpm php-gd php-curl php-zip php-xml php-mbstring php-intl php-bcmath php-gmp php-imagick php-redis php-apcu"
[[ "$DB" == postgres ]] && PHP_PKGS="$PHP_PKGS php-pgsql" || PHP_PKGS="$PHP_PKGS php-mysql"
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
redis-server unzip curl openssl ffmpeg \
$PHP_PKGS \
$( [[ "$WEB" == nginx ]] && echo nginx || echo "apache2 libapache2-mod-php" ) \
$( [[ "$DB" == postgres ]] && echo postgresql || echo mariadb-server )
PHP_VER=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
PHP_SOCK="/run/php/php${PHP_VER}-fpm.sock"
ok "php $PHP_VER, $WEB, $DB, redis — all in"
# -- database -------------------------------------------------------------------
say "setting up the database"
if [[ "$DB" == postgres ]]; then
sudo -u postgres psql -v ON_ERROR_STOP=1 <<SQL
CREATE USER nextcloud WITH PASSWORD '${DB_PASS}';
CREATE DATABASE nextcloud OWNER nextcloud TEMPLATE template0 ENCODING 'UTF8';
SQL
else
sudo mariadb <<SQL
CREATE DATABASE nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER 'nextcloud'@'localhost' IDENTIFIED BY '${DB_PASS}';
GRANT ALL PRIVILEGES ON nextcloud.* TO 'nextcloud'@'localhost';
FLUSH PRIVILEGES;
SQL
fi
ok "database ready"
# -- nextcloud ------------------------------------------------------------------
say "fetching nextcloud"
wget -q https://download.nextcloud.com/server/releases/latest.zip -O /tmp/nextcloud.zip
sudo unzip -q /tmp/nextcloud.zip -d /var/www/
sudo mkdir -p "$DATA_DIR"
sudo chown -R www-data:www-data /var/www/nextcloud "$DATA_DIR"
sudo chmod 750 "$DATA_DIR"
ok "nextcloud unpacked to /var/www/nextcloud"
# -- php tuning -----------------------------------------------------------------
say "tuning php"
sudo tee /etc/php/${PHP_VER}/fpm/conf.d/99-nextcloud.ini >/dev/null <<'INI'
memory_limit = 512M
upload_max_filesize = 10G
post_max_size = 10G
max_execution_time = 360
output_buffering = Off
opcache.enable = 1
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 10000
opcache.memory_consumption = 256
opcache.save_comments = 1
opcache.revalidate_freq = 60
apc.enable_cli = 1
INI
# cli needs the same settings or occ behaves differently from the web app.
sudo cp /etc/php/${PHP_VER}/fpm/conf.d/99-nextcloud.ini /etc/php/${PHP_VER}/cli/conf.d/99-nextcloud.ini
# -- web server -----------------------------------------------------------------
say "configuring $WEB"
if [[ "$WEB" == nginx ]]; then
# .mjs missing from mime.types breaks parts of the ui. ask me how i know.
grep -q 'mjs' /etc/nginx/mime.types || \
sudo sed -i 's|application/javascript\s\+js;|application/javascript js mjs;|' /etc/nginx/mime.types
sudo tee /etc/nginx/sites-available/nextcloud >/dev/null <<NGINX
upstream php-handler {
server unix:${PHP_SOCK};
}
server {
listen 80;
listen [::]:80;
server_name ${NC_DOMAIN};
root /var/www/nextcloud;
server_tokens off;
client_max_body_size 10G;
client_body_timeout 360s;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "no-referrer" always;
fastcgi_hide_header X-Powered-By;
index index.php index.html /index.php\$request_uri;
location = /robots.txt { allow all; log_not_found off; access_log off; }
location ^~ /.well-known {
location = /.well-known/carddav { return 301 /remote.php/dav/; }
location = /.well-known/caldav { return 301 /remote.php/dav/; }
location /.well-known/acme-challenge { try_files \$uri \$uri/ =404; }
return 301 /index.php\$request_uri;
}
location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:\$|/) { return 404; }
location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; }
location ~ \.php(?:\$|/) {
rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php\$request_uri;
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
set \$path_info \$fastcgi_path_info;
try_files \$fastcgi_script_name =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
fastcgi_param PATH_INFO \$path_info;
fastcgi_param HTTPS off;
fastcgi_param modHeadersAvailable true;
fastcgi_param front_controller_active true;
fastcgi_pass php-handler;
fastcgi_intercept_errors on;
fastcgi_request_buffering off;
fastcgi_max_temp_file_size 0;
fastcgi_read_timeout 360s;
}
location ~ \.(?:css|js|mjs|svg|gif|png|jpg|ico|wasm|map)\$ {
try_files \$uri /index.php\$request_uri;
add_header Cache-Control "public, max-age=15778463";
access_log off;
}
location /remote { return 301 /remote.php\$request_uri; }
location / {
try_files \$uri \$uri/ /index.php\$request_uri;
}
}
NGINX
sudo ln -sf /etc/nginx/sites-available/nextcloud /etc/nginx/sites-enabled/nextcloud
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
else
sudo a2enmod rewrite headers env dir mime ssl >/dev/null
sudo tee /etc/apache2/sites-available/nextcloud.conf >/dev/null <<APACHE
<VirtualHost *:80>
ServerName ${NC_DOMAIN}
DocumentRoot /var/www/nextcloud
<Directory /var/www/nextcloud>
Require all granted
AllowOverride All
Options FollowSymLinks MultiViews
</Directory>
</VirtualHost>
APACHE
sudo a2ensite nextcloud >/dev/null
sudo a2dissite 000-default >/dev/null 2>&1 || true
sudo systemctl reload apache2
fi
sudo systemctl restart "php${PHP_VER}-fpm"
ok "$WEB is serving"
# -- install --------------------------------------------------------------------
say "running the nextcloud installer"
if [[ "$DB" == postgres ]]; then
DBARGS=(--database pgsql --database-host 127.0.0.1)
else
DBARGS=(--database mysql)
fi
sudo -u www-data php /var/www/nextcloud/occ maintenance:install \
"${DBARGS[@]}" \
--database-name nextcloud --database-user nextcloud --database-pass "$DB_PASS" \
--admin-user "$ADMIN_USER" --admin-pass "$ADMIN_PASS" \
--data-dir "$DATA_DIR"
ok "installed"
# -- config ---------------------------------------------------------------------
say "wiring up cache, cron, trusted domains"
occ() { sudo -u www-data php /var/www/nextcloud/occ "$@"; }
occ config:system:set trusted_domains 1 --value="$NC_DOMAIN"
occ config:system:set memcache.local --value='\OC\Memcache\APCu'
occ config:system:set memcache.locking --value='\OC\Memcache\Redis'
occ config:system:set redis host --value=127.0.0.1
occ config:system:set redis port --value=6379 --type=integer
occ config:system:set default_phone_region --value=IN
occ background:cron
echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" | sudo crontab -u www-data -
# -- optional: cloudflare tunnel --------------------------------------------------
echo
read -rp " set up a cloudflare tunnel for https + remote access? [y/N]: " TUNNEL
if [[ "${TUNNEL,,}" == "y" ]]; then
say "installing cloudflared"
ARCH=$(dpkg --print-architecture)
wget -q "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${ARCH}.deb" -O /tmp/cloudflared.deb
sudo dpkg -i /tmp/cloudflared.deb
warn "this next step prints a url — open it in your browser and authorize the domain."
read -rp " press enter when ready" _
cloudflared tunnel login
cloudflared tunnel create nextcloud
cloudflared tunnel route dns nextcloud "$NC_DOMAIN"
TUNNEL_ID=$(cloudflared tunnel list 2>/dev/null | awk '/nextcloud/ {print $1}' | head -1)
sudo mkdir -p /etc/cloudflared
sudo cp "$HOME/.cloudflared/${TUNNEL_ID}.json" /etc/cloudflared/
sudo chmod 600 /etc/cloudflared/"${TUNNEL_ID}".json
sudo tee /etc/cloudflared/config.yml >/dev/null <<YAML
tunnel: ${TUNNEL_ID}
credentials-file: /etc/cloudflared/${TUNNEL_ID}.json
ingress:
- hostname: ${NC_DOMAIN}
service: http://localhost:80
- service: http_status:404
YAML
sudo cloudflared service install
sudo systemctl enable --now cloudflared
occ config:system:set overwriteprotocol --value=https
occ config:system:set overwritehost --value="$NC_DOMAIN"
occ config:system:set overwrite.cli.url --value="https://${NC_DOMAIN}"
# without this every visitor looks like 127.0.0.1 and brute-force protection goes blind
occ config:system:set trusted_proxies 0 --value=127.0.0.1
ok "tunnel live — https://${NC_DOMAIN}"
fi
# -- optional: hardening ----------------------------------------------------------
echo
read -rp " add firewall + fail2ban hardening? [Y/n]: " HARDEN
if [[ "${HARDEN,,}" != "n" ]]; then
say "hardening"
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ufw fail2ban
MY_NET=$(ip -4 route show default | head -1 | awk '{print $3}' | sed 's/\.[0-9]*$/.0\/24/')
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from "$MY_NET" to any port 22 proto tcp
sudo ufw allow from "$MY_NET" to any port 80 proto tcp
sudo ufw --force enable
# nextcloud logs failed logins as json; stock fail2ban has no filter for that
sudo tee /etc/fail2ban/filter.d/nextcloud.conf >/dev/null <<'F2B'
[Definition]
_groupsre = (?:(?:,?\s*"\w+":(?:"(?:\\.|[^"])+"|\w+))*)
failregex = ^\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Login failed:
datepattern = ,?\s*"time"\s*:\s*"%%Y-%%m-%%dT%%H:%%M:%%S%%z"
F2B
sudo tee /etc/fail2ban/jail.local >/dev/null <<JAIL
[sshd]
enabled = true
maxretry = 4
bantime = 1h
[nextcloud]
enabled = true
port = 80,443
filter = nextcloud
logpath = ${DATA_DIR}/nextcloud.log
maxretry = 5
bantime = 1h
JAIL
sudo systemctl enable --now fail2ban
sudo systemctl restart fail2ban
occ app:enable password_policy >/dev/null 2>&1 || true
occ config:app:set password_policy minLength --value=12
occ config:app:set password_policy enforceUpperLowerCase --value=1
occ config:app:set password_policy enforceNumericCharacters --value=1
occ config:app:set password_policy enforceSpecialCharacters --value=1
occ config:app:set password_policy enforceNonCommonPassword --value=1
ok "ufw + fail2ban + password policy active"
fi
# -- done -------------------------------------------------------------------------
echo
say "done. here's the summary:"
echo " url: $([[ "${TUNNEL,,}" == "y" ]] && echo "https://${NC_DOMAIN}" || echo "http://${NC_DOMAIN}")"
echo " admin: ${ADMIN_USER} (password = the one you typed)"
echo " data: ${DATA_DIR}"
echo " db: ${DB} (user nextcloud, pass ${DB_PASS} — also in /var/www/nextcloud/config/config.php)"
echo
echo " phone app: install 'Nextcloud' from the play store / f-droid,"
echo " type the url above, log in, enable auto upload. that's it."
echo
echo " last thing — run this and fix whatever it flags:"
echo " sudo -u www-data php /var/www/nextcloud/occ setupchecks"
echo
---
Originally published on CLI-NERDS — practical Linux, DevOps, and self-hosting guides with free browser-based developer tools.
3 comments
Mentions
My self-hosting graveyard is longer than the list of survivors I didn't set up my first server for anything this post is about (for reasons of my own, that part stays a secret under seven seals:D), but it very quickly found a perfectly legitimate and much more boring purpose: I needed a place to deploy my own pet projects - for tests, for demos to friends, to poke at something live instead of localhost - without running into Vercel and Render with their free tiers, strict per-service limits and subscriptions that grow faster than my projects do A VPS has only one limit, the hardware, and as it turned out, that's much easier to negotiate with than someone else's price list ;) Then I noticed that most of the time this hardware just sits idle, and I asked myself a fair question: what exactly am I paying for? Right around then I stumbled on the awesome-selfhosted repo, and down the rabbit hole I went… :D Right now there are three machines, not counting my main PC: a small rented VPS in Europe with 3 gigabytes of memory and 30 gigabytes of disk, which everything that has to be reachable from the internet goes through; a home server that lives at my family's place, because electricity and internet are simply cheaper there; and a small VPS for learning and doing scary things by hand (so I don't end up restoring the production box from backups), with 1 gigabyte of RAM and about 10 gigabytes of disk The home media server runs a Ryzen 3 PRO 3200G, 12 gigabytes of memory, a 480-gigabyte system SSD and two hard drives: a 4 TB WD Gold for everything valuable and an 8 TB Toshiba N300 for media They're called tank and junk, and I don't think I need to explain which is which :D Tank needs a second drive to mirror it, so the backups don't live on a single disk, but storage prices are such that it's easier to buy a Chevrolet Camaro than a new hard drive that size, and good deals on the used market are rare The current one was a lucky find: about $80 for a WD Gold WD4002FYYZ with just over 200 power cycles and a couple of years of service (and of course I ran the tests, the drive is livelier than plenty of modern ones) In all honesty: of those 12 gigabytes, the system currently sees less than ten, because the CMOS battery on the motherboard is dead, so after every power cut it forgets my settings and once again generously hands two gigabytes to the integrated graphics, which a server with no monitor has roughly zero use for For about two months now I've been promising myself every weekend to swap the battery and take that memory back, but, as you can probably tell, I'm a lazy ass, and my weekends are taken up by more important things (lying around, doomscrolling Instagram reels, playing with my cat, sinking ten hours into Civilization V... you know how it is:D) The front door The VPS mostly hosts what glues everything else together and keeps it safe, plus a couple of things I use every day My blog: about a hundred megabytes Homepage: a start page with a tile for every service Out of the box it's boring, and I wanted something pretty, so mine has glass, a background and my own palette, and I use it quite often, because its UI and UX really are great Uptime Kuma: monitoring Strictly speaking, having a server check whether it's still alive is a so-so idea, and once mine was down for half an hour without anyone noticing, but it keeps a proper eye on the containers, and that's why I keep it (the obvious fix is to run it on the home server and watch the VPS from outside, it's on the list) Beszel: CPU, memory and disk graphs for every machine Right now it just sits there: Kuma and Termix cover pretty much everything I need from it, so Beszel is like that distant relative you used to be really close to and now call once a year :D Termix: SSH right in the browser Nexterm lived exactly one day before it, and Termix stayed because it won on every count at once: features, easy setup, convenience, looks and snippets, and I honestly don't know how I managed servers from my phone before them Memos: notes where I write down absolutely everything, and it has completely replaced Saved Messages in Telegram for me I'm a fan of minimalism and simplicity, and I don't see the point of an app where I'll never touch most of the features, and most importantly, it's self-hosted, btw The idea of bolting AI onto it for tags and summaries is sitting in the backlog: there are higher-priority projects right now, but SOMEDAY I'll automate this part of my life:D MeshCentral: remote access to my main PC All my code and Claude Code live on it, and I often work away from home, so I needed access from anywhere, right in the browser, without installing anything on the machine I'm connecting from Donetick: household chores for the family, and right now it's on pause, because I still have questions about the interface and some of the features But it comes with my favorite story: I forked its web app to translate it, and it turned into a full i18n integration and some real help for the maintainer, so now I'm listed as a contributor in donetick/frontend Vaultwarden: a password manager I used Google Password Manager for years, and that was perfectly fine right up until I started running my own services and suddenly realized I was seriously worried about where and how my data was stored, and since I'm running a server anyway, it made sense to keep my passwords there too Pocket ID: passkey sign-in to almost all my services, no passwords When you have twenty services, typing a password into each one gets old within half an hour, and I also wanted my self-written MCP servers to sit behind my own security layer, not behind some third-party platform I'd have to take at its word Backrest: nightly backups that go to the cloud and then get pulled home Knock on wood, I haven't had to restore from them yet, apart from a test run, and I picked it because I like seeing what's actually happening: I enjoy working in the console, but a UI is still more familiar and more pleasant IT-Tools: a set of small converters and generators It can stay :D And two of my own pet projects: MindAmp, mind maps with an LLM layer that speeds up the work and boosts the research part with agents, no third-party subscriptions needed, and VigilAMP, a registry of my projects and their current status that keeps me from getting lost in what I'm working on and what stage it's at All of this fits into one and a half gigabytes of memory, with about as much left in reserve, and the disk is 85% full Yes, I know, and no, I'm not doing anything about it today:/ The warehouse The home server is for the heavy stuff: things that take up a lot of space, and things I'd rather not keep on rented hardware The rule here is simple: I test a service on myself first and only then bring the family in, which is why some of these are still just mine Nextcloud with OnlyOffice: my files and documents, a replacement for Google Drive, which is now just a backup People often call Nextcloud heavy, and the numbers I'd read were scary: 1-1.2 GB of RAM against 300-400 MB for Seafile, so I was all set to go with Seafile Then I decided not to take it on faith and ran both side by side on the home server At idle, the whole Seafile stack (Seafile, MariaDB, memcached) sat at about 450 MiB, and Nextcloud with Postgres and Redis at about 270 Right now the Nextcloud stack, cron included, idles at around 240 MiB, and with OnlyOffice on top it's about 415, roughly 4% of the 9.6 GB my dead battery lets me have OnlyOffice is the only container with a cap (2 GB), because it spins up a worker for every open document and easily climbs into the hundreds of MB while someone's editing These are idle numbers Previews and big syncs make Nextcloud heavier, and my setup is gentle: SSD, LAN only, not much data Still, I haven't run into any problems, it's stable and pretty spry, and on my hardware the memory argument fell apart, so Nextcloud stayed and Seafile went (I could be wrong, ofc, but I'm too lazy to test it under load, and I don't use it that much anyway: it's basically a glorified file dump with a few nice apps that let me get to my files from anywhere) Immich: photos, a great Google Photos replacement, and it's still at the "me first" stage before I bring the family in BookLore: a book library, and I chose it for FB2 support, because my whole library is in that format So far I'm the only one using it BookLore has since gone into maintenance mode, and its developer named BookOrbit as the official successor, so right now I'm trying it out next to BookLore, but I don't have an opinion on it yet Jellyfin: movies and TV shows It's fully open and free, does exactly what it needs to, and these days we watch almost everything I own through it at home SearXNG: a metasearch engine, set up for privacy and security, like most of my services I've barely used it seriously yet Navidrome: my music library, and I listen to it every day: all my music goes through it now, no subscriptions, no limits On my PC I listen through Psysonic, and on my phone through Solound The graveyard The list of things I've removed is longer than the list of things that run, and every one of the deceased had its own cause of death And these are just the ones worth a eulogy: in total around fifty services got installed and then deleted Some got a second chance, like Dashy and Karakeep, and Windmill filled the disk to 100% Ghost and Astro with Keystatic were my first two shots at my blog, and each lasted about two days Ghost brought a crowded communal apartment of other people's services, and Keystatic kept its admin on Vite's dev server, the one you're not supposed to expose to the internet So I ended up writing my own Finding a browser terminal took longer than I'd like ttyd dropped the connection every few minutes Cockpit worked, but I only needed it for the terminal and files, and dragging in a whole server control panel for that is overkill Nexterm was lightweight, but it was still in beta Termix won Seafile was supposed to be the lightweight one and lost to Nextcloud on memory (the numbers are up in the warehouse) Termix also finished off Portainer: it turned out I'd rather manage containers straight from the terminal than click through yet another panel Karakeep, bookmarks with previews, turned out to be three containers, 3.5 GB of images and 700 MB of memory Without the built-in Chrome for previews it's a completely different product, and keeping something like that on a VPS with three gigabytes is a luxury I can't afford Trilium was out because of its cluttered interface and broken built-in AI Minimalism, remember? Notes went to Memos in the end Anki Sync Server lasted two days: I set it up for my wife, my wife gave up on AnkiDroid, and there was nothing left to sync And Inbox Zero never even made it to installation: I wrote a whole spec for it and then decided I didn't need it (and there were security questions too, but that stays between us:D) My rule is this: every new service gets compared to what already works, and if it doesn't click quickly and isn't critical, it gets removed without hours of debugging, which is exactly why 3 gigabytes is still enough Why all this Partly for control: my photos and files sit on disks I can touch with my own hands, and my passwords at least live on a server I run myself But mostly for learning: I write backend in Python and I'm learning everything around the code, and the servers are my sandbox for that, from networking and Docker to backups, monitoring and security And when something breaks at three in the morning, it's my fault and my lesson ¯\_(ツ)_/¯ So what's in your graveyard, and what killed it?
💸 网盘一年三百多,到期还限速到一百多 K。Nextcloud 三万七千星,自建私有云盘,相册自动备份+文件夹同步,容量看硬盘。 nextcloud/server
Unstructured; Supabase: Postgres development platform; yt-dlp | GitHub Curated Picks | 2026-10-23 GitHub Curated Picks reviews 14 source-verified repositories selected for developer usefulness, not popularity position. Each pick is checked against its GitHub repository, README, release evidence, and recent commit; pick numbers are editorial order. The compact card keeps rank, signal, why it matters, and next check together while the ledger preserves direct URLs and release evidence. Treat the list as an educational review surface, not software endorsement, and verify license and maintenance before adoption. Chapters: 00:00 2026-10-23 | GitHub Curated Picks 00:05 Pick #1 Unstructured 00:30 Pick #2 Supabase 00:44 Pick #3 yt-dlp 01:01 Pick #4 Langchain 01:27 Pick #5 Awesome Mac 01:42 Pick #6 Trivy 01:56 Pick #7 Vite 02:14 Pick #8 Jellyfin 02:32 Pick #9 Black 02:51 Pick #10 Coolify 03:13 Pick #11 Doris 03:24 Pick #12 Onnxruntime 03:36 Pick #13 Fluentui 03:49 Pick #14 Server Source ledger: github.com/trending api.github.com github.com/Unstructured-IO/unstructured/blob/main… github.com/Unstructured-IO/unstructured/commit/49… github.com/Unstructured-IO/unstructured/releases/… api.github.com github.com/supabase/supabase/blob/master/README.md github.com/supabase/supabase/commit/2193f2acdf8ef… api.github.com github.com/yt-dlp/yt-dlp/blob/master/README.md github.com/yt-dlp/yt-dlp/commit/9b58cb33d0cd3e823… api.github.com langchain-ai/langchain github.com/langchain-ai/langchain/commit/b84fa61e… github.com/langchain-ai/langchain/releases/tag/la… api.github.com github.com/jaywcjlove/awesome-mac/blob/master/REA… github.com/jaywcjlove/awesome-mac/commit/965f263f… api.github.com github.com/aquasecurity/trivy/blob/main/README.md github.com/aquasecurity/trivy/commit/0231990f702f… api.github.com github.com/vitejs/vite/blob/main/README.md github.com/vitejs/vite/commit/a236886026c3a5de068… api.github.com github.com/jellyfin/jellyfin/blob/master/README.md github.com/jellyfin/jellyfin/releases/tag/v12.3 api.github.com github.com/psf/black/blob/main/README.md github.com/psf/black/commit/f33c3e3c66263d4a3a0e1… api.github.com coollabsio/coolify github.com/coollabsio/coolify/commit/0b2e931a4258… api.github.com github.com/apache/doris/blob/master/README.md github.com/apache/doris/commit/6e5a8929fbaba2ed2a… api.github.com microsoft/onnxruntime github.com/microsoft/onnxruntime/commit/fe7700a7e… api.github.com microsoft/fluentui github.com/microsoft/fluentui/commit/ea1a99bf2bc5… api.github.com nextcloud/server github.com/nextcloud/server/commit/fc9b07d9591f1f… Unstructured-IO/unstructured supabase/supabase yt-dlp/yt-dlp jaywcjlove/awesome-mac aquasecurity/trivy vitejs/vite jellyfin/jellyfin psf/black apache/doris Informational and educational purposes only. Not investment advice, product endorsement, or professional guidance.
Maßnahme notwendig! Wenn Du ein Nextcloud am laufen hast, welcher aus dem Internet erreichbar ist, dann mußt Du jetzt tätig werden und ein Update einspielen! server#65754
server#65754 PHPのApacheイメージがでるまでのんびり待ちますか
Want your own cloud for files and photos? ☁️ Nextcloud lets you host file sync and sharing, with apps for calendars and contacts. Keep control of your setup. 📁 nextcloud/server #SelfHosted #浇朋友 #蓝朋友 #互关 #fo
文件、照片总分散在不同云盘?☁️ Nextcloud 可自建文件同步和分享服务,也能扩展日历、联系人等应用。给自己搭个私有云。📁 nextcloud/server #SelfHosted #浇朋友 #蓝朋友 #互关 #fo
⚠️ PHPのCVE(共通脆弱性識別子)に関する注意喚起! · Issue #65754 · nextcloud/server server#65754
I Replaced Google Photos With a $60 Board in My Closet --- title: I Replaced Google Photos With a $60 Board in My Closet published: true description: Google wanted ₹1,300/year to keep my own photos. So I pointed a domain at an ARM board, installed Nextcloud on bare metal, and got my library back — with nginx, PostgreSQL, a USB drive, and zero open ports. tags: selfhosting, nextcloud, privacy, linux canonical_url: clinerds.com cover_image: clinerds.com --- Google Photos was great until it wasn't. The free tier died years ago, my "Storage almost full" notification had become a weekly ritual, and the idea of paying rent forever to access my own memories started to genuinely annoy me. So last weekend I did what any reasonable person with a spare ARM board and trust issues would do: I built my own Google Photos. The whole thing runs on a 4-core ARM board with 8 GB of RAM sitting in my closet. Photos auto-upload from my phone, I get a timeline view, a map, sharing links, the works. Total cost of ownership: the electricity a 5-watt board draws. Here's exactly how I did it — the native way and the Docker way — with the mistakes included, because that's where the learning is. !Hero — the CLINERDS cloud Too long; just run it: the whole native setup is also packaged as an interactive script — it asks you a handful of questions (domain, nginx or apache, postgres or mariadb, data dir) and does the rest. Grab setup-nextcloud.sh, read it (always read scripts before piping them into your life), then chmod +x setup-nextcloud.sh && ./setup-nextcloud.sh. The post below is the "what and why" behind everything the script does. The stack (and why) - Nextcloud — the obvious pick. It's the only self-hosted suite with a mature Android app that does camera auto-upload properly. - nginx + PHP-FPM instead of Apache. Nextcloud ships .htaccess files and "officially" prefers Apache, but their docs publish a full nginx config, and nginx uses a fraction of the RAM. On a small board that matters. - PostgreSQL instead of MySQL/MariaDB. Nextcloud's own docs recommend it, file locking behaves better under concurrency, and you skip the whole utf8mb4 / row-format dance MariaDB makes you do. - A 128 GB USB drive for data. SD cards die under database + thumbnail workloads. Don't store your photos on the boot card. - Cloudflare Tunnel for HTTPS and remote access. No port forwarding, no exposing my home IP, free valid TLS. Part 1: The native install This is the path I actually ran. Everything below is copy-paste real. Packages sudo apt update sudo apt install -y nginx postgresql redis-server \ php-fpm php-pgsql php-gd php-curl php-zip php-xml php-mbstring \ php-intl php-bcmath php-gmp php-imagick php-redis php-apcu \ unzip ffmpeg (If you're team MySQL: swap postgresql for mariadb-server and php-pgsql for php-mysql. Everything else is identical. I'll show both database setups below.) First gotcha, straight from my terminal history: on a freshly booted Ubuntu, unattended-upgrades holds the dpkg lock hostage for a few minutes and apt just fails. Either wait it out or: while sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do sleep 10; done Database PostgreSQL: sudo -u postgres psql CREATE USER nextcloud WITH PASSWORD 'pick-a-real-password'; CREATE DATABASE nextcloud OWNER nextcloud TEMPLATE template0 ENCODING 'UTF8'; Or MariaDB, if that's your religion: sudo mariadb CREATE DATABASE nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER 'nextcloud'@'localhost' IDENTIFIED BY 'pick-a-real-password'; GRANT ALL PRIVILEGES ON nextcloud.* TO 'nextcloud'@'localhost'; FLUSH PRIVILEGES; One thing to check before you pick a PHP version: Nextcloud is picky about it. There's a compatibility matrix on their wiki — check it before apt installs whatever your distro's default is. My Ubuntu shipped PHP 8.5, which Nextcloud 34/35 supports. Lucky. An older release would have needed a PPA dance. The data directory (on the USB drive) My drive already had a partition, so I just mounted it. If yours is fresh, format it first (sudo mkfs.ext4 -L nextcloud-data /dev/sda1 — check the device name three times before running that). sudo mkdir -p /mnt/usb sudo mount /dev/sda1 /mnt/usb sudo mkdir /mnt/usb/nextcloud-data sudo chown www-data:www-data /mnt/usb/nextcloud-data sudo chmod 750 /mnt/usb/nextcloud-data Make it survive reboots. The nofail flag is important — without it, the board won't boot if the drive is unplugged: echo "UUID=$(sudo blkid -s UUID -o value /dev/sda1) /mnt/usb ext4 defaults,nofail 0 2" | sudo tee -a /etc/fstab Nextcloud itself cd /tmp wget https://download.nextcloud.com/server/releases/latest.zip sudo unzip -q latest.zip -d /var/www/ sudo chown -R www-data:www-data /var/www/nextcloud PHP tuning — /etc/php/8.5/fpm/conf.d/99-nextcloud.ini (drop the same file into cli/conf.d/ too, or occ will behave differently from the web app and confuse you later): memory_limit = 512M upload_max_filesize = 10G post_max_size = 10G max_execution_time = 360 output_buffering = Off opcache.enable = 1 opcache.interned_strings_buffer = 16 opcache.max_accelerated_files = 10000 opcache.memory_consumption = 256 opcache.save_comments = 1 opcache.revalidate_freq = 60 apc.enable_cli = 1 The nginx vhost — /etc/nginx/sites-available/nextcloud. This is adapted from Nextcloud's official nginx template; the non-obvious bits are the .well-known redirects (CalDAV/CardDAV break without them) and the front_controller_active param: upstream php-handler { server unix:/run/php/php8.5-fpm.sock; } server { listen 80; listen [::]:80; server_name cloud.example.com; root /var/www/nextcloud; server_tokens off; client_max_body_size 10G; client_body_timeout 360s; add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "no-referrer" always; fastcgi_hide_header X-Powered-By; index index.php index.html /index.php$request_uri; location = /robots.txt { allow all; log_not_found off; access_log off; } location ^~ /.well-known { location = /.well-known/carddav { return 301 /remote.php/dav/; } location = /.well-known/caldav { return 301 /remote.php/dav/; } location /.well-known/acme-challenge { try_files $uri $uri/ =404; } return 301 /index.php$request_uri; } location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) { return 404; } location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; } location ~ \.php(?:$|/) { rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php$request_uri; fastcgi_split_path_info ^(.+?\.php)(/.*)$; set $path_info $fastcgi_path_info; try_files $fastcgi_script_name =404; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $path_info; fastcgi_param HTTPS off; fastcgi_param modHeadersAvailable true; fastcgi_param front_controller_active true; fastcgi_pass php-handler; fastcgi_intercept_errors on; fastcgi_request_buffering off; fastcgi_max_temp_file_size 0; fastcgi_read_timeout 360s; } location ~ \.(?:css|js|mjs|svg|gif|png|jpg|ico|wasm|map)$ { try_files $uri /index.php$request_uri; add_header Cache-Control "public, max-age=15778463"; access_log off; } location /remote { return 301 /remote.php$request_uri; } location / { try_files $uri $uri/ /index.php$request_uri; } } Two things that bit people before you: one, add mjs to /etc/nginx/mime.types (application/javascript js mjs;) or parts of the web UI silently break. Two, if you'd rather use Apache2 — fine, the zip ships with a working .htaccess, just a2enmod rewrite headers env dir mime and point a vhost at /var/www/nextcloud. I still think nginx is worth it. Enable and light it up: sudo ln -s /etc/nginx/sites-available/nextcloud /etc/nginx/sites-enabled/ sudo rm /etc/nginx/sites-enabled/default sudo nginx -t && sudo systemctl reload nginx sudo systemctl restart php8.5-fpm The actual install Skip the web wizard. The CLI installer is faster and scriptable: sudo -u www-data php /var/www/nextcloud/occ maintenance:install \ --database pgsql --database-host 127.0.0.1 \ --database-name nextcloud --database-user nextcloud \ --database-pass 'pick-a-real-password' \ --admin-user admin --admin-pass 'a-better-password-than-this' \ --data-dir /mnt/usb/nextcloud-data (--database mysql for the MariaDB crowd.) Then the post-install config everyone forgets: cd /var/www/nextcloud sudo -u www-data php occ config:system:set trusted_domains 1 --value=cloud.example.com sudo -u www-data php occ config:system:set memcache.local --value='\OC\Memcache\APCu' sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis' sudo -u www-data php occ config:system:set redis host --value=127.0.0.1 sudo -u www-data php occ config:system:set redis port --value=6379 --type=integer sudo -u www-data php occ config:system:set default_phone_region --value=IN sudo -u www-data php occ background:cron And the cron job that actually runs background tasks (without this, thumbnails and cleanup silently never happen): echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" | sudo crontab -u www-data - Sanity check: curl http://<server-ip>/status.php should return JSON with "installed":true. Part 2: The Docker way (if you hate fun) If the above looked like a lot — fair. Here's the same thing as one docker-compose.yml: services: db: image: postgres:16 restart: always volumes: - db:/var/lib/postgresql/data environment: POSTGRES_DB: nextcloud POSTGRES_USER: nextcloud POSTGRES_PASSWORD: pick-a-real-password redis: image: redis:alpine restart: always app: image: nextcloud:apache # or nextcloud:fpm + an nginx container restart: always ports: - 8080:80 volumes: - nextcloud:/var/www/html - /mnt/usb/nextcloud-data:/var/www/html/data environment: POSTGRES_HOST: db POSTGRES_DB: nextcloud POSTGRES_USER: nextcloud POSTGRES_PASSWORD: pick-a-real-password REDIS_HOST: redis NEXTCLOUD_TRUSTED_DOMAINS: cloud.example.com depends_on: - db - redis volumes: db: nextcloud: docker compose up -d and you're at the same place. Swap postgres:16 for mariadb:11 if you prefer. Honest take: Docker is easier to start, but the native install is easier to understand when something breaks at 2 AM — and something always breaks at 2 AM. Pick your poison knowingly. Part 3: HTTPS without opening ports My ISP puts me behind NAT anyway, so port forwarding was never on the table. Cloudflare Tunnel solved it in about four commands: wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64.deb sudo dpkg -i cloudflared-linux-arm64.deb cloudflared tunnel login # gives you a URL, open it, authorize your domain cloudflared tunnel create mycloud cloudflared tunnel route dns mycloud cloud.example.com /etc/cloudflared/config.yml: tunnel: <tunnel-id-from-the-create-command> credentials-file: /etc/cloudflared/<tunnel-id>.json ingress: - hostname: cloud.example.com service: http://localhost:80 - service: http_status:404 sudo cloudflared service install sudo systemctl enable --now cloudflared Valid HTTPS, real cert, zero open inbound ports. Then tell Nextcloud about its public identity: sudo -u www-data php occ config:system:set overwriteprotocol --value=https sudo -u www-data php occ config:system:set overwritehost --value=cloud.example.com sudo -u www-data php occ config:system:set overwrite.cli.url --value=https://cloud.example.com sudo -u www-data php occ config:system:set trusted_proxies 0 --value=127.0.0.1 That last one matters — without it, every visitor in your logs shows up as 127.0.0.1 and your brute-force protection can't tell attackers apart. Since I own the domain anyway, I also themed the login page — logo, colors, the lot. This is what greets me now: !Themed login page Part 4: Locking it down Since it's internet-facing, minimum viable paranoia: # Firewall: LAN only for SSH and HTTP. The world only enters via the tunnel. sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow from 192.168.1.0/24 to any port 22 proto tcp sudo ufw allow from 192.168.1.0/24 to any port 80 proto tcp sudo ufw --force enable # fail2ban for SSH and Nextcloud logins sudo apt install -y fail2ban The Nextcloud jail needs a custom filter because the log is JSON — /etc/fail2ban/filter.d/nextcloud.conf: [Definition] _groupsre = (?:(?:,?\s*"\w+":(?:"(?:\\.|[^"])+"|\w+))*) failregex = ^\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Login failed: datepattern = ,?\s*"time"\s*:\s*"%%Y-%%m-%%dT%%H:%%M:%%S%%z" /etc/fail2ban/jail.local: [sshd] enabled = true maxretry = 4 bantime = 1h [nextcloud] enabled = true port = 80,443 filter = nextcloud logpath = /mnt/usb/nextcloud-data/nextcloud.log maxretry = 5 bantime = 1h Plus inside Nextcloud itself: enable the password_policy app (12+ chars, block breached passwords), turn on TOTP 2FA for your account, and enable the Suspicious Login app. Then run sudo -u www-data php occ setupchecks — it grades your homework. Keep going until it stops complaining. Part 5: The phone part (the actual point of all this) Install the Nextcloud Android app, type your domain, log in, and enable Auto upload for the camera folder. Then install the Memories app on the server (occ app:install memories, or from the web UI) — that's what gives you the Google-Photos-style timeline, map view, and "on this day" nostalgia hits. I also added Calendar, Contacts, Notes and Tasks; with DAVx⁵ from F-Droid, my phone's native calendar and contacts now sync to my closet instead of Mountain View. What it looks like All of these are from the actual box. I made a throwaway account with stock photos for the screenshots — my real library stays private, which is rather the point of the whole exercise. !Dashboard — dark theme, recommended files !Files — folders and uploads !Photos app — timeline grid !Memories — the Google Photos-style timeline Was it worth it? Honest answer: as a file-syncing appliance, Google is still smoother. Their search is magic, their apps are polished, and nobody calls you when their server breaks at 2 AM. But my photos live on a drive I can unplug and hold in my hand. Nobody's training anything on them. Nobody's holding them for ransom at ₹1,300 a year. And when I pull a photo from three years ago on my phone, it's served out of my closet by a board that draws less power than the LED on my TV. Some things you do because they're practical. Some you do because they're yours. --- Full configs from this build are in the post — everything ran on Ubuntu (ARM64) with Nextcloud 35, nginx 1.28, PostgreSQL 18 and Redis 8. If you build this and something explodes, the comments are open. Appendix: the full setup script Everything in this post, automated. Also available as a downloadable file — read it first, then chmod +x setup-nextcloud.sh && ./setup-nextcloud.sh. #!/usr/bin/env bash # # setup-nextcloud.sh — self-hosted Nextcloud on bare metal, no Docker, no snap. # # i run this on the little ARM board in my closet, but it works on any # ubuntu/debian-ish x86 box too. it asks you a few questions, then sets up: # # nginx or apache + php-fpm + postgres or mariadb + redis # nextcloud itself, cron, caching, and (optionally) a cloudflare tunnel # so you get real https without opening ports on your router. # # read it before you run it. that's just good hygiene. # # chmod +x setup-nextcloud.sh && ./setup-nextcloud.sh # set -euo pipefail # -- pretty output ------------------------------------------------------------ c_cyan=$'\e[36m'; c_green=$'\e[32m'; c_yellow=$'\e[33m'; c_red=$'\e[31m'; c_off=$'\e[0m' say() { echo "${c_cyan}==>${c_off} $*"; } ok() { echo "${c_green} ok${c_off} $*"; } warn() { echo "${c_yellow} !!${c_off} $*"; } die() { echo "${c_red} xx${c_off} $*" >&2; exit 1; } # -- sanity checks -------------------------------------------------------------- [[ $EUID -eq 0 ]] && die "don't run me as root directly. run as your normal user; i'll sudo when needed." command -v apt-get >/dev/null || die "this script speaks apt. debian/ubuntu only, sorry." sudo -v || die "need sudo." # fresh-booted ubuntu boxes love holding the dpkg lock hostage for a bit say "waiting for any background apt runs to finish" while sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do sleep 5; done # -- questions ------------------------------------------------------------------ echo say "a few questions before we start" read -rp " public domain for the cloud (e.g. cloud.example.com, blank = lan only): " NC_DOMAIN NC_DOMAIN=${NC_DOMAIN:-$(hostname -I | awk '{print $1}')} read -rp " web server [nginx/apache] (default nginx): " WEB WEB=${WEB:-nginx} read -rp " database [postgres/mariadb] (default postgres): " DB DB=${DB:-postgres} read -rp " data directory (default /var/nextcloud-data): " DATA_DIR DATA_DIR=${DATA_DIR:-/var/nextcloud-data} read -rp " admin username (default admin): " ADMIN_USER ADMIN_USER=${ADMIN_USER:-admin} read -rsp " admin password: " ADMIN_PASS; echo [[ -z "$ADMIN_PASS" ]] && die "empty password? nice try." DB_PASS=$(openssl rand -base64 24 | tr -d '/+=' | head -c 24) # warn gently if the data dir is about to live on the root sd card ROOT_SRC=$(findmnt -n -o SOURCE /) DATA_SRC=$(findmnt -n -o SOURCE -T "$DATA_DIR" 2>/dev/null || echo "$ROOT_SRC") if [[ "$DATA_SRC" == "$ROOT_SRC" && "$ROOT_SRC" == *mmcblk* ]]; then warn "$DATA_DIR will sit on the SD card ($ROOT_SRC)." warn "sd cards die young under database + thumbnail workloads." read -rp " continue anyway? [y/N]: " YOLO [[ "${YOLO,,}" == "y" ]] || die "mount a usb drive first, point the data dir at it, rerun me." fi # -- packages ------------------------------------------------------------------- say "installing packages" sudo apt-get update -qq PHP_PKGS="php-fpm php-gd php-curl php-zip php-xml php-mbstring php-intl php-bcmath php-gmp php-imagick php-redis php-apcu" [[ "$DB" == postgres ]] && PHP_PKGS="$PHP_PKGS php-pgsql" || PHP_PKGS="$PHP_PKGS php-mysql" sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ redis-server unzip curl openssl ffmpeg \ $PHP_PKGS \ $( [[ "$WEB" == nginx ]] && echo nginx || echo "apache2 libapache2-mod-php" ) \ $( [[ "$DB" == postgres ]] && echo postgresql || echo mariadb-server ) PHP_VER=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') PHP_SOCK="/run/php/php${PHP_VER}-fpm.sock" ok "php $PHP_VER, $WEB, $DB, redis — all in" # -- database ------------------------------------------------------------------- say "setting up the database" if [[ "$DB" == postgres ]]; then sudo -u postgres psql -v ON_ERROR_STOP=1 <<SQL CREATE USER nextcloud WITH PASSWORD '${DB_PASS}'; CREATE DATABASE nextcloud OWNER nextcloud TEMPLATE template0 ENCODING 'UTF8'; SQL else sudo mariadb <<SQL CREATE DATABASE nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER 'nextcloud'@'localhost' IDENTIFIED BY '${DB_PASS}'; GRANT ALL PRIVILEGES ON nextcloud.* TO 'nextcloud'@'localhost'; FLUSH PRIVILEGES; SQL fi ok "database ready" # -- nextcloud ------------------------------------------------------------------ say "fetching nextcloud" wget -q https://download.nextcloud.com/server/releases/latest.zip -O /tmp/nextcloud.zip sudo unzip -q /tmp/nextcloud.zip -d /var/www/ sudo mkdir -p "$DATA_DIR" sudo chown -R www-data:www-data /var/www/nextcloud "$DATA_DIR" sudo chmod 750 "$DATA_DIR" ok "nextcloud unpacked to /var/www/nextcloud" # -- php tuning ----------------------------------------------------------------- say "tuning php" sudo tee /etc/php/${PHP_VER}/fpm/conf.d/99-nextcloud.ini >/dev/null <<'INI' memory_limit = 512M upload_max_filesize = 10G post_max_size = 10G max_execution_time = 360 output_buffering = Off opcache.enable = 1 opcache.interned_strings_buffer = 16 opcache.max_accelerated_files = 10000 opcache.memory_consumption = 256 opcache.save_comments = 1 opcache.revalidate_freq = 60 apc.enable_cli = 1 INI # cli needs the same settings or occ behaves differently from the web app. sudo cp /etc/php/${PHP_VER}/fpm/conf.d/99-nextcloud.ini /etc/php/${PHP_VER}/cli/conf.d/99-nextcloud.ini # -- web server ----------------------------------------------------------------- say "configuring $WEB" if [[ "$WEB" == nginx ]]; then # .mjs missing from mime.types breaks parts of the ui. ask me how i know. grep -q 'mjs' /etc/nginx/mime.types || \ sudo sed -i 's|application/javascript\s\+js;|application/javascript js mjs;|' /etc/nginx/mime.types sudo tee /etc/nginx/sites-available/nextcloud >/dev/null <<NGINX upstream php-handler { server unix:${PHP_SOCK}; } server { listen 80; listen [::]:80; server_name ${NC_DOMAIN}; root /var/www/nextcloud; server_tokens off; client_max_body_size 10G; client_body_timeout 360s; add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "no-referrer" always; fastcgi_hide_header X-Powered-By; index index.php index.html /index.php\$request_uri; location = /robots.txt { allow all; log_not_found off; access_log off; } location ^~ /.well-known { location = /.well-known/carddav { return 301 /remote.php/dav/; } location = /.well-known/caldav { return 301 /remote.php/dav/; } location /.well-known/acme-challenge { try_files \$uri \$uri/ =404; } return 301 /index.php\$request_uri; } location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:\$|/) { return 404; } location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; } location ~ \.php(?:\$|/) { rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php\$request_uri; fastcgi_split_path_info ^(.+?\.php)(/.*)$; set \$path_info \$fastcgi_path_info; try_files \$fastcgi_script_name =404; include fastcgi_params; fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name; fastcgi_param PATH_INFO \$path_info; fastcgi_param HTTPS off; fastcgi_param modHeadersAvailable true; fastcgi_param front_controller_active true; fastcgi_pass php-handler; fastcgi_intercept_errors on; fastcgi_request_buffering off; fastcgi_max_temp_file_size 0; fastcgi_read_timeout 360s; } location ~ \.(?:css|js|mjs|svg|gif|png|jpg|ico|wasm|map)\$ { try_files \$uri /index.php\$request_uri; add_header Cache-Control "public, max-age=15778463"; access_log off; } location /remote { return 301 /remote.php\$request_uri; } location / { try_files \$uri \$uri/ /index.php\$request_uri; } } NGINX sudo ln -sf /etc/nginx/sites-available/nextcloud /etc/nginx/sites-enabled/nextcloud sudo rm -f /etc/nginx/sites-enabled/default sudo nginx -t sudo systemctl reload nginx else sudo a2enmod rewrite headers env dir mime ssl >/dev/null sudo tee /etc/apache2/sites-available/nextcloud.conf >/dev/null <<APACHE <VirtualHost *:80> ServerName ${NC_DOMAIN} DocumentRoot /var/www/nextcloud <Directory /var/www/nextcloud> Require all granted AllowOverride All Options FollowSymLinks MultiViews </Directory> </VirtualHost> APACHE sudo a2ensite nextcloud >/dev/null sudo a2dissite 000-default >/dev/null 2>&1 || true sudo systemctl reload apache2 fi sudo systemctl restart "php${PHP_VER}-fpm" ok "$WEB is serving" # -- install -------------------------------------------------------------------- say "running the nextcloud installer" if [[ "$DB" == postgres ]]; then DBARGS=(--database pgsql --database-host 127.0.0.1) else DBARGS=(--database mysql) fi sudo -u www-data php /var/www/nextcloud/occ maintenance:install \ "${DBARGS[@]}" \ --database-name nextcloud --database-user nextcloud --database-pass "$DB_PASS" \ --admin-user "$ADMIN_USER" --admin-pass "$ADMIN_PASS" \ --data-dir "$DATA_DIR" ok "installed" # -- config --------------------------------------------------------------------- say "wiring up cache, cron, trusted domains" occ() { sudo -u www-data php /var/www/nextcloud/occ "$@"; } occ config:system:set trusted_domains 1 --value="$NC_DOMAIN" occ config:system:set memcache.local --value='\OC\Memcache\APCu' occ config:system:set memcache.locking --value='\OC\Memcache\Redis' occ config:system:set redis host --value=127.0.0.1 occ config:system:set redis port --value=6379 --type=integer occ config:system:set default_phone_region --value=IN occ background:cron echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" | sudo crontab -u www-data - # -- optional: cloudflare tunnel -------------------------------------------------- echo read -rp " set up a cloudflare tunnel for https + remote access? [y/N]: " TUNNEL if [[ "${TUNNEL,,}" == "y" ]]; then say "installing cloudflared" ARCH=$(dpkg --print-architecture) wget -q "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${ARCH}.deb" -O /tmp/cloudflared.deb sudo dpkg -i /tmp/cloudflared.deb warn "this next step prints a url — open it in your browser and authorize the domain." read -rp " press enter when ready" _ cloudflared tunnel login cloudflared tunnel create nextcloud cloudflared tunnel route dns nextcloud "$NC_DOMAIN" TUNNEL_ID=$(cloudflared tunnel list 2>/dev/null | awk '/nextcloud/ {print $1}' | head -1) sudo mkdir -p /etc/cloudflared sudo cp "$HOME/.cloudflared/${TUNNEL_ID}.json" /etc/cloudflared/ sudo chmod 600 /etc/cloudflared/"${TUNNEL_ID}".json sudo tee /etc/cloudflared/config.yml >/dev/null <<YAML tunnel: ${TUNNEL_ID} credentials-file: /etc/cloudflared/${TUNNEL_ID}.json ingress: - hostname: ${NC_DOMAIN} service: http://localhost:80 - service: http_status:404 YAML sudo cloudflared service install sudo systemctl enable --now cloudflared occ config:system:set overwriteprotocol --value=https occ config:system:set overwritehost --value="$NC_DOMAIN" occ config:system:set overwrite.cli.url --value="https://${NC_DOMAIN}" # without this every visitor looks like 127.0.0.1 and brute-force protection goes blind occ config:system:set trusted_proxies 0 --value=127.0.0.1 ok "tunnel live — https://${NC_DOMAIN}" fi # -- optional: hardening ---------------------------------------------------------- echo read -rp " add firewall + fail2ban hardening? [Y/n]: " HARDEN if [[ "${HARDEN,,}" != "n" ]]; then say "hardening" sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ufw fail2ban MY_NET=$(ip -4 route show default | head -1 | awk '{print $3}' | sed 's/\.[0-9]*$/.0\/24/') sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow from "$MY_NET" to any port 22 proto tcp sudo ufw allow from "$MY_NET" to any port 80 proto tcp sudo ufw --force enable # nextcloud logs failed logins as json; stock fail2ban has no filter for that sudo tee /etc/fail2ban/filter.d/nextcloud.conf >/dev/null <<'F2B' [Definition] _groupsre = (?:(?:,?\s*"\w+":(?:"(?:\\.|[^"])+"|\w+))*) failregex = ^\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Login failed: datepattern = ,?\s*"time"\s*:\s*"%%Y-%%m-%%dT%%H:%%M:%%S%%z" F2B sudo tee /etc/fail2ban/jail.local >/dev/null <<JAIL [sshd] enabled = true maxretry = 4 bantime = 1h [nextcloud] enabled = true port = 80,443 filter = nextcloud logpath = ${DATA_DIR}/nextcloud.log maxretry = 5 bantime = 1h JAIL sudo systemctl enable --now fail2ban sudo systemctl restart fail2ban occ app:enable password_policy >/dev/null 2>&1 || true occ config:app:set password_policy minLength --value=12 occ config:app:set password_policy enforceUpperLowerCase --value=1 occ config:app:set password_policy enforceNumericCharacters --value=1 occ config:app:set password_policy enforceSpecialCharacters --value=1 occ config:app:set password_policy enforceNonCommonPassword --value=1 ok "ufw + fail2ban + password policy active" fi # -- done ------------------------------------------------------------------------- echo say "done. here's the summary:" echo " url: $([[ "${TUNNEL,,}" == "y" ]] && echo "https://${NC_DOMAIN}" || echo "http://${NC_DOMAIN}")" echo " admin: ${ADMIN_USER} (password = the one you typed)" echo " data: ${DATA_DIR}" echo " db: ${DB} (user nextcloud, pass ${DB_PASS} — also in /var/www/nextcloud/config/config.php)" echo echo " phone app: install 'Nextcloud' from the play store / f-droid," echo " type the url above, log in, enable auto upload. that's it." echo echo " last thing — run this and fix whatever it flags:" echo " sudo -u www-data php /var/www/nextcloud/occ setupchecks" echo --- Originally published on CLI-NERDS — practical Linux, DevOps, and self-hosting guides with free browser-based developer tools.
[Bug]: Getting a Database error after Manually Updating to Nextcloud 35.0.0 · Issue #64505 · nextcloud/server server#64505 Nextcloud を軽率に 34 から 35.0.2 にアップグレードしたらなんか踏んだっぽい (無害であることを祈っている)