|
| 1 | +# Decision: Bind Mount Standardization for Docker Compose |
| 2 | + |
| 3 | +## Status |
| 4 | + |
| 5 | +Accepted |
| 6 | + |
| 7 | +## Date |
| 8 | + |
| 9 | +2026-01-24 |
| 10 | + |
| 11 | +## Context |
| 12 | + |
| 13 | +The Docker Compose template currently uses a mix of named volumes and bind mounts for persistent data: |
| 14 | + |
| 15 | +```yaml |
| 16 | +# Bind mounts (host path → container path) - CURRENT PATTERN |
| 17 | +- ./storage/tracker/lib:/var/lib/torrust/tracker:Z |
| 18 | + |
| 19 | +# Named volumes (volume name → container path) - PROBLEMATIC |
| 20 | +- caddy_data:/data |
| 21 | +- grafana_data:/var/lib/grafana |
| 22 | +- mysql_data:/var/lib/mysql |
| 23 | +``` |
| 24 | +
|
| 25 | +This inconsistency creates several problems: |
| 26 | +
|
| 27 | +### 1. Observability |
| 28 | +
|
| 29 | +- Named volumes hide data in `/var/lib/docker/volumes/` - not obvious to users |
| 30 | +- Users cannot easily see where persistent data is stored |
| 31 | +- File system tools (ls, du, find) don't work directly on named volume data |
| 32 | + |
| 33 | +### 2. Backup Complexity |
| 34 | + |
| 35 | +- Named volumes require `docker volume` commands or finding the internal path |
| 36 | +- No single command can back up all data |
| 37 | +- Docker-specific tooling is required |
| 38 | +- Standard backup scripts don't work without modification |
| 39 | + |
| 40 | +### 3. Restore Complexity |
| 41 | + |
| 42 | +- Restoring named volumes requires Docker volume recreation |
| 43 | +- Cannot simply copy files to restore data |
| 44 | +- Migration between hosts requires Docker volume export/import |
| 45 | + |
| 46 | +### 4. Inconsistency |
| 47 | + |
| 48 | +- Some services use bind mounts, others use named volumes |
| 49 | +- Different patterns for different services create cognitive overhead |
| 50 | +- No predictable directory structure |
| 51 | + |
| 52 | +### 5. Portability Limitations |
| 53 | + |
| 54 | +- Named volumes cannot be moved between hosts by copying files |
| 55 | +- Docker volume export/import dance is required |
| 56 | +- Tied to Docker's internal storage format |
| 57 | + |
| 58 | +### 6. Debugging & Troubleshooting Difficulties |
| 59 | + |
| 60 | +- Cannot directly inspect files without entering containers |
| 61 | +- Checking file permissions, ownership, disk usage is difficult |
| 62 | +- Cannot modify config files directly for debugging |
| 63 | +- Log files not accessible without `docker logs` |
| 64 | + |
| 65 | +### 7. Development Experience |
| 66 | + |
| 67 | +- Cannot easily reset state by deleting directories |
| 68 | +- Cannot pre-populate data for testing scenarios |
| 69 | +- IDE file watchers cannot observe changes in named volumes |
| 70 | + |
| 71 | +### 8. Deployment Architecture Complexity |
| 72 | + |
| 73 | +- Named volumes require a top-level `volumes:` section in docker-compose.yml |
| 74 | +- Must derive which volumes are required based on enabled services |
| 75 | +- Ansible must manage both directories and Docker volumes |
| 76 | + |
| 77 | +### 9. Security Visibility |
| 78 | + |
| 79 | +- File permissions are hidden inside Docker volume directories |
| 80 | +- SELinux labels cannot be applied consistently |
| 81 | +- Data locations are not transparent to users |
| 82 | + |
| 83 | +## Decision |
| 84 | + |
| 85 | +Standardize on **bind mounts exclusively** for all persistent data in Docker Compose deployments. |
| 86 | + |
| 87 | +All persistent data will be stored under `./storage/{service}/`: |
| 88 | + |
| 89 | +| Service | Bind Mount | Data Location | |
| 90 | +| ---------- | ------------------------------------------ | -------------------------- | |
| 91 | +| Tracker | `./storage/tracker/lib:/var/lib/torrust` | `./storage/tracker/lib` | |
| 92 | +| Tracker | `./storage/tracker/log:/var/log/torrust` | `./storage/tracker/log` | |
| 93 | +| Tracker | `./storage/tracker/etc:/etc/torrust` | `./storage/tracker/etc` | |
| 94 | +| Caddy | `./storage/caddy/data:/data` | `./storage/caddy/data` | |
| 95 | +| Caddy | `./storage/caddy/config:/config` | `./storage/caddy/config` | |
| 96 | +| Caddy | `./storage/caddy/etc/Caddyfile:/etc/...` | `./storage/caddy/etc` | |
| 97 | +| Grafana | `./storage/grafana/data:/var/lib/grafana` | `./storage/grafana/data` | |
| 98 | +| Prometheus | `./storage/prometheus/etc:/etc/prometheus` | `./storage/prometheus/etc` | |
| 99 | +| MySQL | `./storage/mysql/data:/var/lib/mysql` | `./storage/mysql/data` | |
| 100 | + |
| 101 | +Mount options: |
| 102 | + |
| 103 | +- `:ro` - Read-only for config files that shouldn't be modified |
| 104 | +- `:Z` - SELinux private relabeling for writable data directories |
| 105 | + |
| 106 | +## Consequences |
| 107 | + |
| 108 | +### Positive |
| 109 | + |
| 110 | +- **Simplified backup**: Single command `cp -r ./storage/ backup/` backs up everything |
| 111 | +- **Easy restore**: Copy files back to `./storage/` to restore |
| 112 | +- **Full observability**: All persistent data is visible at predictable paths |
| 113 | +- **Consistent pattern**: Same approach for all services |
| 114 | +- **Portable**: Data directory can be moved between hosts by copying |
| 115 | +- **Easy debugging**: Direct file inspection without entering containers |
| 116 | +- **Better development experience**: Reset state by deleting directories |
| 117 | +- **Simpler deployment**: No top-level `volumes:` section needed in docker-compose.yml |
| 118 | +- **Security visibility**: File permissions are visible and controllable |
| 119 | + |
| 120 | +### Negative |
| 121 | + |
| 122 | +- **Explicit directory creation**: Directories must be created with correct permissions before container start |
| 123 | +- **Permission management**: Must ensure correct ownership for non-root containers (Grafana: 472:472, MySQL: 999:999) |
| 124 | +- **SELinux handling**: Must apply `:Z` suffix for writable directories on SELinux systems |
| 125 | +- **Additional Ansible playbooks**: Need playbooks to create directories with correct ownership |
| 126 | + |
| 127 | +### Risks |
| 128 | + |
| 129 | +- **Breaking change**: Existing deployments using named volumes will need migration |
| 130 | +- **Permission errors**: Incorrect directory ownership will prevent containers from starting |
| 131 | + |
| 132 | +### Mitigation |
| 133 | + |
| 134 | +- Create new Ansible playbooks for Grafana and MySQL directory creation with correct ownership |
| 135 | +- Document migration path for existing deployments |
| 136 | +- E2E tests will verify correct permission handling |
| 137 | + |
| 138 | +## Alternatives Considered |
| 139 | + |
| 140 | +### 1. Named Volumes Only |
| 141 | + |
| 142 | +**Rejected** because: |
| 143 | + |
| 144 | +- Data is hidden in `/var/lib/docker/volumes/` |
| 145 | +- Backup requires Docker-specific commands |
| 146 | +- Inconsistent with our observability principles |
| 147 | +- Users cannot easily access or inspect persistent data |
| 148 | + |
| 149 | +### 2. Mixed Approach (Current State) |
| 150 | + |
| 151 | +**Rejected** because: |
| 152 | + |
| 153 | +- Inconsistency creates confusion and maintenance burden |
| 154 | +- Different services have different storage patterns |
| 155 | +- No single backup strategy works for all services |
| 156 | +- Cognitive overhead for developers and operators |
| 157 | + |
| 158 | +### 3. Docker Volume Plugins |
| 159 | + |
| 160 | +**Rejected** because: |
| 161 | + |
| 162 | +- Overkill for single-VM deployments |
| 163 | +- Adds complexity and external dependencies |
| 164 | +- Our deployment model is single-VM, not distributed |
| 165 | +- Standard bind mounts meet all our requirements |
| 166 | + |
| 167 | +## Related Decisions |
| 168 | + |
| 169 | +- [Grafana Integration Pattern](./grafana-integration-pattern.md) - This ADR supersedes the volume recommendations in that decision |
| 170 | +- [Configuration Directories as Secrets](./configuration-directories-as-secrets.md) - Related security considerations for data directories |
| 171 | + |
| 172 | +## References |
| 173 | + |
| 174 | +- [Docker Compose bind mounts documentation](https://docs.docker.com/compose/compose-file/07-volumes/) |
| 175 | +- [SELinux and Docker](https://docs.docker.com/storage/bind-mounts/#configure-the-selinux-label) |
| 176 | +- [Refactoring Plan: Docker Compose Topology Domain Model](../refactors/plans/docker-compose-topology-domain-model.md) |
0 commit comments