torrust-tracker-deployer 0.1.0

Torrust Tracker Deployer - Deployment Infrastructure with Ansible and OpenTofu
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
# Backup Management

## Overview

The Torrust Tracker Deployer includes an automated backup system that protects your tracker database and configuration files. Backups are created and managed automatically with configurable retention policies.

**What gets backed up:**

- Database (SQLite or MySQL)
- Tracker configuration file (tracker.toml)
- Prometheus configuration
- Grafana provisioning files (dashboards, datasources)

**Key features:**

- **Automatic**: Initial backup created during deployment, additional backups on configured schedule
- **Scheduled**: Configurable cron schedule (e.g., daily at 3 AM UTC)
- **Retention policy**: Automatically removes old backups after configured retention period
- **Database-aware**: Handles both SQLite and MySQL databases appropriately
- **Minimal downtime**: Backup process briefly stops tracker service (10-15 seconds)
- **Compressed**: All backups are compressed to save storage space

---

## Configuration

### Enabling Backups

Backups are configured in your environment file (e.g., `envs/my-deployment.json`):

```json
{
  "backup": {
    "schedule": "0 3 * * *",
    "retention_days": 7
  }
}
```

### Configuration Options

#### `schedule` (required)

Cron schedule for automatic backups. Specified in standard cron format: `minute hour day month day_of_week`

**Examples:**

- `0 3 * * *` - Every day at 3:00 AM UTC
- `0 2 * * 1` - Every Monday at 2:00 AM UTC
- `0 */6 * * *` - Every 6 hours
- `0 0 1 * *` - First day of every month

**Constraints:**

- Must be a valid cron expression
- Recommended: Off-peak hours to minimize tracker downtime
- Minimum frequency: Once per week (for meaningful backups)

#### `retention_days` (required)

How many days to keep backups before automatic deletion.

**Examples:**

- `7` - Keep one week of backups
- `30` - Keep one month of backups
- `90` - Keep three months of backups

**Constraints:**

- Must be between 1 and 365 days
- Recommended: 7-30 days for normal deployments
- Higher retention requires more storage space

### Configuration Examples

#### Basic Backup (Daily)

```json
{
  "backup": {
    "schedule": "0 3 * * *",
    "retention_days": 7
  }
}
```

Daily backups at 3 AM UTC, keep one week of backups.

#### Conservative Backup (Weekly)

```json
{
  "backup": {
    "schedule": "0 3 * * 0",
    "retention_days": 30
  }
}
```

Weekly backups on Sundays at 3 AM UTC, keep one month of backups.

#### Frequent Backup (Every 6 Hours)

```json
{
  "backup": {
    "schedule": "0 */6 * * *",
    "retention_days": 3
  }
}
```

Backups every 6 hours, keep 3 days (18 backup files).

#### Disable Automatic Backup

Currently, automatic backups cannot be completely disabled. If you don't want automatic backups, you can use a cron schedule that never matches:

```json
{
  "backup": {
    "schedule": "0 0 31 2 *",
    "retention_days": 7
  }
}
```

