342 lines
9.9 KiB
Markdown
342 lines
9.9 KiB
Markdown
# 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` | `<VPS_IP>` |
|
|
| `git.t-gstone.de` | `<VPS_IP>` |
|
|
|
|
## Local Setup
|
|
|
|
### SSH Access
|
|
|
|
Add this to `~/.ssh/config` on your local machine:
|
|
|
|
```
|
|
Host t-gstone.de
|
|
HostName t-gstone.de
|
|
User gstone
|
|
Port 55
|
|
IdentityFile ~/.ssh/id_ed25519
|
|
UseKeychain yes
|
|
AddKeysToAgent yes
|
|
```
|
|
|
|
Generate a key and copy it to the VPS:
|
|
|
|
```bash
|
|
# Generate key (skip if you already have ~/.ssh/id_ed25519)
|
|
ssh-keygen -t ed25519
|
|
|
|
# Copy it to the VPS (will ask for your password once)
|
|
ssh-copy-id -p 55 gstone@t-gstone.de
|
|
|
|
# Store passphrase in macOS Keychain
|
|
ssh-add --apple-use-keychain ~/.ssh/id_ed25519
|
|
```
|
|
|
|
After this, `ssh t-gstone.de` connects without any password prompts.
|
|
|
|
## Syncing to the VPS
|
|
|
|
If the VPS doesn't have a git remote set up yet, use `rsync` over SSH to push the repo:
|
|
|
|
```bash
|
|
# Sync the repo to the VPS (dry-run first to check what would be sent)
|
|
rsync -avz --dry-run -e 'ssh -p 55' \
|
|
--exclude '.env' --exclude '*/.env' --exclude '.git' \
|
|
./ gstone@t-gstone.de:~/nextcloud-selfhosted/
|
|
|
|
# Run for real (remove --dry-run)
|
|
rsync -avz -e 'ssh -p 55' \
|
|
--exclude '.env' --exclude '*/.env' --exclude '.git' \
|
|
./ gstone@t-gstone.de:~/nextcloud-selfhosted/
|
|
```
|
|
|
|
The `.env` files are excluded because they contain secrets and should be created directly on the VPS from the `.env.example` templates.
|
|
|
|
## Syncing via Git (recommended)
|
|
|
|
Now that Gitea is running, you can clone the repo directly on the VPS instead of using rsync.
|
|
|
|
### First-time setup on the VPS
|
|
|
|
```bash
|
|
# 1. Add your SSH key to Gitea (via https://git.t-gstone.de user settings)
|
|
|
|
# 2. Clone the repo
|
|
cd ~
|
|
git clone ssh://git@git.t-gstone.de:2222/gstone/nextcloud-selfhosted.git
|
|
cd nextcloud-selfhosted
|
|
|
|
# 3. 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
|
|
|
|
# 4. Edit each .env file with real values
|
|
nano .env
|
|
nano nextcloud/.env
|
|
nano gitea/.env
|
|
nano monitoring/.env
|
|
|
|
# 5. Deploy
|
|
sudo ./scripts/deploy.sh
|
|
```
|
|
|
|
### Updating the VPS after pushing changes
|
|
|
|
```bash
|
|
cd ~/nextcloud-selfhosted
|
|
git pull
|
|
sudo docker compose --env-file .env up -d
|
|
```
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
# 1. Clone this repo on the VPS
|
|
git clone <repo-url> && 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.*<HOST>.*$
|
|
```
|
|
|
|
### 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
|