# Nextcloud Self-hosted VPS Docker Compose setup for a self-hosted VPS running Nextcloud, Gitea, and monitoring — managed as a GitOps-style repo. ## Prerequisites - A VPS with SSH access - Domain `t-gstone.de` with DNS control - Git installed locally Check your VPS OS: ```bash cat /etc/os-release ``` ## DNS Setup Create these A records pointing to your VPS IP: | Record | Value | |-------------------------|------------| | `nextcloud.t-gstone.de` | `` | | `git.t-gstone.de` | `` | ## Quick Start ```bash # 1. Clone this repo on the VPS git clone && cd nextcloud-selfhosted # 2. Create .env files from examples cp .env.example .env cp nextcloud/.env.example nextcloud/.env cp gitea/.env.example gitea/.env cp monitoring/.env.example monitoring/.env # 3. Edit each .env file with real values # - Generate strong passwords for Postgres and Nextcloud admin # - Add Grafana Cloud credentials to monitoring/.env # 4. Deploy ./scripts/deploy.sh ``` ## Services | Service | Subdomain | Stack | |------------|-------------------------|-------------------------------------| | Nextcloud | `nextcloud.t-gstone.de` | Nextcloud + PostgreSQL 16 + Redis 7 | | Gitea | `git.t-gstone.de` | Gitea (SQLite) | | Caddy | — | Reverse proxy, auto HTTPS | | Monitoring | — | Grafana Alloy -> Grafana Cloud | ## Data Layout All persistent data lives under `/opt/docker-data/`: ``` /opt/docker-data/ ├── caddy/data/ # TLS certificates ├── caddy/config/ ├── nextcloud/html/ # Nextcloud application ├── nextcloud/data/ # User files ├── nextcloud/db/ # PostgreSQL data ├── gitea/data/ # Repositories + Gitea DB └── gitea/config/ # Gitea configuration ``` ## Managing Services A root `docker-compose.yml` includes all stacks, so you can manage everything with one command: ```bash # Start / restart all services docker compose --env-file .env up -d # View logs for all services docker compose --env-file .env logs -f # Stop everything docker compose --env-file .env down ``` You can still target individual services via their compose file: ```bash docker compose -f nextcloud/docker-compose.yml --env-file .env up -d docker compose -f gitea/docker-compose.yml --env-file .env logs -f ``` ## Adding a New Service 1. Create a new directory: `mkdir myapp/` 2. Create `myapp/docker-compose.yml`: - Join the `proxy` external network - Bind mount data to `${DATA_ROOT}/myapp/` - Add `myapp/.env.example` if the service needs secrets 3. Add `- path: myapp/docker-compose.yml` to root `docker-compose.yml` 4. Add a reverse proxy entry in `caddy/Caddyfile`: ``` myapp.t-gstone.de { reverse_proxy myapp:8080 } ``` 5. Reload Caddy: `docker exec caddy caddy reload --config /etc/caddy/Caddyfile` 6. Add a DNS A record for `myapp.t-gstone.de` -> VPS IP 7. Add data directory creation to `scripts/deploy.sh` 8. Add backup steps to `scripts/backup.sh` if the service has persistent data ## Backup & Restore ### Creating Backups ```bash ./scripts/backup.sh ``` This dumps the Nextcloud Postgres database, archives Nextcloud data/config and Gitea data, and stores them in `/opt/backups/` with date-stamped filenames. Backups older than 7 days are automatically removed. Schedule daily backups: ```bash crontab -e # Add: 0 3 * * * /path/to/scripts/backup.sh >> /var/log/backup.log 2>&1 ``` ### Restoring ```bash ./scripts/restore.sh 2026-03-22 ``` This stops services, restores data from the specified date's backup files, restores the database, and restarts everything. ### Backup Strategy Options The current setup stores backups locally on the same VPS. For production use, consider an off-site strategy: | Option | Pros | Cons | |---------------------------------------------------------------------------------|--------------------------------------|---------------------------| | **Local only** (`/opt/backups/`) | Simplest, no extra cost | Lost if VPS dies | | **rsync to second VPS or home server** | Simple, full control | Need a second machine | | **S3-compatible object storage** (Backblaze B2, Hetzner Object Storage, Wasabi) | Cheap, durable, off-site | Monthly cost (~$0.005/GB) | | **Restic or BorgBackup** to any remote target | Encrypted, deduplicated, incremental | More setup complexity | Recommendation for a personal setup: **Backblaze B2 or Hetzner Object Storage with Restic**. Both offer free egress (B2) or low cost, and Restic handles encryption + deduplication automatically. A cron job running `restic backup` after `backup.sh` completes the pipeline. ## Monitoring ### Setup 1. Sign up at [grafana.com](https://grafana.com) (free tier) 2. Go to **My Account** -> **Grafana Cloud** -> your stack 3. Find your Loki and Prometheus endpoints + credentials 4. Fill in `monitoring/.env` with those values 5. Start the monitoring stack: `docker compose --env-file .env up -d` ### Recommended Alerts Set these up in Grafana Cloud UI (**Alerting** -> **Alert rules**): | Alert | Condition | Severity | |----------------------|-----------------------------------------------------------------------|----------| | Disk usage high | `node_filesystem_avail_bytes` / `node_filesystem_size_bytes` < 0.2 | Critical | | Container restarting | Container restart count > 3 in 10 min | Warning | | High memory usage | `node_memory_MemAvailable_bytes` / `node_memory_MemTotal_bytes` < 0.1 | Warning | | High CPU usage | `node_cpu_seconds_total` idle < 10% sustained 5 min | Warning | | Nextcloud cron stale | No log line from `nextcloud-cron` in 15 min | Warning | ### Recommended Dashboards Import these from [Grafana Dashboards](https://grafana.com/grafana/dashboards/): - **Node Exporter Full** (ID: 1860) — CPU, memory, disk, network - **Docker Container Monitoring** (ID: 893) — per-container resource usage - **Loki Docker Logs** — search and filter container logs ## Security Hardening (Reference) These steps are **not automated** — apply them manually based on your needs. ### Firewall (UFW) ```bash ufw default deny incoming ufw default allow outgoing ufw allow 22/tcp # SSH (change if using non-default port) ufw allow 80/tcp # HTTP (Caddy redirect) ufw allow 443/tcp # HTTPS ufw allow 443/udp # HTTPS (HTTP/3 / QUIC) ufw allow 2222/tcp # Gitea SSH ufw enable ``` ### SSH Hardening Edit `/etc/ssh/sshd_config`: ``` PasswordAuthentication no PermitRootLogin prohibit-password MaxAuthTries 3 ``` Then: `systemctl restart sshd` ### fail2ban ```bash apt install fail2ban ``` Create `/etc/fail2ban/jail.local`: ```ini [sshd] enabled = true maxretry = 5 bantime = 3600 [nextcloud] enabled = true port = 80,443 filter = nextcloud logpath = /opt/docker-data/nextcloud/data/nextcloud.log maxretry = 5 bantime = 3600 ``` Create `/etc/fail2ban/filter.d/nextcloud.conf`: ```ini [Definition] failregex = ^.*Login failed.*Remote IP.*.*$ ``` ### Unattended Security Upgrades ```bash apt install unattended-upgrades dpkg-reconfigure -plow unattended-upgrades ``` ### Docker Daemon - Keep Docker updated: `apt update && apt upgrade docker-ce` - Don't run containers as root unless necessary (Gitea uses rootless image) - The Docker socket is mounted read-only into the Alloy container — be aware this still grants significant access