|
| 1 | +# Environment Variable Injection in Docker Compose Templates |
| 2 | + |
| 3 | +- **Status**: ✅ Accepted |
| 4 | +- **Date**: 2025-12-13 |
| 5 | +- **Related Issues**: #232 (MySQL Slice - Release and Run Commands) |
| 6 | +- **Related Decisions**: |
| 7 | + - [Tera Minimal Templating Strategy](./tera-minimal-templating-strategy.md) |
| 8 | + - [Environment Variable Prefix](./environment-variable-prefix.md) |
| 9 | + |
| 10 | +## Context |
| 11 | + |
| 12 | +When implementing MySQL support for the tracker deployment (#232), we initially hardcoded MySQL credentials directly in the `docker-compose.yml.tera` template using Tera template variables: |
| 13 | + |
| 14 | +```yaml |
| 15 | +# ❌ INCORRECT: Hardcoded values at template generation time |
| 16 | +environment: |
| 17 | + - MYSQL_ROOT_PASSWORD={{ database.mysql.root_password }} |
| 18 | + - MYSQL_DATABASE={{ database.mysql.database }} |
| 19 | + - MYSQL_USER={{ database.mysql.user }} |
| 20 | + - MYSQL_PASSWORD={{ database.mysql.password }} |
| 21 | +``` |
| 22 | +
|
| 23 | +This approach had a critical flaw: values are embedded into the `docker-compose.yml` file at template generation time (during the `release` command). System administrators managing a deployed system cannot modify these values without regenerating the entire template infrastructure. |
| 24 | + |
| 25 | +The existing tracker service already uses the correct pattern - referencing environment variables from the `.env` file: |
| 26 | + |
| 27 | +```yaml |
| 28 | +# ✅ CORRECT: Reference to .env file variable |
| 29 | +environment: |
| 30 | + - TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN=${TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN} |
| 31 | +``` |
| 32 | + |
| 33 | +This pattern allows system administrators to: |
| 34 | + |
| 35 | +1. Edit the `.env` file with new values |
| 36 | +2. Restart the Docker Compose stack: `docker-compose down && docker-compose up -d` |
| 37 | +3. Have the new configuration take effect immediately |
| 38 | + |
| 39 | +Without regenerating any templates or redeploying the entire application stack. |
| 40 | + |
| 41 | +## Decision |
| 42 | + |
| 43 | +**All configuration values that may need to be changed during system maintenance must be injected via environment variables from the `.env` file, not hardcoded in the `docker-compose.yml` template.** |
| 44 | + |
| 45 | +### Implementation Pattern |
| 46 | + |
| 47 | +**Template Generation (deploy-time)**: |
| 48 | + |
| 49 | +- `EnvContext` contains actual credential values |
| 50 | +- `.env.tera` template renders these values: `MYSQL_ROOT_PASSWORD='{{ mysql_root_password }}'` |
| 51 | +- `docker-compose.yml.tera` references environment variables: `MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}` |
| 52 | + |
| 53 | +**Runtime (maintenance)**: |
| 54 | + |
| 55 | +- System administrator edits `.env` file with new credentials |
| 56 | +- Restarts Docker Compose services |
| 57 | +- New credentials take effect without template regeneration |
| 58 | + |
| 59 | +### Template Documentation |
| 60 | + |
| 61 | +Added comment block to `docker-compose.yml.tera` to prevent future violations: |
| 62 | + |
| 63 | +```yaml |
| 64 | +# IMPORTANT: Environment Variable Injection Pattern |
| 65 | +# ================================================ |
| 66 | +# All configuration values that may need to be changed during maintenance |
| 67 | +# should be injected via environment variables from the .env file, not |
| 68 | +# hardcoded in this docker-compose template. |
| 69 | +# |
| 70 | +# Pattern to follow: |
| 71 | +# CORRECT: - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} |
| 72 | +# INCORRECT: - MYSQL_ROOT_PASSWORD=hardcoded_value |
| 73 | +# |
| 74 | +# Rationale: |
| 75 | +# - System administrators can modify .env values and restart services without |
| 76 | +# regenerating templates |
| 77 | +# - Supports runtime configuration changes without redeployment |
| 78 | +# - Follows Docker Compose best practices for configuration management |
| 79 | +# - Separates template generation (deploy-time) from configuration (runtime) |
| 80 | +``` |
| 81 | + |
| 82 | +## Consequences |
| 83 | + |
| 84 | +### Positive |
| 85 | + |
| 86 | +1. **Runtime Configuration Changes**: System administrators can modify credentials and configuration without regenerating templates or redeploying |
| 87 | +2. **Standard Docker Compose Practice**: Follows official Docker Compose recommendations for environment variable management |
| 88 | +3. **Separation of Concerns**: Clear separation between: |
| 89 | + - Template structure (generated once during release) |
| 90 | + - Configuration values (modifiable during maintenance) |
| 91 | +4. **Consistent Pattern**: All services follow the same environment variable injection pattern |
| 92 | +5. **Security Benefits**: Credentials can be rotated without code changes or redeployment |
| 93 | +6. **Reduced Coupling**: Template generation is independent of specific credential values |
| 94 | + |
| 95 | +### Negative |
| 96 | + |
| 97 | +1. **Template Complexity**: Requires maintaining both `.env.tera` and `docker-compose.yml.tera` templates |
| 98 | +2. **Two-File Coordination**: Developers must ensure `.env.tera` defines all variables referenced in `docker-compose.yml.tera` |
| 99 | +3. **Testing Considerations**: Tests must verify both template generation and environment variable injection work correctly |
| 100 | + |
| 101 | +### Neutral |
| 102 | + |
| 103 | +1. **Documentation Requirement**: Pattern must be clearly documented to prevent future hardcoding |
| 104 | +2. **Code Review Focus**: PRs adding new services must verify environment variable injection pattern |
| 105 | + |
| 106 | +## Implementation Notes |
| 107 | + |
| 108 | +### Files Modified |
| 109 | + |
| 110 | +1. **`src/infrastructure/templating/docker_compose/template/wrappers/env/context.rs`**: |
| 111 | + |
| 112 | + - Extended `EnvContext` to include optional MySQL credentials |
| 113 | + - Added `new_with_mysql()` constructor for MySQL mode |
| 114 | + - Added getters for MySQL fields |
| 115 | + |
| 116 | +2. **`templates/docker-compose/.env.tera`**: |
| 117 | + |
| 118 | + - Added conditional MySQL environment variables section |
| 119 | + - Variables only rendered when MySQL is configured |
| 120 | + |
| 121 | +3. **`templates/docker-compose/docker-compose.yml.tera`**: |
| 122 | + |
| 123 | + - Changed MySQL service to use `${MYSQL_ROOT_PASSWORD}` syntax |
| 124 | + - Added documentation comment explaining the pattern |
| 125 | + - Port hardcoded to 3306 (not configuration-dependent) |
| 126 | + |
| 127 | +4. **`src/application/steps/rendering/docker_compose_templates.rs`**: |
| 128 | + - Updated to create `EnvContext` with MySQL credentials when configured |
| 129 | + - Passes MySQL values to both `EnvContext` and `DockerComposeContext` |
| 130 | + |
| 131 | +### Testing Strategy |
| 132 | + |
| 133 | +- Unit tests verify environment variable references appear in rendered `docker-compose.yml` |
| 134 | +- Tests check for `${MYSQL_ROOT_PASSWORD}` instead of hardcoded values |
| 135 | +- `.env` file generation tests verify MySQL variables are present when configured |
| 136 | + |
| 137 | +## Alternatives Considered |
| 138 | + |
| 139 | +### 1. Hardcode Values in docker-compose.yml |
| 140 | + |
| 141 | +**Approach**: Keep using `{{ database.mysql.root_password }}` in `docker-compose.yml.tera` |
| 142 | + |
| 143 | +**Rejected because**: |
| 144 | + |
| 145 | +- Requires regenerating entire template stack for credential changes |
| 146 | +- Violates Docker Compose best practices |
| 147 | +- Inconsistent with existing tracker service pattern |
| 148 | +- Forces redeployment for routine maintenance tasks |
| 149 | + |
| 150 | +### 2. Use docker-compose.yml Variables with Defaults |
| 151 | + |
| 152 | +**Approach**: Use `${MYSQL_ROOT_PASSWORD:-default_value}` syntax |
| 153 | + |
| 154 | +**Rejected because**: |
| 155 | + |
| 156 | +- Defaults in production deployments are security anti-pattern |
| 157 | +- Still requires `.env` file for actual production values |
| 158 | +- Adds unnecessary complexity |
| 159 | +- No benefit over explicit `.env` requirement |
| 160 | + |
| 161 | +### 3. External Configuration Management (Vault, AWS Secrets Manager) |
| 162 | + |
| 163 | +**Approach**: Fetch credentials from external secrets management system |
| 164 | + |
| 165 | +**Deferred because**: |
| 166 | + |
| 167 | +- Out of scope for current milestone |
| 168 | +- Adds infrastructure dependencies |
| 169 | +- Current pattern is sufficient for target use case |
| 170 | +- Can be added later without breaking changes |
| 171 | + |
| 172 | +## References |
| 173 | + |
| 174 | +- [Docker Compose Environment Variables Documentation](https://docs.docker.com/compose/environment-variables/) |
| 175 | +- [Issue #232: MySQL Slice - Release and Run Commands](https://github.com/torrust/torrust-tracker-deployer/issues/232) |
| 176 | +- [12-Factor App: Config](https://12factor.net/config) |
| 177 | + |
| 178 | +## Notes |
| 179 | + |
| 180 | +- This pattern applies to all Docker Compose services, not just MySQL |
| 181 | +- Future services must follow the same environment variable injection pattern |
| 182 | +- Template variable syntax (`{{ var }}`) should only be used for structural elements that never change at runtime |
| 183 | +- Port mapping for MySQL hardcoded to 3306:3306 as it's not expected to vary per deployment |
0 commit comments