diff --git a/ansible/roles/nextcloud/tasks/main.yml b/ansible/roles/nextcloud/tasks/main.yml index ea8931c..279a0bb 100644 --- a/ansible/roles/nextcloud/tasks/main.yml +++ b/ansible/roles/nextcloud/tasks/main.yml @@ -1,6 +1,12 @@ --- # Main tasks for Nextcloud deployment +- name: Include volume mounting tasks + include_tasks: mount-volume.yml + tags: + - nextcloud + - volume + - name: Include Docker deployment tasks include_tasks: docker.yml tags: diff --git a/ansible/roles/nextcloud/tasks/mount-volume.yml b/ansible/roles/nextcloud/tasks/mount-volume.yml new file mode 100644 index 0000000..e1c2479 --- /dev/null +++ b/ansible/roles/nextcloud/tasks/mount-volume.yml @@ -0,0 +1,74 @@ +--- +# Mount Hetzner Volume for Nextcloud Data Storage +# +# This task file handles mounting the Hetzner Volume that stores Nextcloud user data. +# The volume is created and attached by OpenTofu, we just mount it here. + +- name: Wait for volume device to appear + wait_for: + path: /dev/disk/by-id/ + timeout: 30 + register: disk_ready + +- name: Find Nextcloud volume device + shell: | + ls -1 /dev/disk/by-id/scsi-0HC_Volume_* 2>/dev/null | grep -i "nextcloud-data-{{ client_name }}" | head -1 + register: volume_device_result + changed_when: false + failed_when: volume_device_result.rc != 0 + +- name: Set volume device fact + set_fact: + volume_device: "{{ volume_device_result.stdout }}" + +- name: Display found volume device + debug: + msg: "Found Nextcloud volume at: {{ volume_device }}" + +- name: Check if volume is already formatted + shell: | + blkid {{ volume_device }} | grep -q 'TYPE="ext4"' + register: volume_formatted + changed_when: false + failed_when: false + +- name: Format volume as ext4 if not formatted + filesystem: + fstype: ext4 + dev: "{{ volume_device }}" + when: volume_formatted.rc != 0 + +- name: Create mount point directory + file: + path: /mnt/nextcloud-data + state: directory + mode: '0755' + +- name: Mount Nextcloud data volume + mount: + path: /mnt/nextcloud-data + src: "{{ volume_device }}" + fstype: ext4 + state: mounted + opts: defaults,discard + register: mount_result + +- name: Ensure mount persists across reboots + mount: + path: /mnt/nextcloud-data + src: "{{ volume_device }}" + fstype: ext4 + state: present + opts: defaults,discard + +- name: Create Nextcloud data directory on volume + file: + path: /mnt/nextcloud-data/data + state: directory + owner: www-data + group: www-data + mode: '0750' + +- name: Display mount success + debug: + msg: "Nextcloud volume successfully mounted at /mnt/nextcloud-data" diff --git a/ansible/roles/nextcloud/templates/docker-compose.nextcloud.yml.j2 b/ansible/roles/nextcloud/templates/docker-compose.nextcloud.yml.j2 index 1c7a216..04199e8 100644 --- a/ansible/roles/nextcloud/templates/docker-compose.nextcloud.yml.j2 +++ b/ansible/roles/nextcloud/templates/docker-compose.nextcloud.yml.j2 @@ -35,7 +35,8 @@ services: - nextcloud-db - nextcloud-redis volumes: - - nextcloud-data:/var/www/html + - nextcloud-app:/var/www/html + - /mnt/nextcloud-data/data:/var/www/html/data # User data on Hetzner Volume entrypoint: /cron.sh networks: - nextcloud-internal @@ -49,7 +50,8 @@ services: - nextcloud-db - nextcloud-redis volumes: - - nextcloud-data:/var/www/html + - nextcloud-app:/var/www/html + - /mnt/nextcloud-data/data:/var/www/html/data # User data on Hetzner Volume environment: # Database configuration POSTGRES_HOST: {{ nextcloud_db_host }} @@ -158,5 +160,6 @@ volumes: name: nextcloud-db-data nextcloud-redis-data: name: nextcloud-redis-data - nextcloud-data: - name: nextcloud-data + nextcloud-app: + name: nextcloud-app + # Note: nextcloud-data volume removed - user data now stored on Hetzner Volume at /mnt/nextcloud-data diff --git a/docs/storage-architecture.md b/docs/storage-architecture.md new file mode 100644 index 0000000..81c08a4 --- /dev/null +++ b/docs/storage-architecture.md @@ -0,0 +1,449 @@ +# Storage Architecture + +Comprehensive guide to storage architecture using Hetzner Volumes for Nextcloud data. + +## Overview + +The infrastructure uses **Hetzner Volumes** (block storage) for Nextcloud user data, separating application and data layers: + +- **Server local disk**: Operating system, Docker images, application code +- **Hetzner Volume**: Nextcloud user files (/var/www/html/data) +- **Docker volumes**: Database and Redis data (ephemeral, can be rebuilt) + +## Architecture Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Hetzner Cloud Server (cpx22) │ +│ │ +│ ┌──────────────────────┐ ┌────────────────────────┐ │ +│ │ Local Disk (80 GB) │ │ Hetzner Volume (100GB) │ │ +│ │ │ │ │ │ +│ │ - OS (Ubuntu 24.04) │ │ Mounted at: │ │ +│ │ - Docker images │ │ /mnt/nextcloud-data │ │ +│ │ - Application code │ │ │ │ +│ │ - Config files │ │ Contains: │ │ +│ │ │ │ - Nextcloud user files │ │ +│ │ Docker volumes: │ │ - Uploaded documents │ │ +│ │ - postgres-db │ │ - Photos, videos │ │ +│ │ - redis-cache │ │ - All user data │ │ +│ │ - nextcloud-app │ │ │ │ +│ └──────────────────────┘ └────────────────────────┘ │ +│ │ │ │ +│ └────────────────────────────────┘ │ +│ Both accessible to │ +│ Docker containers │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Benefits + +### 1. Data Independence +- User data survives server rebuilds +- Can detach volume from one server and attach to another +- Easier disaster recovery + +### 2. Flexible Scaling +- Resize storage without touching server +- Pay only for storage you need +- Start small (100 GB), grow as needed + +### 3. Better Separation +- Application layer (ephemeral, can be rebuilt) +- Data layer (persistent, backed up) +- Clear distinction between code and content + +### 4. Simplified Backups +- Snapshot volumes independently +- Smaller, faster snapshots (only data, not OS) +- Point-in-time recovery of user files + +### 5. Cost Optimization +- Small clients: 50 GB (~€2.70/month) +- Medium clients: 100 GB (~€5.40/month) +- Large clients: 250+ GB (~€13.50+/month) +- Only pay for what you use + +## Volume Specifications + +| Feature | Value | +|---------|-------| +| Minimum size | 10 GB | +| Maximum size | 10 TB (10,000 GB) | +| Pricing | €0.054/GB/month | +| Performance | Fast NVMe SSD | +| IOPS | High performance | +| Filesystem | ext4 (pre-formatted) | +| Snapshots | Supported | +| Backups | Via Hetzner API | + +## How It Works + +### 1. OpenTofu Creates Volume + +When deploying a client: + +```hcl +# tofu/volumes.tf +resource "hcloud_volume" "nextcloud_data" { + for_each = var.clients + + name = "nextcloud-data-${each.key}" + size = each.value.nextcloud_volume_size # e.g., 100 GB + location = each.value.location + format = "ext4" +} + +resource "hcloud_volume_attachment" "nextcloud_data" { + for_each = var.clients + volume_id = hcloud_volume.nextcloud_data[each.key].id + server_id = hcloud_server.client[each.key].id + automount = false +} +``` + +### 2. Ansible Mounts Volume + +During deployment: + +```yaml +# ansible/roles/nextcloud/tasks/mount-volume.yml +- Find volume device at /dev/disk/by-id/scsi-0HC_Volume_* +- Format as ext4 (if not already formatted) +- Mount at /mnt/nextcloud-data +- Create data directory with proper permissions +``` + +### 3. Docker Uses Mount + +Docker Compose configuration: + +```yaml +services: + nextcloud: + volumes: + - nextcloud-app:/var/www/html # Application code (local) + - /mnt/nextcloud-data/data:/var/www/html/data # User data (volume) +``` + +## Directory Structure + +### On Server Local Disk + +``` +/var/lib/docker/volumes/ +├── nextcloud-app/ # Nextcloud application code +├── nextcloud-db-data/ # PostgreSQL database +└── nextcloud-redis-data/ # Redis cache + +/opt/docker/ +├── authentik/ # Authentik configuration +├── nextcloud/ # Nextcloud docker-compose.yml +└── traefik/ # Traefik configuration +``` + +### On Hetzner Volume + +``` +/mnt/nextcloud-data/ +└── data/ # Nextcloud user data directory + ├── admin/ # Admin user files + ├── user1/ # User 1 files + ├── user2/ # User 2 files + └── appdata_*/ # Application data +``` + +## Volume Sizing Guidelines + +### Small Clients (1-10 users) +- **Starting size**: 50 GB +- **Monthly cost**: ~€2.70 +- **Use case**: Personal use, small teams +- **Growth**: +10 GB increments + +### Medium Clients (10-50 users) +- **Starting size**: 100 GB +- **Monthly cost**: ~€5.40 +- **Use case**: Small businesses, departments +- **Growth**: +25 GB increments + +### Large Clients (50-200 users) +- **Starting size**: 250 GB +- **Monthly cost**: ~€13.50 +- **Use case**: Medium businesses +- **Growth**: +50 GB increments + +### Enterprise Clients (200+ users) +- **Starting size**: 500 GB+ +- **Monthly cost**: ~€27+ +- **Use case**: Large organizations +- **Growth**: +100 GB increments + +**Pro tip**: Start conservative and grow as needed. Resizing is online and takes seconds. + +## Volume Operations + +### Resize Volume + +Increase volume size (cannot decrease): + +```bash +./scripts/resize-client-volume.sh +``` + +Example: +```bash +# Resize dev client from 100 GB to 200 GB +./scripts/resize-client-volume.sh dev 200 +``` + +The script will: +1. Resize via Hetzner API +2. Expand filesystem +3. Verify new size +4. Show cost increase + +**Note**: Resizing is **online** (no downtime) and **instant**. + +### Snapshot Volume + +Create a point-in-time snapshot: + +```bash +# Via Hetzner Cloud Console +# Or via API: +hcloud volume create-snapshot nextcloud-data-dev \ + --description "Before major update" +``` + +### Restore from Snapshot + +1. Create new volume from snapshot +2. Attach to server +3. Update mount in Ansible +4. Restart Nextcloud containers + +### Detach and Move Volume + +Move data between servers: + +```bash +# 1. Stop Nextcloud on old server +ansible old-server -i hcloud.yml -m shell -a "docker stop nextcloud" + +# 2. Detach volume via Hetzner Console or API +hcloud volume detach nextcloud-data-client1 + +# 3. Attach to new server +hcloud volume attach nextcloud-data-client1 --server new-server + +# 4. Mount on new server +ansible new-server -i hcloud.yml -m shell -a "mount /dev/disk/by-id/scsi-0HC_Volume_* /mnt/nextcloud-data" + +# 5. Start Nextcloud +ansible new-server -i hcloud.yml -m shell -a "docker start nextcloud" +``` + +## Backup Strategy + +### Option 1: Hetzner Volume Snapshots + +**Pros:** +- Fast (incremental) +- Integrated with Hetzner +- Point-in-time recovery + +**Cons:** +- Stored in same region +- Not off-site + +**Implementation:** +```bash +# Daily snapshots via cron +0 2 * * * hcloud volume create-snapshot nextcloud-data-prod \ + --description "Daily backup $(date +%Y-%m-%d)" +``` + +### Option 2: Rsync to External Storage + +**Pros:** +- Off-site backup +- Full control +- Can use any storage provider + +**Cons:** +- Slower +- More complex + +**Implementation:** +```bash +# Backup to external server +ansible client -i hcloud.yml -m shell -a "\ + rsync -av /mnt/nextcloud-data/data/ \ + backup-server:/backups/client/nextcloud/" +``` + +### Option 3: Nextcloud Built-in Backup + +**Pros:** +- Uses Nextcloud's own backup tools +- Consistent with application state + +**Cons:** +- Slower than volume snapshots + +**Implementation:** +```bash +# Using occ command +docker exec -u www-data nextcloud php occ maintenance:mode --on +rsync -av /mnt/nextcloud-data/ /backup/location/ +docker exec -u www-data nextcloud php occ maintenance:mode --off +``` + +## Performance Considerations + +### Hetzner Volume Performance + +| Metric | Specification | +|--------|---------------| +| Type | NVMe SSD | +| IOPS | High (exact spec varies) | +| Throughput | Fast sequential R/W | +| Latency | Low (local to server) | + +### Optimization Tips + +1. **Use ext4 filesystem** (default, well-tested) +2. **Enable discard** for SSD optimization (default in our setup) +3. **Monitor I/O** with `iostat -x 1` +4. **Check volume usage** regularly + +### Monitoring + +```bash +# Check volume usage +df -h /mnt/nextcloud-data + +# Check I/O stats +iostat -x 1 /dev/disk/by-id/scsi-0HC_Volume_* + +# Check mount status +mount | grep nextcloud-data +``` + +## Troubleshooting + +### Volume Not Mounting + +**Problem:** Volume doesn't mount after server restart + +**Solutions:** +1. Check if volume is attached: + ```bash + lsblk + ls -la /dev/disk/by-id/scsi-0HC_Volume_* + ``` + +2. Check fstab entry: + ```bash + cat /etc/fstab | grep nextcloud-data + ``` + +3. Manually mount: + ```bash + mount /dev/disk/by-id/scsi-0HC_Volume_* /mnt/nextcloud-data + ``` + +4. Re-run Ansible: + ```bash + ansible-playbook -i hcloud.yml playbooks/deploy.yml --limit client --tags volume + ``` + +### Volume Full + +**Problem:** Nextcloud reports "not enough space" + +**Solutions:** +1. Check usage: + ```bash + df -h /mnt/nextcloud-data + ``` + +2. Resize volume: + ```bash + ./scripts/resize-client-volume.sh client 200 + ``` + +3. Clean up old files: + ```bash + docker exec -u www-data nextcloud php occ files:scan --all + docker exec -u www-data nextcloud php occ files:cleanup + ``` + +### Permission Issues + +**Problem:** Nextcloud can't write to volume + +**Solutions:** +1. Check ownership: + ```bash + ls -la /mnt/nextcloud-data/ + ``` + +2. Fix permissions: + ```bash + chown -R www-data:www-data /mnt/nextcloud-data/data + chmod -R 750 /mnt/nextcloud-data/data + ``` + +3. Re-run mount tasks: + ```bash + ansible-playbook -i hcloud.yml playbooks/deploy.yml --limit client --tags volume + ``` + +### Volume Detached Accidentally + +**Problem:** Volume was detached and lost mount + +**Solutions:** +1. Re-attach via Hetzner Console or API +2. Remount: + ```bash + ansible client -i hcloud.yml -m shell -a "\ + mount /dev/disk/by-id/scsi-0HC_Volume_* /mnt/nextcloud-data" + ``` +3. Restart Nextcloud: + ```bash + docker restart nextcloud nextcloud-cron + ``` + +## Cost Analysis + +### Example Scenarios + +**Scenario 1: 10 Clients, 100 GB each** +- Volume cost: 10 × 100 GB × €0.054 = €54/month +- Server cost: 10 × €7/month = €70/month +- **Total**: €124/month + +**Scenario 2: 5 Small + 3 Medium + 2 Large** +- Small (50 GB): 5 × €2.70 = €13.50 +- Medium (100 GB): 3 × €5.40 = €16.20 +- Large (250 GB): 2 × €13.50 = €27.00 +- **Volume total**: €56.70/month +- Plus server costs + +**Cost Savings vs Local Disk:** +- Can use smaller servers (cheaper compute) +- Pay only for storage needed +- Resize incrementally vs over-provisioning + +## Migration from Local Volumes + +See [volume-migration.md](volume-migration.md) for detailed migration procedures. + +## Related Documentation + +- [Volume Migration Guide](volume-migration.md) - Migrating existing clients +- [Deployment Guide](deployment.md) - Full deployment with volumes +- [Maintenance Tracking](maintenance-tracking.md) - Monitoring and updates diff --git a/docs/volume-migration.md b/docs/volume-migration.md new file mode 100644 index 0000000..8e0a416 --- /dev/null +++ b/docs/volume-migration.md @@ -0,0 +1,398 @@ +# Volume Migration Guide + +Step-by-step guide for migrating existing Nextcloud clients from local Docker volumes to Hetzner Volumes. + +## Overview + +This guide covers migrating an existing client (like `dev`) that currently stores Nextcloud data in a Docker volume to the new Hetzner Volume architecture. + +**Migration is SAFE and REVERSIBLE** - we keep the old data until verification is complete. + +## Prerequisites + +- Client currently deployed and running +- SSH access to the server +- Hetzner API token (`HCLOUD_TOKEN`) +- SOPS age key for secrets (`SOPS_AGE_KEY_FILE`) +- At least 30 minutes of maintenance window + +## Migration Steps + +### Phase 1: Preparation + +#### 1. Verify Current State + +```bash +# Check client is running +./scripts/client-status.sh dev + +# Check current data location +cd ansible +ansible dev -i hcloud.yml -m shell -a "docker inspect nextcloud | jq '.[0].Mounts'" +``` + +Expected output shows Docker volume: +```json +{ + "Type": "volume", + "Name": "nextcloud-data", + "Source": "/var/lib/docker/volumes/nextcloud-data/_data", + "Destination": "/var/www/html" +} +``` + +#### 2. Check Data Size + +```bash +# Check how much data we're migrating +ansible dev -i hcloud.yml -m shell -a "\ + du -sh /var/lib/docker/volumes/nextcloud-data/_data/data" +``` + +Note the size - you'll need a volume at least this big (we recommend 2x for growth). + +#### 3. Notify Users + +⚠️ **Important**: Inform users that Nextcloud will be unavailable during migration (typically 10-30 minutes depending on data size). + +### Phase 2: Create and Attach Volume + +#### 4. Update OpenTofu Configuration + +Already done if you're following the issue #18 implementation: + +```hcl +# tofu/terraform.tfvars +clients = { + dev = { + # ... existing config ... + nextcloud_volume_size = 100 # Adjust based on current data size + } +} +``` + +#### 5. Apply OpenTofu Changes + +```bash +cd tofu + +# Review changes +tofu plan + +# Apply - this creates the volume and attaches it +tofu apply +``` + +Expected output: +``` ++ hcloud_volume.nextcloud_data["dev"] ++ hcloud_volume_attachment.nextcloud_data["dev"] +``` + +The volume is now attached to the server but not yet mounted. + +### Phase 3: Stop Services and Mount Volume + +#### 6. Enable Maintenance Mode + +```bash +cd ansible + +# Enable Nextcloud maintenance mode +ansible dev -i hcloud.yml -m shell -a "\ + docker exec -u www-data nextcloud php occ maintenance:mode --on" +``` + +#### 7. Stop Nextcloud Containers + +```bash +# Stop Nextcloud and cron (keep database and redis running) +ansible dev -i hcloud.yml -m shell -a "\ + docker stop nextcloud nextcloud-cron" +``` + +#### 8. Mount the Volume + +```bash +# Run Ansible volume mounting tasks +ansible-playbook -i hcloud.yml playbooks/deploy.yml \ + --limit dev \ + --tags volume +``` + +This will: +- Find the volume device +- Format as ext4 (if needed) +- Mount at `/mnt/nextcloud-data` +- Create data directory with correct permissions +- Add to `/etc/fstab` for persistence + +#### 9. Verify Mount + +```bash +# Check mount is successful +ansible dev -i hcloud.yml -m shell -a "df -h /mnt/nextcloud-data" +ansible dev -i hcloud.yml -m shell -a "ls -la /mnt/nextcloud-data" +``` + +### Phase 4: Migrate Data + +#### 10. Copy Data to Volume + +```bash +# Copy all data from Docker volume to Hetzner Volume +ansible dev -i hcloud.yml -m shell -a "\ + rsync -avh --progress \ + /var/lib/docker/volumes/nextcloud-data/_data/data/ \ + /mnt/nextcloud-data/data/" -b +``` + +This will take some time depending on data size. Progress is shown. + +**Estimated times:** +- 1 GB: ~30 seconds +- 10 GB: ~5 minutes +- 50 GB: ~20 minutes +- 100 GB: ~40 minutes + +#### 11. Verify Data Copy + +```bash +# Check data was copied +ansible dev -i hcloud.yml -m shell -a "\ + du -sh /mnt/nextcloud-data/data" + +# Verify file count matches +ansible dev -i hcloud.yml -m shell -a "\ + find /var/lib/docker/volumes/nextcloud-data/_data/data -type f | wc -l && \ + find /mnt/nextcloud-data/data -type f | wc -l" +``` + +Both counts should match. + +#### 12. Fix Permissions + +```bash +# Ensure correct ownership +ansible dev -i hcloud.yml -m shell -a "\ + chown -R www-data:www-data /mnt/nextcloud-data/data" -b +``` + +### Phase 5: Update Configuration and Restart + +#### 13. Update Docker Compose + +Already done if you're following the issue #18 implementation. The new template uses: + +```yaml +volumes: + - /mnt/nextcloud-data/data:/var/www/html/data +``` + +#### 14. Deploy Updated Configuration + +```bash +# Deploy updated docker-compose.yml +ansible-playbook -i hcloud.yml playbooks/deploy.yml \ + --limit dev \ + --tags nextcloud,docker +``` + +This will: +- Update docker-compose.yml +- Restart Nextcloud with new volume mounts + +#### 15. Disable Maintenance Mode + +```bash +# Turn off maintenance mode +ansible dev -i hcloud.yml -m shell -a "\ + docker exec -u www-data nextcloud php occ maintenance:mode --off" +``` + +### Phase 6: Verification + +#### 16. Test Nextcloud Access + +```bash +# Check containers are running +ansible dev -i hcloud.yml -m shell -a "docker ps | grep nextcloud" + +# Test HTTPS endpoint +curl -I https://nextcloud.dev.vrije.cloud +``` + +Expected: HTTP 200 OK + +#### 17. Login and Verify Files + +1. Open https://nextcloud.dev.vrije.cloud in browser +2. Login with admin credentials +3. Navigate to Files +4. Check that all files are visible +5. Try uploading a new file +6. Try downloading an existing file + +#### 18. Run Files Scan + +```bash +# Scan all files to update Nextcloud's database +ansible dev -i hcloud.yml -m shell -a "\ + docker exec -u www-data nextcloud php occ files:scan --all" +``` + +#### 19. Check for Errors + +```bash +# Check Nextcloud logs +ansible dev -i hcloud.yml -m shell -a "\ + docker logs nextcloud --tail 50" + +# Check for any errors in admin panel +# Login → Settings → Administration → Logging +``` + +### Phase 7: Cleanup (Optional) + +⚠️ **Wait at least 24-48 hours before cleanup to ensure everything works!** + +#### 20. Remove Old Docker Volume + +After confirming everything works: + +```bash +# Remove old Docker volume (THIS IS IRREVERSIBLE!) +ansible dev -i hcloud.yml -m shell -a "\ + docker volume rm nextcloud-data" +``` + +You'll get an error if any container is still using it (good safety check). + +## Rollback Procedure + +If something goes wrong, you can rollback: + +### Quick Rollback (During Migration) + +If you haven't removed the old Docker volume: + +```bash +# 1. Stop containers +ansible dev -i hcloud.yml -m shell -a "docker stop nextcloud nextcloud-cron" + +# 2. Revert docker-compose.yml to use old volume +# (restore from git or manually edit) + +# 3. Restart containers +ansible dev -i hcloud.yml -m shell -a "cd /opt/docker/nextcloud && docker-compose up -d" + +# 4. Disable maintenance mode +ansible dev -i hcloud.yml -m shell -a "\ + docker exec -u www-data nextcloud php occ maintenance:mode --off" +``` + +### Full Rollback (After Cleanup) + +If you've removed the old volume but have a backup: + +```bash +# 1. Restore from backup to new volume +# 2. Continue with Phase 5 (restart with new config) +``` + +## Verification Checklist + +After migration, verify: + +- [ ] Nextcloud web interface loads +- [ ] Can login with existing credentials +- [ ] All files and folders visible +- [ ] Can upload new files +- [ ] Can download existing files +- [ ] Can edit files (if Collabora Online installed) +- [ ] Sharing links still work +- [ ] Mobile apps can sync +- [ ] Desktop clients can sync +- [ ] No errors in Nextcloud logs +- [ ] No errors in admin panel +- [ ] Volume is mounted in `/etc/fstab` +- [ ] Volume mounts after server reboot + +## Common Issues + +### Issue: "Permission denied" errors + +**Cause:** Wrong ownership on volume + +**Fix:** +```bash +ansible dev -i hcloud.yml -m shell -a "\ + chown -R www-data:www-data /mnt/nextcloud-data/data" -b +``` + +### Issue: "Volume not found" in Docker + +**Cause:** Docker compose still referencing old volume name + +**Fix:** +```bash +# Check docker-compose.yml has correct mount +ansible dev -i hcloud.yml -m shell -a "cat /opt/docker/nextcloud/docker-compose.yml | grep mnt" + +# Should show: /mnt/nextcloud-data/data:/var/www/html/data +``` + +### Issue: Files missing after migration + +**Cause:** Incomplete rsync + +**Fix:** +```bash +# Re-run rsync (it will only copy missing files) +ansible dev -i hcloud.yml -m shell -a "\ + rsync -avh \ + /var/lib/docker/volumes/nextcloud-data/_data/data/ \ + /mnt/nextcloud-data/data/" -b +``` + +### Issue: Volume unmounted after reboot + +**Cause:** Not in `/etc/fstab` + +**Fix:** +```bash +# Re-run volume mounting tasks +ansible-playbook -i hcloud.yml playbooks/deploy.yml --limit dev --tags volume +``` + +## Post-Migration Benefits + +After successful migration: + +- ✅ Can resize storage independently: `./scripts/resize-client-volume.sh dev 200` +- ✅ Can snapshot data separately from system +- ✅ Can move data to new server if needed +- ✅ Better separation of application and data +- ✅ Clearer backup strategy + +## Timeline Example + +Real-world timeline for 10 GB Nextcloud instance: + +| Step | Duration | Notes | +|------|----------|-------| +| Preparation | 5 min | Check status, plan | +| Create volume (OpenTofu) | 2 min | Automated | +| Stop services | 1 min | Quick | +| Mount volume | 2 min | Ansible tasks | +| Copy data (10 GB) | 5 min | Depends on size | +| Update config | 2 min | Ansible deploy | +| Restart services | 2 min | Docker restart | +| Verification | 10 min | Manual testing | +| **Total** | **~30 min** | Includes safety checks | + +## Related Documentation + +- [Storage Architecture](storage-architecture.md) - Understanding volumes +- [Deployment Guide](deployment.md) - New deployments with volumes +- [Client Registry](client-registry.md) - Track migration status diff --git a/scripts/resize-client-volume.sh b/scripts/resize-client-volume.sh new file mode 100755 index 0000000..cf0cfd1 --- /dev/null +++ b/scripts/resize-client-volume.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash +# +# Resize a client's Nextcloud data volume +# +# Usage: ./scripts/resize-client-volume.sh +# +# This script will: +# 1. Resize the Hetzner Volume via API +# 2. Expand the filesystem on the server +# 3. Verify the new size +# +# Note: Volumes can only be increased in size, never decreased + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +# Check arguments +if [ $# -ne 2 ]; then + echo -e "${RED}Error: Client name and new size required${NC}" + echo "Usage: $0 " + echo "" + echo "Example: $0 dev 200" + echo "" + echo "Note: You can only INCREASE volume size, never decrease" + exit 1 +fi + +CLIENT_NAME="$1" +NEW_SIZE="$2" + +# Validate new size is a number +if ! [[ "$NEW_SIZE" =~ ^[0-9]+$ ]]; then + echo -e "${RED}Error: Size must be a number${NC}" + exit 1 +fi + +# Check minimum size +if [ "$NEW_SIZE" -lt 10 ]; then + echo -e "${RED}Error: Minimum volume size is 10 GB${NC}" + exit 1 +fi + +# Check maximum size +if [ "$NEW_SIZE" -gt 10000 ]; then + echo -e "${RED}Error: Maximum volume size is 10,000 GB (10 TB)${NC}" + exit 1 +fi + +# Check required environment variables +if [ -z "${HCLOUD_TOKEN:-}" ]; then + echo -e "${RED}Error: HCLOUD_TOKEN environment variable not set${NC}" + echo "Export your Hetzner Cloud API token:" + echo " export HCLOUD_TOKEN='your-token-here'" + exit 1 +fi + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}Resizing Nextcloud Volume${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" +echo "Client: $CLIENT_NAME" +echo "New size: ${NEW_SIZE} GB" +echo "" + +# Step 1: Get volume ID from Hetzner API +echo -e "${YELLOW}[1/4] Looking up volume...${NC}" + +VOLUME_NAME="nextcloud-data-${CLIENT_NAME}" + +# Get volume info +VOLUME_INFO=$(curl -s -H "Authorization: Bearer $HCLOUD_TOKEN" \ + "https://api.hetzner.cloud/v1/volumes?name=$VOLUME_NAME") + +VOLUME_ID=$(echo "$VOLUME_INFO" | jq -r '.volumes[0].id // empty') +CURRENT_SIZE=$(echo "$VOLUME_INFO" | jq -r '.volumes[0].size // empty') + +if [ -z "$VOLUME_ID" ] || [ "$VOLUME_ID" = "null" ]; then + echo -e "${RED}Error: Volume '$VOLUME_NAME' not found${NC}" + echo "Make sure the client exists and has been deployed with volume support" + exit 1 +fi + +echo "Volume ID: $VOLUME_ID" +echo "Current size: ${CURRENT_SIZE} GB" +echo "" + +# Check if new size is larger +if [ "$NEW_SIZE" -le "$CURRENT_SIZE" ]; then + echo -e "${RED}Error: New size ($NEW_SIZE GB) must be larger than current size ($CURRENT_SIZE GB)${NC}" + echo "Volumes can only be increased in size, never decreased" + exit 1 +fi + +# Calculate cost increase +COST_INCREASE=$(echo "scale=2; ($NEW_SIZE - $CURRENT_SIZE) * 0.054" | bc) + +echo -e "${YELLOW}Warning: This will increase monthly costs by approximately €${COST_INCREASE}${NC}" +echo "" +read -p "Continue with resize? (yes/no): " confirm + +if [ "$confirm" != "yes" ]; then + echo "Resize cancelled" + exit 0 +fi + +echo "" + +# Step 2: Resize volume via API +echo -e "${YELLOW}[2/4] Resizing volume via Hetzner API...${NC}" + +RESIZE_RESULT=$(curl -s -X POST \ + -H "Authorization: Bearer $HCLOUD_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"size\": $NEW_SIZE}" \ + "https://api.hetzner.cloud/v1/volumes/$VOLUME_ID/actions/resize") + +ACTION_ID=$(echo "$RESIZE_RESULT" | jq -r '.action.id // empty') + +if [ -z "$ACTION_ID" ] || [ "$ACTION_ID" = "null" ]; then + echo -e "${RED}Error: Failed to resize volume${NC}" + echo "$RESIZE_RESULT" | jq . + exit 1 +fi + +# Wait for resize action to complete +echo "Waiting for resize action to complete..." +while true; do + ACTION_STATUS=$(curl -s -H "Authorization: Bearer $HCLOUD_TOKEN" \ + "https://api.hetzner.cloud/v1/volumes/actions/$ACTION_ID" | jq -r '.action.status') + + if [ "$ACTION_STATUS" = "success" ]; then + break + elif [ "$ACTION_STATUS" = "error" ]; then + echo -e "${RED}Error: Resize action failed${NC}" + exit 1 + fi + + sleep 2 +done + +echo -e "${GREEN}✓ Volume resized${NC}" +echo "" + +# Step 3: Expand filesystem on the server +echo -e "${YELLOW}[3/4] Expanding filesystem on server...${NC}" + +cd "$PROJECT_ROOT/ansible" + +# Find the device +DEVICE_CMD="ls -1 /dev/disk/by-id/scsi-0HC_Volume_* 2>/dev/null | grep -i 'nextcloud-data-${CLIENT_NAME}' | head -1" +DEVICE=$(~/.local/bin/ansible -i hcloud.yml "$CLIENT_NAME" -m shell -a "$DEVICE_CMD" -o 2>/dev/null | tail -1 | awk '{print $NF}') + +if [ -z "$DEVICE" ]; then + echo -e "${RED}Error: Could not find volume device on server${NC}" + exit 1 +fi + +echo "Device: $DEVICE" + +# Resize filesystem +~/.local/bin/ansible -i hcloud.yml "$CLIENT_NAME" -m shell -a "resize2fs $DEVICE" -b + +echo -e "${GREEN}✓ Filesystem expanded${NC}" +echo "" + +# Step 4: Verify new size +echo -e "${YELLOW}[4/4] Verifying new size...${NC}" + +DF_OUTPUT=$(~/.local/bin/ansible -i hcloud.yml "$CLIENT_NAME" -m shell -a "df -h /mnt/nextcloud-data" -o 2>/dev/null | tail -1) + +echo "$DF_OUTPUT" +echo "" + +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN}✓ Resize complete!${NC}" +echo -e "${GREEN}========================================${NC}" +echo "" +echo "Volume resized from ${CURRENT_SIZE} GB to ${NEW_SIZE} GB" +echo "Additional monthly cost: €${COST_INCREASE}" +echo "" +echo "The new storage is immediately available to Nextcloud." +echo "" diff --git a/tofu/variables.tf b/tofu/variables.tf index 2906a44..6c4d85c 100644 --- a/tofu/variables.tf +++ b/tofu/variables.tf @@ -26,10 +26,11 @@ variable "base_domain" { variable "clients" { description = "Map of client configurations" type = map(object({ - server_type = string # e.g., "cx22" (2 vCPU, 4 GB RAM) - location = string # e.g., "fsn1" (Falkenstein), "nbg1" (Nuremberg), "hel1" (Helsinki) - subdomain = string # e.g., "alpha" for alpha.platform.nl - apps = list(string) # e.g., ["zitadel", "nextcloud"] + server_type = string # e.g., "cx22" (2 vCPU, 4 GB RAM) + location = string # e.g., "fsn1" (Falkenstein), "nbg1" (Nuremberg), "hel1" (Helsinki) + subdomain = string # e.g., "alpha" for alpha.platform.nl + apps = list(string) # e.g., ["zitadel", "nextcloud"] + nextcloud_volume_size = number # Size in GB for Nextcloud data volume (min 10, max 10000) })) default = {} } diff --git a/tofu/volumes.tf b/tofu/volumes.tf new file mode 100644 index 0000000..a742f68 --- /dev/null +++ b/tofu/volumes.tf @@ -0,0 +1,31 @@ +# Hetzner Volumes for Nextcloud Data Storage +# +# Each client gets a dedicated volume for Nextcloud user data. +# Volumes are independent from server instances, enabling: +# - Independent storage scaling +# - Easy data migration between servers +# - Simpler backup/restore procedures +# - Better separation of application and data + +resource "hcloud_volume" "nextcloud_data" { + for_each = var.clients + + name = "nextcloud-data-${each.key}" + size = each.value.nextcloud_volume_size + location = each.value.location + format = "ext4" + + labels = { + client = each.key + purpose = "nextcloud-data" + managed = "terraform" + } +} + +resource "hcloud_volume_attachment" "nextcloud_data" { + for_each = var.clients + + volume_id = hcloud_volume.nextcloud_data[each.key].id + server_id = hcloud_server.client[each.key].id + automount = false # We mount manually via Ansible for better control +}