Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
18ef367
feat: implement backup container with comprehensive tests and documen…
josecelano Feb 2, 2026
47ab60c
docs: add deployed instance structure and Phase 1.1b manual testing g…
josecelano Feb 2, 2026
b982214
docs: complete Phase 1.1b manual E2E testing for backup container
josecelano Feb 2, 2026
bb3d5ea
feat: [#315] add GitHub workflows for tracker-backup container
josecelano Feb 2, 2026
0b313ba
docs: [#315] update image name in progress tracking
josecelano Feb 2, 2026
da238ef
fix: [#315] correct security scan results for tracker-backup
josecelano Feb 2, 2026
15c8d6c
feat: [#315] upgrade backup container to Debian 13 (trixie)
josecelano Feb 2, 2026
7da6fac
docs: [#315] update security scan index with backup container
josecelano Feb 2, 2026
7bdad2e
fix: [#315] override entrypoint in container verification step
josecelano Feb 2, 2026
6cb633e
feat: [#315] add backup domain layer with parametrized tests
josecelano Feb 3, 2026
cec6ca7
feat: [#315] add backup configuration DTO with comprehensive help mes…
josecelano Feb 3, 2026
e88c2c8
feat: [#315] integrate backup configuration into environment creation…
josecelano Feb 3, 2026
11651c9
fix: [#315] correct doc examples to include backup parameter
josecelano Feb 3, 2026
9d297cc
fix: [#315] correct Dockerfile COPY paths for CI build context
josecelano Feb 3, 2026
6ff96cb
docs: [#315] add backup section to environment config JSON example
josecelano Feb 3, 2026
b935871
docs: [#315] update Phase 2.1 completion status in issue spec
josecelano Feb 3, 2026
4f8fb4c
feat: [#315] add backup configuration templates and docker-compose se…
josecelano Feb 3, 2026
61a3da3
feat: [#315] add backup template infrastructure with context and rend…
josecelano Feb 3, 2026
86773da
fix: [#315] set Docker build context to repository root in backup wor…
josecelano Feb 3, 2026
d0ce37e
docs: [#315] update Phase 2.2 completion status in issue spec
josecelano Feb 3, 2026
832fc0e
refactor: [#315] extract DependencyCondition and ServiceDependency to…
josecelano Feb 3, 2026
062f4dc
feat: [#315] Add backup release workflow integration
josecelano Feb 3, 2026
1594b4a
fix: [#315] Correct SQLite database path in backup configuration
josecelano Feb 3, 2026
237e64c
fix: [#315] Simplify MySQL backup SSL configuration by embedding in D…
josecelano Feb 3, 2026
a4e7bd9
docs: [#315] Update backup verification guide with MySQL-specific lea…
josecelano Feb 3, 2026
5cd5b4a
docs: [#315] Update progress tracking with MySQL backup verification …
josecelano Feb 3, 2026
549fcaf
feat: [#315] Step 2.4 - Update create template command to include bac…
josecelano Feb 3, 2026
5cecf69
docs: [#315] Update progress tracking - Step 2.4 complete
josecelano Feb 3, 2026
0e563fd
feat: [#315] Step 3.1 - Add crontab templates and renderers for sched…
josecelano Feb 3, 2026
1611b63
docs: [#315] Phase 3 - Update progress tracking with implementation c…
josecelano Feb 4, 2026
22246dc
fix: [#315] Properly fix Docker build context to avoid regression
josecelano Feb 4, 2026
11c5b0d
docs: [#315] Document Docker build context to prevent regression
josecelano Feb 4, 2026
0e3cf3e
fix: [#315] Use correct build context for backup image in security sc…
josecelano Feb 4, 2026
9c366ab
docs: [#315] Add backup container workflow badge to README
josecelano Feb 4, 2026
ad4ead6
feat: [#315] Step 3.2 & 3.3 - Crontab installation and wiring
josecelano Feb 4, 2026
ca0e883
fix: [#315] Fix markdown emphasis spacing errors in documentation
josecelano Feb 4, 2026
46a50ca
fix: Fix provisioned-instance Dockerfile COPY paths for build context
josecelano Feb 4, 2026
2ed37cf
fix: Set correct Docker build context for provisioned-instance E2E tests
josecelano Feb 4, 2026
99632b7
fix: Set correct Docker build context for provisioned-instance E2E tests
josecelano Feb 4, 2026
97891c4
docs: Update backup-verification.md to document automatic crontab-bas…
josecelano Feb 4, 2026
a49e24a
feat: [#315] Phase 4 Part 2 - E2E backup verification tests
josecelano Feb 4, 2026
d0d024d
docs: Update Phase 4 progress - Parts 1 & 2 nearly complete
josecelano Feb 4, 2026
14b1d5e
docs: Clarify that initial backups are not yet automatically created …
josecelano Feb 4, 2026
44c2830
docs: Update Phase 4 completion notes
josecelano Feb 4, 2026
36c5675
fix: Add blank line before code fence in run.md for markdown linting
josecelano Feb 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 219 additions & 0 deletions .github/workflows/backup-container.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# Backup Container workflow for Torrust Tracker Deployer
#
# This workflow builds, tests, and publishes the backup Docker image.
# Following patterns from container.yaml workflow.
#
# Triggers:
# - Push to main/develop branches (only when backup container files change)
# - Pull requests to main/develop (only when backup container files change)
# - Manual dispatch
#
# Publishing:
# - Images are pushed to Docker Hub on push to main/develop (not PRs)
# - Requires Docker Hub credentials in repository secrets (dockerhub-torrust-backup environment)

name: Backup Container

on:
push:
branches:
- "develop"
- "main"
paths:
- "docker/backup/**"
- ".github/workflows/backup-container.yaml"

pull_request:
branches:
- "develop"
- "main"
paths:
- "docker/backup/**"
- ".github/workflows/backup-container.yaml"

workflow_dispatch:

env:
CARGO_TERM_COLOR: always
DOCKER_HUB_USERNAME: torrust

jobs:
test:
name: Build & Test
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build Image
uses: docker/build-push-action@v6
with:
# CRITICAL: Context must be ./docker/backup (not ./)
# Docker COPY/ADD commands resolve paths RELATIVE TO BUILD CONTEXT, not Dockerfile location.
# Setting context: ./docker/backup allows the Dockerfile to use simple relative paths like:
# COPY backup.sh /scripts/backup.sh
# This creates consistency between local builds and CI builds.
#
# Regression History: commit 9d297cc5 used context: . with full paths in Dockerfile
# (e.g., COPY docker/backup/backup.sh). This worked but was confusing and error-prone
# for future changes. The current approach is cleaner and prevents regression.
#
# See docker/backup/README.md "Building the Container" section for details.
context: ./docker/backup
file: ./docker/backup/Dockerfile
target: production
push: false
load: true
tags: torrust/tracker-backup:local
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Inspect Image
run: docker image inspect torrust/tracker-backup:local

- name: Verify Container Structure
run: |
echo "=== Verifying backup container structure ==="

echo "=== Checking backup script exists ==="
docker run --rm --entrypoint ls torrust/tracker-backup:local -lh /scripts/backup.sh

echo "=== Checking backup directories ==="
docker run --rm --entrypoint ls torrust/tracker-backup:local -ld /backups/mysql /backups/sqlite /backups/config

echo "=== Verifying tools installed ==="
docker run --rm --entrypoint which torrust/tracker-backup:local bash
docker run --rm --entrypoint which torrust/tracker-backup:local mysql
docker run --rm --entrypoint which torrust/tracker-backup:local sqlite3
docker run --rm --entrypoint which torrust/tracker-backup:local gzip
docker run --rm --entrypoint which torrust/tracker-backup:local tar

echo "=== Checking entrypoint ==="
docker inspect --format='{{.Config.Entrypoint}}' torrust/tracker-backup:local

- name: Test Container Execution
run: |
echo "=== Testing backup container without config (should fail gracefully) ==="
docker run --rm torrust/tracker-backup:local || true

context:
name: Context
needs: test
runs-on: ubuntu-latest

outputs:
continue: ${{ steps.check.outputs.continue }}
type: ${{ steps.check.outputs.type }}

steps:
- name: Check Context
id: check
run: |
if [[ "${{ github.repository }}" == "torrust/torrust-tracker-deployer" ]]; then
if [[ "${{ github.event_name }}" == "push" ]]; then
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "type=production" >> $GITHUB_OUTPUT
echo "continue=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == "refs/heads/develop" ]]; then
echo "type=development" >> $GITHUB_OUTPUT
echo "continue=true" >> $GITHUB_OUTPUT
fi
fi
fi

# Default: don't continue
if [[ -z "$(cat $GITHUB_OUTPUT 2>/dev/null)" ]]; then
echo "continue=false" >> $GITHUB_OUTPUT
fi

publish_development:
name: Publish (Development)
environment: dockerhub-torrust-backup
needs: context
if: needs.context.outputs.continue == 'true' && needs.context.outputs.type == 'development'
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Docker Meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.DOCKER_HUB_USERNAME }}/tracker-backup
tags: |
type=ref,event=branch
type=sha,prefix=dev-

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build and Push
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/backup/Dockerfile
target: production
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

publish_production:
name: Publish (Production)
environment: dockerhub-torrust-backup
needs: context
if: needs.context.outputs.continue == 'true' && needs.context.outputs.type == 'production'
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Docker Meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.DOCKER_HUB_USERNAME }}/tracker-backup
tags: |
type=raw,value=latest
type=ref,event=branch
type=sha

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build and Push
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/backup/Dockerfile
target: production
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
5 changes: 4 additions & 1 deletion .github/workflows/docker-security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ jobs:
- dockerfile: docker/ssh-server/Dockerfile
context: docker/ssh-server
name: ssh-server
- dockerfile: docker/backup/Dockerfile
context: docker/backup
name: tracker-backup

steps:
- name: Checkout code
Expand All @@ -54,7 +57,7 @@ jobs:
docker build \
-t torrust-tracker-deployer/${{ matrix.image.name }}:latest \
-f ${{ matrix.image.dockerfile }} \
.
${{ matrix.image.context }}

# Human-readable output in logs
# This NEVER fails the job; it’s only for visibility
Expand Down
36 changes: 33 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ These principles should guide all development decisions, code reviews, and featu
7. **Before working with Tera templates**: Read [`docs/contributing/templates/tera.md`](docs/contributing/templates/tera.md) for correct variable syntax - use `{{ variable }}` not `{ { variable } }`. Tera template files have the `.tera` extension.

8. **When adding new Ansible playbooks**: Read [`docs/contributing/templates/ansible.md`](docs/contributing/templates/ansible.md) and the ADR [`atomic-ansible-playbooks.md`](docs/decisions/atomic-ansible-playbooks.md).
- **CRITICAL: One playbook = one responsibility** (atomic playbook rule)
- Conditional enablement belongs in Rust commands/steps, not in Ansible `when:` clauses (use `when:` only for host facts)
- Static playbooks must be registered in `src/infrastructure/external_tools/ansible/template/renderer/project_generator.rs` under `copy_static_templates()` so they are copied into the build directory
- **CRITICAL: One playbook = one responsibility** (atomic playbook rule)
- Conditional enablement belongs in Rust commands/steps, not in Ansible `when:` clauses (use `when:` only for host facts)
- Static playbooks must be registered in `src/infrastructure/external_tools/ansible/template/renderer/project_generator.rs` under `copy_static_templates()` so they are copied into the build directory

9. **When handling errors in code**: Read [`docs/contributing/error-handling.md`](docs/contributing/error-handling.md) for error handling principles. Prefer explicit enum errors over anyhow for better pattern matching and user experience. Make errors clear, include sufficient context for traceability, and ensure they are actionable with specific fix instructions.

Expand Down Expand Up @@ -151,6 +151,36 @@ These principles should guide all development decisions, code reviews, and featu

20. **When generating environment configurations** (for AI agents): Reference the Rust types in [`src/application/command_handlers/create/config/`](src/application/command_handlers/create/config/) for accurate constraint information. These types express richer validation rules than the JSON schema alone (e.g., `NonZeroU32`, tagged enums, newtype wrappers). Read the [README](src/application/command_handlers/create/config/README.md) in that folder for the full guide. The JSON schema (`schemas/environment-config.json`) provides basic structure, but the Rust types are authoritative for constraints. See the [ADR](docs/decisions/configuration-dto-layer-placement.md) for why these types are in the application layer.

## πŸ—οΈ Deployed Instance Structure

After running the complete deployment workflow (`create β†’ provision β†’ configure β†’ release β†’ run`), the virtual machine has the following structure:

```text
/opt/torrust/ # Application root directory
β”œβ”€β”€ docker-compose.yml # Main orchestration file
β”œβ”€β”€ .env # Environment variables
└── storage/ # Persistent data volumes
β”œβ”€β”€ tracker/
β”‚ β”œβ”€β”€ lib/ # Database files (tracker.db for SQLite)
β”‚ β”œβ”€β”€ log/ # Tracker logs
β”‚ └── etc/ # Configuration (tracker.toml)
β”œβ”€β”€ prometheus/
β”‚ └── etc/ # Prometheus configuration
└── grafana/
β”œβ”€β”€ data/ # Grafana database
└── provisioning/ # Dashboards and datasources
```

**Key commands inside the VM**:

```bash
cd /opt/torrust # Application root
docker compose ps # Check services
docker compose logs tracker # View logs
```

For detailed information about working with deployed instances, see [`docs/user-guide/`](docs/user-guide/README.md).

## πŸ§ͺ Build & Test

- **Setup Dependencies**: `cargo run --bin dependency-installer install` (sets up required development tools)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[![Linting](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/linting.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/linting.yml) [![Testing](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/testing.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/testing.yml) [![E2E Infrastructure Tests](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-infrastructure.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-infrastructure.yml) [![E2E Deployment Tests](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-deployment.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-deployment.yml) [![Test LXD Container Provisioning](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-lxd-provision.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-lxd-provision.yml) [![Coverage](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/coverage.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/coverage.yml) [![Container](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/container.yaml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/container.yaml) [![Docker Security Scan](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/docker-security-scan.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/docker-security-scan.yml)
[![Linting](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/linting.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/linting.yml) [![Testing](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/testing.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/testing.yml) [![E2E Infrastructure Tests](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-infrastructure.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-infrastructure.yml) [![E2E Deployment Tests](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-deployment.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-deployment.yml) [![Test LXD Container Provisioning](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-lxd-provision.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-lxd-provision.yml) [![Coverage](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/coverage.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/coverage.yml) [![Container](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/container.yaml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/container.yaml) [![Backup Container](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/backup-container.yaml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/backup-container.yaml) [![Docker Security Scan](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/docker-security-scan.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/docker-security-scan.yml)

# Torrust Tracker Deployer

Expand Down
98 changes: 98 additions & 0 deletions docker/backup/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# ============================================================================
# Torrust Backup Container
# ============================================================================
# Production backup container for Torrust Tracker deployments.
# Configuration is provided via mounted config files - no environment variables.
#
# Configuration Files:
# /etc/backup/backup.conf - Main configuration (sourced by backup.sh)
# /etc/backup/backup-paths.txt - List of files/directories to backup
#
# Mount Points:
# /backups - Output directory for all backups (read-write)
# /data - Source data directory (read-only, app storage mounted here)
#
# Output Structure:
# /backups/mysql/mysql_YYYYMMDD_HHMMSS.sql.gz - MySQL dumps (compressed)
# /backups/sqlite/sqlite_YYYYMMDD_HHMMSS.db.gz - SQLite backups (compressed)
# /backups/config/config_YYYYMMDD_HHMMSS.tar.gz - Config archives (compressed)
#
# Security:
# Container runs as uid 1000 (torrust user) to match app file ownership.
# This ensures backup files have correct ownership on host.
#
# Testing:
# Tests run during build using bats-core. Build fails if tests fail.
# ============================================================================

FROM debian:trixie-slim AS base

# Install required utilities
# - bash: for scripting
# - default-mysql-client: MariaDB client (compatible with MySQL 8)
# - sqlite3: SQLite client for .backup command
# - gzip: for compression
# - tar: for config file archiving
RUN apt-get update && apt-get install -y --no-install-recommends \
bash \
default-mysql-client \
sqlite3 \
gzip \
tar \
&& rm -rf /var/lib/apt/lists/*

# =============================================================================
# Test Stage - Run unit tests during build
# =============================================================================
FROM base AS test

# Install bats-core for testing
RUN apt-get update && apt-get install -y --no-install-recommends \
bats \
&& rm -rf /var/lib/apt/lists/*

# Copy test files
COPY backup.sh /scripts/backup.sh
COPY backup_test.bats /scripts/backup_test.bats
RUN chmod +x /scripts/backup.sh

# Run tests - build fails if tests fail
# Create a marker file to prove tests passed
RUN cd /scripts && bats backup_test.bats && touch /scripts/.tests_passed

# =============================================================================
# Production Stage
# =============================================================================
FROM base AS production

# Require tests to have passed by copying marker from test stage
# This ensures test stage is always executed before production stage
COPY --from=test /scripts/.tests_passed /tmp/.tests_passed

# Create backup user with same UID as torrust app user
# This ensures backup files have correct ownership on host
# Using 'torrust' as the username to match the app user
ARG BACKUP_UID=1000
ARG BACKUP_GID=1000
RUN groupadd -g ${BACKUP_GID} torrust 2>/dev/null || true && \
useradd -u ${BACKUP_UID} -g ${BACKUP_GID} -s /bin/bash torrust 2>/dev/null || true

# Create directories with correct ownership
RUN mkdir -p /scripts /backups/mysql /backups/sqlite /backups/config /etc/mysql && \
chown -R ${BACKUP_UID}:${BACKUP_GID} /backups

# Create MySQL client configuration (disable SSL verification for Docker connections)
RUN cat > /etc/mysql/mysql-client.cnf <<'EOF' && \
chmod 644 /etc/mysql/mysql-client.cnf
[mysqldump]
ssl=FALSE
EOF

# Copy backup script (tests already passed in test stage)
COPY backup.sh /scripts/backup.sh
RUN chmod +x /scripts/backup.sh

# Run as non-root user (torrust, uid 1000)
USER torrust

ENTRYPOINT ["/scripts/backup.sh"]
Loading
Loading