This schedule would never run (February 31st doesn't exist). Manual backups can still be triggered.

---

## How It Works

### Deployment Phases

#### Phase 1: Backup Setup (during `release` and `run` commands)

During the `release` command:

1. Backup storage directories are created
2. Backup configuration files are deployed
3. Crontab entry is installed for scheduled backups

During the `run` command:

1. Docker Compose is started with the backup service defined
2. **Initial backup is NOT automatically triggered** (planned for Phase 4.2.2)
3. Scheduled backups are ready to run on the configured schedule

**To create an initial backup manually**:

```bash
ssh -i ~/.ssh/your-key user@$VM_IP
cd /opt/torrust
docker compose --profile backup run --rm backup
```

#### Phase 2: Scheduled Backups (via crontab)

After the `release` command, a system cron entry is installed at `/etc/cron.d/tracker-backup`. On your configured schedule:

1. At scheduled time, the maintenance script `/usr/local/bin/maintenance-backup.sh` executes
2. Script stops the tracker service (briefly)
3. Runs backup container: `docker compose run --rm backup`
4. Backup creates new backup files with current timestamp
5. Runs retention cleanup (deletes backups older than retention_days)
6. Tracker service restarts
7. Output logged to `/var/log/tracker-backup.log`

#### Phase 3: Retention Cleanup

After each backup, cleanup runs automatically:

1. Lists all backup files in backup directory
2. Calculates age of each backup (current_time - backup_timestamp)
3. Deletes any backups older than `retention_days`
4. Logs cleanup actions

---

## Backup File Storage

### Directory Structure

Backup files are stored in the backup container's `/backups/` directory, which is mounted to `/opt/torrust/storage/backup/` on the host:

**On the VM (host path)**:

```text
/opt/torrust/storage/backup/
├── etc/
│   ├── backup.conf                   # Backup service configuration
│   └── backup-paths.txt              # Paths to backup
├── sqlite/                           # SQLite database backups
│   ├── sqlite_20260203_030000.db.gz
│   ├── sqlite_20260204_030000.db.gz
│   └── sqlite_20260205_030000.db.gz
├── mysql/                            # MySQL database backups
│   ├── mysql_20260203_030000.sql.gz
│   ├── mysql_20260204_030000.sql.gz
│   └── mysql_20260205_030000.sql.gz
└── config/                           # Configuration backups
    ├── config_20260203_030000.tar.gz
    ├── config_20260204_030000.tar.gz
    └── config_20260205_030000.tar.gz
```

**Inside the backup container (container path)**:

```text
/backups/                            # Mounted to /opt/torrust/storage/backup/
├── sqlite/                          # SQLite database backups
├── mysql/                           # MySQL database backups
└── config/                          # Configuration backups
```

**Docker Compose volume mapping:**

```yaml
volumes:
  - ./storage/backup:/backups # Host path: /opt/torrust/storage/backup/
```

All backup files are accessible via the host path `/opt/torrust/storage/backup/` when you SSH into the VM.

### Filename Format

**SQLite database backups:**

```text
sqlite_YYYYMMDD_HHMMSS.db.gz
```

**MySQL database backups:**

```text
mysql_YYYYMMDD_HHMMSS.sql.gz
```

**Configuration backups:**

```text
config_YYYYMMDD_HHMMSS.tar.gz
```

The timestamp suffix (`YYYYMMDD_HHMMSS`) makes backups sortable and uniquely identifiable.

### File Sizes

Typical sizes for test deployments:

- **SQLite backup**: 4-10 KB (compressed)
- **MySQL backup**: 4-50 KB (compressed)
- **Config backup**: 2-5 KB (compressed)

Production deployments with active trackers may have larger backups. Monitor disk usage:

```bash
# Check backup directory size
du -sh /opt/torrust/storage/backup/
```

---

## Monitoring & Verification

### Verify Initial Backup

After deployment, verify the initial backup was created:

```bash
# SSH to your VM
ssh -i fixtures/testing_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
  torrust@<instance-ip>

# Check backup files
ls -lh /opt/torrust/storage/backup/sqlite/
ls -lh /opt/torrust/storage/backup/mysql/
ls -lh /opt/torrust/storage/backup/config/
```

You should see files like:

```text
-rw-r--r-- 1 torrust torrust 4.0K Feb  3 03:00 sqlite_20260203_030000.db.gz
-rw-r--r-- 1 torrust torrust 3.2K Feb  3 03:00 config_20260203_030000.tar.gz
```

### Check Crontab Configuration

Verify the backup system cron entry was installed during the `release` command:

```bash
# Check if system cron entry exists
ssh -i fixtures/testing_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
  torrust@<instance-ip> "cat /etc/cron.d/tracker-backup"
```

**Expected output** (for schedule `0 3 * * *`):

```text
# Backup Maintenance Schedule
0 3 * * * root cd /opt/torrust && /usr/local/bin/maintenance-backup.sh >> /var/log/tracker-backup.log 2>&1
```

The cron entry uses a maintenance script that:

1. Stops the tracker service
2. Runs the backup container
3. Restarts the tracker service
4. Logs all output to `/var/log/tracker-backup.log`

**If cron entry not found**:

- The `release` command did not properly install the cron entry
- Re-run the `release` command

### Monitor Automatic Backups

After the scheduled backup time passes, verify automatic backups are running:

```bash
ssh -i fixtures/testing_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
  torrust@<instance-ip> "tail -20 /var/log/tracker-backup.log"
```

**Expected output**:

```text
[2026-02-04 16:35:01] INFO: Tracker stopped successfully
[2026-02-04 16:35:01] INFO: Running backup container (via backup profile)...
[2026-02-04 16:35:06] INFO: Backup completed successfully
[2026-02-04 16:35:06] INFO: Starting tracker container...
[2026-02-04 16:35:21] INFO: Tracker started successfully
[2026-02-04 16:35:21] Backup maintenance completed (exit code: 0)
```

You can also verify backup files were created:

```bash
ssh -i fixtures/testing_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
  torrust@<instance-ip> "ls -lh /opt/torrust/storage/backup/sqlite/"
```

**Expected**: Multiple backup files with different timestamps (one per backup execution):

```text
-rw-r--r-- 1 root root 4.0K Feb  3 03:00 sqlite_20260203_030000.db.gz
-rw-r--r-- 1 root root 4.0K Feb  4 03:00 sqlite_20260204_030000.db.gz
-rw-r--r-- 1 root root 4.0K Feb  5 03:00 sqlite_20260205_030000.db.gz
```

Multiple files with different dates indicate automatic backups are executing on schedule.

**Note**: Backup logging to `/var/log/torrust-backup.log` is a planned enhancement for future versions. Currently, backup output is captured only when running manually via `docker compose run`.

### Verify Backup Content

For SQLite backups, verify the database backup is valid:

```bash
ssh -i fixtures/testing_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
  torrust@<instance-ip> "cd /opt/torrust/storage/backup/sqlite && gunzip -c sqlite_*.db.gz | file -"
```

Expected: `SQLite 3.x database`

For configuration backups, list contents:

```bash
ssh -i fixtures/testing_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
  torrust@<instance-ip> "tar -tzf /opt/torrust/storage/backup/config/config_*.tar.gz | head -20"
```

Expected: Files like `tracker.toml`, `prometheus.yml`, etc.

---

## Troubleshooting

### No Backup Files Created

**Symptoms**: `/opt/torrust/storage/backup/` is empty or missing subdirectories

**Possible causes**:

1. Backup not configured in environment (check `environment.json`)
2. Release command failed to deploy backup configuration
3. Backup container not running

**Solutions**:

```bash
# 1. Verify backup was configured
cat data/<env-name>/environment.json | jq '.Running.context.user_inputs.backup'

# 2. Check if backup directory exists
ls -la /opt/torrust/storage/backup/

# 3. Check backup container logs
docker compose logs backup | tail -50

# 4. Try running backup manually
docker compose run --rm backup
```

### Backup Files Not Updating

**Symptoms**: Backup files exist but have old timestamps (older than cron schedule suggests)

**Possible causes**:

1. Crontab not installed or incorrect
2. Docker daemon not running
3. Insufficient disk space

**Solutions**:

```bash
# 1. Verify crontab is installed
crontab -l

# 2. Check Docker status
docker ps -a

# 3. Check disk space
df -h /opt/torrust/storage/backup/

# 4. Run backup manually to test
docker compose run --rm backup
```

### MySQL Connection Error

**Symptoms**: Backup log shows "Access denied" or "Connection refused" for MySQL

**Possible causes**:

1. MySQL service not healthy
2. Database credentials incorrect
3. Backup service not on same network as MySQL

**Solutions**:

```bash
# 1. Check MySQL service is running
docker compose ps mysql

# 2. Check MySQL is healthy
docker compose exec mysql mysql -u root -p<password> -e "SELECT 1"

# 3. Check backup service has database network
docker compose config | grep -A 30 'backup:'
```

### MySQL TLS/SSL Warning

**Symptoms**: Backup log shows "SSL error" warning but backup still completes

**Status**: ✅ **Expected and not a problem**

The warning appears because the backup user lacks the PROCESS privilege for tablespace metadata, but the backup container is configured to skip strict SSL verification. The database backup is created successfully.

### Retention Cleanup Not Running

**Symptoms**: Old backup files not being deleted after retention period

**Possible causes**:

1. Backup script not cleaning up (check manual backup output)
2. Insufficient disk permissions
3. Backup files have wrong ownership/permissions

**Solutions**:

```bash
# 1. Run backup manually and check for cleanup messages
docker compose run --rm backup

# 2. Check file permissions
ls -la /opt/torrust/storage/backup/*/

# 3. Check backup configuration
cat /opt/torrust/storage/backup/etc/backup.conf
```

### Backup Container Shows as "Exited"

**Status**: ✅ **This is normal**

The backup container is configured with `restart: no`, which means:

- It runs once (on schedule or manual trigger)
- Container exits after completing backup
- Service shows as "Exited (0)" - exit code 0 = success
- This is the correct behavior

---

## Database-Specific Notes

### SQLite Backups

**How it works:**

1. Database file located at `/data/storage/tracker/lib/tracker.db`
2. Backup compresses the entire database file: `sqlite_<timestamp>.db.gz`
3. No need to stop database (SQLite file-based)
4. Minimal downtime: Only brief lock during file read

**File format:**

- Backup is complete SQLite database file (compressed)
- Can be restored by decompressing and copying back
- Compatible with SQLite command-line tools

**Storage:**

- Typical size: 4-10 KB (compressed)
- Increases with tracker activity (larger database)

### MySQL Backups

**How it works:**

1. Uses `mysqldump` to export database structure and data
2. Creates SQL dump file: `mysql_<timestamp>.sql.gz`
3. Must connect to MySQL service
4. Backup user: `tracker_user` with full database privileges

**File format:**

- Backup is SQL dump (compressed)
- Contains `CREATE TABLE` statements and `INSERT` statements
- Compatible with MySQL command-line: `mysql < backup.sql`

**Expected warnings:**

Backup logs may show:

```text
mysqldump: Error: 'Access denied; you need (at least one of) the PROCESS privilege(s) for this operation'
```

This is **expected and not a problem** - the backup user has sufficient privileges for table backup.

**Storage:**

- Typical size: 4-50 KB (compressed)
- Depends on database size and tracker activity

---

## Manual Backup Execution

### Running a Backup On-Demand

You can trigger a backup manually anytime:

```bash
# SSH to your VM
ssh -i fixtures/testing_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
  torrust@<instance-ip>

# Navigate to application directory
cd /opt/torrust

# Run backup immediately
docker compose run --rm backup
```

This creates a new backup file with current timestamp, independent of the cron schedule.

**Use cases for manual backup:**

- Before making configuration changes
- Before deploying software updates
- Before scaling operations
- Testing backup restoration

---

## Recovery (Future Enhancement)

Backup restoration is a planned feature. Currently, recovery requires manual steps:

1. Stop the tracker service
2. Download backup file from VM
3. Decompress backup file
4. Restore database from backup
5. Restart tracker service

Recovery procedures will be documented once the feature is implemented.

---

## Best Practices

1. **Choose appropriate backup frequency**:
   - High-traffic tracker: Daily backups (or more frequent)
   - Medium-traffic tracker: Daily backups
   - Low-traffic tracker: Weekly backups are sufficient

2. **Monitor backup disk usage**:
   - Check `/var/log/torrust-backup.log` regularly
   - Use `du -sh /opt/torrust/storage/backup/` to monitor growth
   - Adjust retention_days if disk space becomes an issue

3. **Schedule backups during off-peak hours**:
   - Backup briefly stops the tracker (~10-15 seconds)
   - Schedule when user traffic is lowest
   - Avoid peak usage times

4. **Test backup restoration occasionally**:
   - Verify backups are actually restorable
   - Document restoration procedures
   - Test with staging environment first

5. **Keep configuration and database backups in sync**:
   - Both are backed up together automatically
   - Enables consistent restoration
   - Don't delete backups manually unless necessary

6. **Monitor backup execution**:

   For now, verify backups exist by checking the filesystem:

   ```bash
   # SSH to VM
   ssh -i fixtures/testing_rsa -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
     torrust@<instance-ip>

   # Check backup files
   ls -lh /opt/torrust/storage/backup/sqlite/
   ls -lh /opt/torrust/storage/backup/config/

   # When manually triggered, backup output appears on stdout
   cd /opt/torrust
   docker compose run --rm backup
   ```

   Backup logging to `/var/log/torrust-backup.log` is planned for a future release.

---

## See Also

- [Manual Backup Verification Guide]../e2e-testing/manual/backup-verification.md - Step-by-step verification procedures
- [Create Environment Command]commands/create.md - Backup configuration during environment creation
- [Release Command]commands/release.md - How backup service is deployed
- [Run Command]commands/run.md - Initial backup during deployment