diff --git a/.icons/restic.svg b/.icons/restic.svg
new file mode 100644
index 000000000..976d894a9
--- /dev/null
+++ b/.icons/restic.svg
@@ -0,0 +1,590 @@
+
\ No newline at end of file
diff --git a/registry/coder/modules/restic/README.md b/registry/coder/modules/restic/README.md
new file mode 100644
index 000000000..6cd25a45a
--- /dev/null
+++ b/registry/coder/modules/restic/README.md
@@ -0,0 +1,523 @@
+---
+display_name: Restic Backup
+description: Cloud-backed ephemeral workspaces with automatic backup on stop and restore on start using Restic
+icon: ../../../../.icons/restic.svg
+verified: false
+tags: [backup, restore, cloud, restic, s3, b2]
+---
+
+# Restic Backup
+
+Automatic cloud backups for Coder workspaces. Backs up on stop, restores on start.
+
+## Features
+
+- Auto backup/restore on workspace stop/start
+- Works with S3, B2, Azure, GCS, SFTP, local storage
+- Encrypted and deduplicated
+- Workspace-aware tagging for easy browsing
+- Configurable retention policies
+- Clone backups between workspaces
+
+## Quick Start
+
+```tf
+module "restic" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/restic/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ repository = "s3:s3.amazonaws.com/my-workspace-backups"
+ password = var.restic_password
+
+ env = {
+ AWS_ACCESS_KEY_ID = var.aws_access_key
+ AWS_SECRET_ACCESS_KEY = var.aws_secret_key
+ }
+}
+```
+
+## How It Works
+
+1. Workspace stops → automatic backup to cloud
+2. Workspace starts → automatic restore from backup
+3. Backups are tagged with `workspace-id`, `workspace-owner`, `workspace-name`
+4. Auto-restore uses `workspace-id` to find the correct backup
+5. Manually restore any backup using `snapshot_id`
+
+## Storage Backend Configuration
+
+### AWS S3
+
+[Official Restic S3 Documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#amazon-s3)
+
+```tf
+module "restic" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/restic/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ repository = "s3:s3.amazonaws.com/my-bucket/workspace-backups"
+ password = var.restic_password
+
+ env = {
+ AWS_ACCESS_KEY_ID = var.aws_access_key
+ AWS_SECRET_ACCESS_KEY = var.aws_secret_key
+ AWS_DEFAULT_REGION = "us-east-1"
+ }
+}
+```
+
+### Backblaze B2 (Cost-Effective)
+
+[Official Restic B2 Documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#backblaze-b2)
+
+```tf
+module "restic" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/restic/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ repository = "b2:my-bucket:workspace-backups"
+ password = var.restic_password
+
+ env = {
+ B2_ACCOUNT_ID = var.b2_account_id
+ B2_ACCOUNT_KEY = var.b2_account_key
+ }
+}
+```
+
+### Azure Blob Storage
+
+[Official Restic Azure Documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#microsoft-azure-blob-storage)
+
+```tf
+module "restic" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/restic/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ repository = "azure:container-name:/workspace-backups"
+ password = var.restic_password
+
+ env = {
+ AZURE_ACCOUNT_NAME = var.azure_account_name
+ AZURE_ACCOUNT_KEY = var.azure_account_key
+ }
+}
+```
+
+### Google Cloud Storage
+
+[Official Restic GCS Documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#google-cloud-storage)
+
+```tf
+module "restic" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/restic/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ repository = "gs:my-bucket:/workspace-backups"
+ password = var.restic_password
+
+ env = {
+ GOOGLE_PROJECT_ID = var.gcp_project_id
+ GOOGLE_APPLICATION_CREDENTIALS = "/path/to/service-account.json"
+ }
+}
+```
+
+### MinIO or S3-Compatible Storage
+
+[Official Restic Minio Documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#minio-server) | [S3-Compatible](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#s3-compatible-storage)
+
+```tf
+module "restic" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/restic/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ repository = "s3:http://minio.company.com:9000/workspace-backups"
+ password = var.restic_password
+
+ env = {
+ AWS_ACCESS_KEY_ID = var.minio_access_key
+ AWS_SECRET_ACCESS_KEY = var.minio_secret_key
+ }
+}
+```
+
+### SFTP
+
+[Official Restic SFTP Documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#sftp)
+
+```tf
+module "restic" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/restic/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ repository = "sftp:user@backup-server.com:/backups/restic"
+ password = var.restic_password
+
+ # SSH key should be at ~/.ssh/id_rsa
+ # Or configure custom SSH command:
+ env = {
+ RESTIC_SFTP_COMMAND = "ssh user@host -i /path/to/key -s sftp"
+ }
+}
+```
+
+### Local Directory (Testing)
+
+[Official Restic Local Documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#local)
+
+```tf
+module "restic" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/restic/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ repository = "/backup/restic-repo"
+ password = var.restic_password
+}
+```
+
+**Note:** Use persistent storage (Docker volume, PV) for local repositories.
+
+## Advanced Configuration
+
+### Selective Backup Paths
+
+Only backup specific directories:
+
+```tf
+module "restic" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/restic/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ repository = "s3:s3.amazonaws.com/backups"
+ password = var.restic_password
+
+ backup_paths = [
+ "/home/coder/projects",
+ "/home/coder/.config",
+ "/home/coder/data",
+ ]
+
+ exclude_patterns = [
+ "**/.git",
+ "**/node_modules",
+ "**/__pycache__",
+ "**/target",
+ "**/.venv",
+ "**/tmp",
+ ]
+
+ env = {
+ AWS_ACCESS_KEY_ID = var.aws_access_key
+ AWS_SECRET_ACCESS_KEY = var.aws_secret_key
+ }
+}
+```
+
+### Periodic Backups While Running
+
+Backup every N minutes while workspace is active:
+
+```tf
+module "restic" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/restic/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ repository = "b2:workspace-backups"
+ password = var.restic_password
+
+ # Backup every 30 minutes while workspace is running
+ backup_interval_minutes = 30
+
+ env = {
+ B2_ACCOUNT_ID = var.b2_account_id
+ B2_ACCOUNT_KEY = var.b2_account_key
+ }
+}
+```
+
+### Custom Stop Script
+
+Run cleanup before backup:
+
+```tf
+module "restic" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/restic/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ repository = "s3:s3.amazonaws.com/backups"
+ password = var.restic_password
+
+ custom_stop_script = <<-EOF
+ #!/bin/bash
+ echo "Cleaning up before backup..."
+ rm -rf /tmp/*
+ docker system prune -f
+ find /home/coder -name "*.log" -delete
+ EOF
+
+ env = {
+ AWS_ACCESS_KEY_ID = var.aws_access_key
+ AWS_SECRET_ACCESS_KEY = var.aws_secret_key
+ }
+}
+```
+
+### Clone Another Workspace's Backup
+
+Restore from a specific snapshot:
+
+```tf
+module "restic" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/restic/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ repository = "s3:s3.amazonaws.com/backups"
+ password = var.restic_password
+
+ # Restore from specific snapshot (find ID using: restic snapshots)
+ restore_on_start = true
+ snapshot_id = "abc123def" # The snapshot ID to restore
+
+ env = {
+ AWS_ACCESS_KEY_ID = var.aws_access_key
+ AWS_SECRET_ACCESS_KEY = var.aws_secret_key
+ }
+}
+```
+
+To find snapshot IDs from another workspace:
+
+```bash
+# List all snapshots grouped by workspace
+restic snapshots --group-by tags
+
+# Or filter by specific workspace
+restic snapshots --tag workspace-owner:john --tag workspace-name:dev-workspace
+```
+
+### Custom Retention Policies
+
+Control how many backups to keep:
+
+```tf
+module "restic" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/restic/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ repository = "s3:s3.amazonaws.com/backups"
+ password = var.restic_password
+
+ # Keep last 10 backups
+ retention_keep_last = 10
+
+ # Keep daily backups for 14 days
+ retention_keep_daily = 14
+
+ # Keep weekly backups for 8 weeks
+ retention_keep_weekly = 8
+
+ # Keep monthly backups for 6 months
+ retention_keep_monthly = 6
+
+ # Apply retention automatically
+ auto_forget = true
+
+ # Don't prune on stop (too slow)
+ auto_prune = false
+
+ env = {
+ AWS_ACCESS_KEY_ID = var.aws_access_key
+ AWS_SECRET_ACCESS_KEY = var.aws_secret_key
+ }
+}
+```
+
+### Using HCP Vault Secrets
+
+Store credentials securely:
+
+```tf
+module "vault_secrets" {
+ source = "registry.coder.com/coder/hcp-vault-secrets/coder"
+ version = "1.0.34"
+ agent_id = coder_agent.main.id
+ app_name = "workspace-backups"
+ project_id = var.hcp_project_id
+ secrets = ["RESTIC_PASSWORD", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
+}
+
+module "restic" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/restic/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ repository = "s3:s3.amazonaws.com/backups"
+ password = "" # Will use RESTIC_PASSWORD from vault
+
+ depends_on = [module.vault_secrets]
+}
+```
+
+## Manual Operations
+
+### Trigger Manual Backup
+
+Click the **"Backup Now"** button in the Coder UI, or run from terminal:
+
+```bash
+restic-backup --tag manual-backup
+```
+
+### List Your Workspace's Backups
+
+```bash
+restic snapshots --tag workspace-id:$RESTIC_WORKSPACE_ID
+```
+
+Or view all snapshots:
+
+```bash
+restic snapshots
+```
+
+### List All Workspace Backups in Repository
+
+```bash
+restic snapshots --group-by tags
+```
+
+This shows snapshots grouped by workspace, making it easy to see all workspace backups in the repository.
+
+### Restore Specific Snapshot
+
+```bash
+# List snapshots for this workspace
+restic snapshots --tag workspace-id:$RESTIC_WORKSPACE_ID
+
+# Restore to temporary location for inspection
+restic restore /tmp/restore < snapshot-id > --target
+
+# Or restore to original location
+restic restore / < snapshot-id > --target
+```
+
+### Check Repository Health
+
+```bash
+restic check
+```
+
+### Manual Cleanup
+
+```bash
+# Remove old snapshots for this workspace
+restic forget --tag workspace-id:$RESTIC_WORKSPACE_ID --keep-last 3
+
+# Reclaim space (removes unreferenced data)
+restic prune
+```
+
+## Important Considerations
+
+### Stop Backup Limitations
+
+> **Warning**: The `backup_on_stop` feature may not work on all template types if the agent is terminated before backup completes. See [coder/coder#6174](https://github.com/coder/coder/issues/6174) for details.
+
+**Recommendations**:
+
+- Test stop backups with your specific template
+- Keep backups fast (use selective paths and exclusions)
+- Use `backup_interval_minutes` for important data
+- Set `auto_prune = false` for stop backups (prune is slow)
+
+### Repository Organization
+
+**Single Shared Repository** (Recommended):
+
+- All workspaces share one repository
+- Backups are tagged with workspace metadata
+- Deduplication saves space
+- Easy credential management
+
+**Per-Workspace Repositories**:
+
+- Each workspace uses separate repository
+- More isolation but more complex
+- No cross-workspace restore
+
+### Security
+
+- Repository password encrypts ALL backups
+- Use Coder parameters or external secrets for credentials
+- Backend credentials should have minimal permissions
+- Consider separate repositories for different teams
+
+### Performance Tips
+
+- **Use exclusions**: Skip `.git`, `node_modules`, caches
+- **Selective paths**: Only backup what you need
+- **Interval backups**: Balance frequency vs performance
+- **Retention policies**: Keep low retention to save storage costs
+- **Prune manually**: Don't enable `auto_prune` on stop (too slow)
+
+## Troubleshooting
+
+### Backup Fails on Stop
+
+The workspace might be terminating before backup completes. Try:
+
+- Reducing backup size with selective paths
+- Using interval backups instead
+- Testing with a local repository first
+
+### Restore Blocks Login Too Long
+
+- Reduce restore size with selective backup paths
+- Set `start_blocks_login = false` to allow login during restore
+- Use faster storage backend
+
+### Repository Not Found
+
+Ensure:
+
+- Repository URL is correct
+- Backend credentials are valid
+- Network connectivity to storage backend
+- Repository has been initialized (`auto_init_repo = true`)
+
+### Permission Denied
+
+Check:
+
+- Backend credentials have write permissions
+- Local directory (if used) is writable
+- SSH key (for SFTP) is accessible
+
+### Out of Storage Space
+
+Run cleanup:
+
+```bash
+restic forget --tag workspace-id:$RESTIC_WORKSPACE_ID --keep-last 2
+restic prune
+```
+
+## Links
+
+- [Restic Documentation](https://restic.readthedocs.io/)
+- [Restic GitHub](https://github.com/restic/restic)
+- [Coder Documentation](https://coder.com/docs)
diff --git a/registry/coder/modules/restic/main.test.ts b/registry/coder/modules/restic/main.test.ts
new file mode 100644
index 000000000..0453b8681
--- /dev/null
+++ b/registry/coder/modules/restic/main.test.ts
@@ -0,0 +1,75 @@
+import { describe, expect, it } from "bun:test";
+import {
+ executeScriptInContainer,
+ runTerraformApply,
+ runTerraformInit,
+ testRequiredVariables,
+} from "~test";
+
+describe("restic", async () => {
+ await runTerraformInit(import.meta.dir);
+
+ testRequiredVariables(import.meta.dir, {
+ agent_id: "test-agent-id",
+ repository: "s3:s3.amazonaws.com/test-bucket",
+ password: "test-password",
+ });
+
+ it("installs restic successfully", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ repository: "/tmp/restic-repo",
+ password: "test-password",
+ install_restic: "true",
+ auto_init_repo: "false",
+ restore_on_start: "false",
+ });
+
+ const output = await executeScriptInContainer(
+ state,
+ "alpine",
+ "sh",
+ "apk add --no-cache curl bzip2",
+ );
+
+ if (output.exitCode !== 0) {
+ console.log("Exit code:", output.exitCode);
+ console.log("STDOUT:", output.stdout.join("\n"));
+ console.log("STDERR:", output.stderr.join("\n"));
+ }
+
+ expect(output.exitCode).toBe(0);
+ const stdout = output.stdout.join("\n");
+ expect(stdout).toContain("Restic Backup Module Setup");
+ expect(stdout).toContain("Installing Restic...");
+ expect(stdout).toContain("Detected OS: linux");
+ expect(stdout).toContain("Architecture:");
+ expect(stdout).toContain("Fetching latest version");
+ expect(stdout).toContain("Version:");
+ expect(stdout).toContain("Downloading Restic");
+ expect(stdout).toContain("Restic installed:");
+ expect(stdout).toContain("Restic verified:");
+ expect(stdout).toContain("restic");
+ expect(stdout).toContain("Restic setup complete");
+ });
+
+ it("creates backup helper script in workspace", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ repository: "/tmp/restic-repo",
+ password: "test-password",
+ install_restic: "false",
+ auto_init_repo: "false",
+ restore_on_start: "false",
+ });
+
+ const output = await executeScriptInContainer(state, "alpine");
+
+ const stdout = output.stdout.join("\n");
+
+ expect(stdout).toContain("Installing backup helper script");
+ expect(stdout).toContain("Backup helper installed:");
+ expect(stdout).toContain("/restic-backup");
+ expect(stdout).toContain("Backup helper verified as executable");
+ });
+});
diff --git a/registry/coder/modules/restic/main.tf b/registry/coder/modules/restic/main.tf
new file mode 100644
index 000000000..cd88cb45c
--- /dev/null
+++ b/registry/coder/modules/restic/main.tf
@@ -0,0 +1,271 @@
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = ">= 0.12"
+ }
+ }
+}
+
+data "coder_workspace" "me" {}
+
+data "coder_workspace_owner" "me" {}
+
+variable "agent_id" {
+ type = string
+ description = "The ID of a Coder agent."
+}
+
+variable "repository" {
+ type = string
+ description = "Restic repository location (e.g., 's3:s3.amazonaws.com/bucket', 'b2:bucket-name', '/local/path')."
+}
+
+variable "password" {
+ type = string
+ description = "Password for encrypting the Restic repository. Keep this secure!"
+ sensitive = true
+}
+
+variable "install_restic" {
+ type = bool
+ description = "Whether to install Restic binary."
+ default = true
+}
+
+variable "restic_version" {
+ type = string
+ description = "Version of Restic to install (e.g., '0.16.4' or 'latest')."
+ default = "latest"
+}
+
+variable "backup_paths" {
+ type = list(string)
+ description = "List of paths to backup. Can be absolute or relative to 'directory'."
+ default = ["/home/coder"]
+}
+
+variable "exclude_patterns" {
+ type = list(string)
+ description = "Patterns to exclude from backup (e.g., ['**/.git', '**/node_modules'])."
+ default = []
+}
+
+variable "backup_tags" {
+ type = list(string)
+ description = "Additional tags to apply to all snapshots."
+ default = []
+}
+
+variable "directory" {
+ type = string
+ description = "Working directory for backup operations."
+ default = "~"
+}
+
+variable "backup_on_stop" {
+ type = bool
+ description = "Whether to automatically backup when workspace stops."
+ default = true
+}
+
+variable "backup_interval_minutes" {
+ type = number
+ description = "Backup every N minutes while workspace is running (0 = disabled)."
+ default = 0
+}
+
+variable "restore_on_start" {
+ type = bool
+ description = "Whether to restore from backup when workspace starts."
+ default = true
+}
+
+variable "snapshot_id" {
+ type = string
+ description = "Specific snapshot ID to restore. If empty and restore_on_start is true, restores latest backup of this workspace. If set, restores that specific snapshot (useful for cloning workspaces)."
+ default = ""
+}
+
+variable "restore_target" {
+ type = string
+ description = "Target directory for restore ('/' restores to original paths)."
+ default = "/"
+}
+
+variable "start_blocks_login" {
+ type = bool
+ description = "Whether to block login until restore completes."
+ default = true
+}
+
+variable "custom_stop_script" {
+ type = string
+ description = "Custom script to run before stop backup."
+ default = ""
+}
+
+variable "retention_keep_last" {
+ type = number
+ description = "Keep last N snapshots per workspace."
+ default = 10
+}
+
+variable "retention_keep_daily" {
+ type = number
+ description = "Keep daily snapshots for N days."
+ default = 14
+}
+
+variable "retention_keep_weekly" {
+ type = number
+ description = "Keep weekly snapshots for N weeks."
+ default = 8
+}
+
+variable "retention_keep_monthly" {
+ type = number
+ description = "Keep monthly snapshots for N months."
+ default = 6
+}
+
+variable "auto_forget" {
+ type = bool
+ description = "Apply retention policies automatically after backup."
+ default = false
+}
+
+variable "auto_prune" {
+ type = bool
+ description = "Run prune after forget to reclaim space (slower but frees storage)."
+ default = false
+}
+
+variable "auto_init_repo" {
+ type = bool
+ description = "Automatically initialize repository if it doesn't exist."
+ default = true
+}
+
+variable "env" {
+ type = map(string)
+ description = "Environment variables for backend configuration (e.g., AWS_ACCESS_KEY_ID, B2_ACCOUNT_KEY). See README for backend-specific examples."
+ default = {}
+ sensitive = true
+}
+
+variable "icon" {
+ type = string
+ description = "Icon to use for Restic apps."
+ default = "/icon/restic.svg"
+}
+
+variable "order" {
+ type = number
+ description = "Order of apps in UI."
+ default = null
+}
+
+variable "group" {
+ type = string
+ description = "Group name for apps."
+ default = null
+}
+
+resource "coder_env" "restic_repository" {
+ agent_id = var.agent_id
+ name = "RESTIC_REPOSITORY"
+ value = var.repository
+}
+
+resource "coder_env" "restic_password" {
+ agent_id = var.agent_id
+ name = "RESTIC_PASSWORD"
+ value = var.password
+}
+
+resource "coder_env" "backend_env" {
+ for_each = nonsensitive(var.env)
+ agent_id = var.agent_id
+ name = each.key
+ value = each.value
+}
+
+resource "coder_env" "workspace_owner" {
+ agent_id = var.agent_id
+ name = "RESTIC_WORKSPACE_OWNER"
+ value = data.coder_workspace_owner.me.name
+}
+
+resource "coder_env" "workspace_name" {
+ agent_id = var.agent_id
+ name = "RESTIC_WORKSPACE_NAME"
+ value = data.coder_workspace.me.name
+}
+
+resource "coder_env" "workspace_id" {
+ agent_id = var.agent_id
+ name = "RESTIC_WORKSPACE_ID"
+ value = data.coder_workspace.me.id
+}
+
+resource "coder_script" "install_and_restore" {
+ agent_id = var.agent_id
+ display_name = "Restic Setup"
+ icon = var.icon
+ run_on_start = true
+ start_blocks_login = var.restore_on_start && var.start_blocks_login
+
+ script = templatefile("${path.module}/scripts/run.sh", {
+ INSTALL_RESTIC = var.install_restic
+ RESTIC_VERSION = var.restic_version
+ AUTO_INIT = var.auto_init_repo
+ RESTORE_ON_START = var.restore_on_start
+ SNAPSHOT_ID = var.snapshot_id
+ RESTORE_TARGET = var.restore_target
+ BACKUP_INTERVAL = var.backup_interval_minutes
+ BACKUP_PATHS = jsonencode(var.backup_paths)
+ EXCLUDE_PATTERNS = jsonencode(var.exclude_patterns)
+ BACKUP_TAGS = jsonencode(var.backup_tags)
+ DIRECTORY = var.directory
+ RETENTION_LAST = var.retention_keep_last
+ RETENTION_DAILY = var.retention_keep_daily
+ RETENTION_WEEKLY = var.retention_keep_weekly
+ RETENTION_MONTHLY = var.retention_keep_monthly
+ AUTO_FORGET = var.auto_forget
+ AUTO_PRUNE = var.auto_prune
+ BACKUP_SCRIPT_B64 = base64encode(file("${path.module}/scripts/backup.sh"))
+ })
+}
+
+resource "coder_script" "stop_backup" {
+ count = var.backup_on_stop ? 1 : 0
+ agent_id = var.agent_id
+ display_name = "Restic Backup"
+ icon = var.icon
+ run_on_stop = true
+ start_blocks_login = false
+
+ script = <<-EOT
+ #!/usr/bin/env bash
+ set -euo pipefail
+
+ ${var.custom_stop_script}
+
+ "$CODER_SCRIPT_BIN_DIR/restic-backup" --tag "stop-backup"
+ EOT
+}
+
+resource "coder_app" "restic_backup" {
+ agent_id = var.agent_id
+ slug = "restic-backup"
+ display_name = "Backup Now"
+ icon = var.icon
+ order = var.order
+ group = var.group
+
+ command = "$CODER_SCRIPT_BIN_DIR/restic-backup --tag manual-backup"
+}
+
diff --git a/registry/coder/modules/restic/restic.tftest.hcl b/registry/coder/modules/restic/restic.tftest.hcl
new file mode 100644
index 000000000..823ad455b
--- /dev/null
+++ b/registry/coder/modules/restic/restic.tftest.hcl
@@ -0,0 +1,333 @@
+run "required_variables" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "s3:s3.amazonaws.com/test-bucket"
+ password = "test-password"
+ }
+}
+
+run "stop_backup_script_created_when_enabled" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "/tmp/restic-repo"
+ password = "test-password"
+ backup_on_stop = true
+ }
+
+ assert {
+ condition = coder_script.stop_backup[0].run_on_stop == true
+ error_message = "Stop backup script should have run_on_stop enabled"
+ }
+
+ assert {
+ condition = coder_script.stop_backup[0].agent_id == "test-agent"
+ error_message = "Stop backup script should use correct agent_id"
+ }
+}
+
+run "stop_backup_script_not_created_when_disabled" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "/tmp/restic-repo"
+ password = "test-password"
+ backup_on_stop = false
+ }
+
+ assert {
+ condition = length(coder_script.stop_backup) == 0
+ error_message = "Stop backup script should not be created when backup_on_stop is false"
+ }
+}
+
+run "restore_blocks_login_by_default" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "/tmp/restic-repo"
+ password = "test-password"
+ restore_on_start = true
+ }
+
+ assert {
+ condition = coder_script.install_and_restore.start_blocks_login == true
+ error_message = "Install script should block login when restore_on_start and start_blocks_login are true"
+ }
+}
+
+run "restore_does_not_block_login_when_disabled" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "/tmp/restic-repo"
+ password = "test-password"
+ restore_on_start = true
+ start_blocks_login = false
+ }
+
+ assert {
+ condition = coder_script.install_and_restore.start_blocks_login == false
+ error_message = "Install script should not block login when start_blocks_login is false"
+ }
+}
+
+run "workspace_metadata_env_vars_created" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "/tmp/restic-repo"
+ password = "test-password"
+ }
+
+ assert {
+ condition = coder_env.workspace_owner.name == "RESTIC_WORKSPACE_OWNER"
+ error_message = "Workspace owner env var should be RESTIC_WORKSPACE_OWNER"
+ }
+
+ assert {
+ condition = coder_env.workspace_name.name == "RESTIC_WORKSPACE_NAME"
+ error_message = "Workspace name env var should be RESTIC_WORKSPACE_NAME"
+ }
+
+ assert {
+ condition = coder_env.workspace_id.name == "RESTIC_WORKSPACE_ID"
+ error_message = "Workspace ID env var should be RESTIC_WORKSPACE_ID"
+ }
+}
+
+run "core_env_vars_created" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "s3:s3.amazonaws.com/bucket"
+ password = "secure-password"
+ }
+
+ assert {
+ condition = coder_env.restic_repository.name == "RESTIC_REPOSITORY"
+ error_message = "Repository env var should be RESTIC_REPOSITORY"
+ }
+
+ assert {
+ condition = coder_env.restic_repository.value == "s3:s3.amazonaws.com/bucket"
+ error_message = "Repository env var should match input"
+ }
+
+ assert {
+ condition = coder_env.restic_password.name == "RESTIC_PASSWORD"
+ error_message = "Password env var should be RESTIC_PASSWORD"
+ }
+}
+
+run "safe_retention_defaults" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "/tmp/restic-repo"
+ password = "test-password"
+ }
+
+ # Verify auto_forget is false by default (safe)
+ assert {
+ condition = var.auto_forget == false
+ error_message = "auto_forget should be false by default for safety"
+ }
+
+ # Verify reasonable retention defaults
+ assert {
+ condition = var.retention_keep_last == 10
+ error_message = "Default retention_keep_last should be 10"
+ }
+
+ assert {
+ condition = var.retention_keep_daily == 14
+ error_message = "Default retention_keep_daily should be 14"
+ }
+}
+
+run "manual_backup_app_created" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "/tmp/restic-repo"
+ password = "test-password"
+ }
+
+ assert {
+ condition = coder_app.restic_backup.slug == "restic-backup"
+ error_message = "Backup app should have slug restic-backup"
+ }
+
+ assert {
+ condition = coder_app.restic_backup.display_name == "Backup Now"
+ error_message = "Backup app should display 'Backup Now'"
+ }
+
+ assert {
+ condition = can(regex("restic-backup", coder_app.restic_backup.command))
+ error_message = "Backup app command should call restic-backup helper"
+ }
+}
+
+run "install_restic_enabled_in_script" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "/tmp/restic-repo"
+ password = "test-password"
+ install_restic = true
+ }
+
+ assert {
+ condition = can(regex("INSTALL_RESTIC=\"true\"", coder_script.install_and_restore.script))
+ error_message = "Script should have INSTALL_RESTIC set to true"
+ }
+}
+
+run "install_restic_disabled_in_script" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "/tmp/restic-repo"
+ password = "test-password"
+ install_restic = false
+ }
+
+ assert {
+ condition = can(regex("INSTALL_RESTIC=\"false\"", coder_script.install_and_restore.script))
+ error_message = "Script should have INSTALL_RESTIC set to false"
+ }
+}
+
+run "auto_init_repo_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "/tmp/restic-repo"
+ password = "test-password"
+ auto_init_repo = false
+ }
+
+ assert {
+ condition = can(regex("AUTO_INIT=\"false\"", coder_script.install_and_restore.script))
+ error_message = "Script should have AUTO_INIT set to false"
+ }
+}
+
+run "restore_on_start_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "/tmp/restic-repo"
+ password = "test-password"
+ restore_on_start = true
+ snapshot_id = "abc123"
+ }
+
+ assert {
+ condition = can(regex("RESTORE_ON_START=\"true\"", coder_script.install_and_restore.script))
+ error_message = "Script should have RESTORE_ON_START set to true"
+ }
+
+ assert {
+ condition = can(regex("SNAPSHOT_ID=\"abc123\"", coder_script.install_and_restore.script))
+ error_message = "Script should have SNAPSHOT_ID set to abc123"
+ }
+}
+
+run "interval_backup_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "/tmp/restic-repo"
+ password = "test-password"
+ backup_interval_minutes = 30
+ }
+
+ assert {
+ condition = can(regex("BACKUP_INTERVAL=\"30\"", coder_script.install_and_restore.script))
+ error_message = "Script should have BACKUP_INTERVAL set to 30"
+ }
+}
+
+run "interval_backup_disabled_by_default" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "/tmp/restic-repo"
+ password = "test-password"
+ }
+
+ assert {
+ condition = can(regex("BACKUP_INTERVAL=\"0\"", coder_script.install_and_restore.script))
+ error_message = "Script should have BACKUP_INTERVAL set to 0 by default"
+ }
+}
+
+run "backup_paths_and_exclusions_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "/tmp/restic-repo"
+ password = "test-password"
+ backup_paths = ["/home/coder", "/workspace"]
+ exclude_patterns = ["*.log", "node_modules"]
+ backup_tags = ["production", "daily"]
+ }
+
+ assert {
+ condition = can(regex("/home/coder", coder_script.install_and_restore.script))
+ error_message = "Script should contain backup path /home/coder"
+ }
+
+ assert {
+ condition = can(regex("/workspace", coder_script.install_and_restore.script))
+ error_message = "Script should contain backup path /workspace"
+ }
+
+ assert {
+ condition = can(regex("\\*.log", coder_script.install_and_restore.script))
+ error_message = "Script should contain exclude pattern *.log"
+ }
+
+ assert {
+ condition = can(regex("production", coder_script.install_and_restore.script))
+ error_message = "Script should contain backup tag production"
+ }
+}
+
+run "custom_stop_script_included" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ repository = "/tmp/restic-repo"
+ password = "test-password"
+ backup_on_stop = true
+ custom_stop_script = "echo 'Pre-backup cleanup'"
+ }
+
+ assert {
+ condition = can(regex("echo 'Pre-backup cleanup'", coder_script.stop_backup[0].script))
+ error_message = "Stop script should contain custom stop script"
+ }
+}
+
diff --git a/registry/coder/modules/restic/scripts/backup.sh b/registry/coder/modules/restic/scripts/backup.sh
new file mode 100644
index 000000000..6326e574d
--- /dev/null
+++ b/registry/coder/modules/restic/scripts/backup.sh
@@ -0,0 +1,104 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+CONF_FILE="$CODER_SCRIPT_DATA_DIR/restic-backup.conf"
+if [ -f "$CONF_FILE" ]; then
+ # shellcheck source=/dev/null
+ source "$CONF_FILE"
+else
+ echo "Error: Configuration file not found: $CONF_FILE" >&2
+ exit 1
+fi
+
+EXTRA_TAGS=()
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --tag)
+ EXTRA_TAGS+=("$2")
+ shift 2
+ ;;
+ *)
+ echo "Unknown argument: $1" >&2
+ echo "Usage: restic-backup [--tag TAG]" >&2
+ exit 1
+ ;;
+ esac
+done
+
+echo "--------------------------------"
+echo "Restic Backup"
+echo "--------------------------------"
+
+DIRECTORY="${DIRECTORY/#\~/$HOME}"
+
+PATHS=$(echo "$BACKUP_PATHS" | python3 -c "import json, sys; print(' '.join(json.load(sys.stdin)))" 2> /dev/null || echo ".")
+EXCLUDES=$(echo "$EXCLUDE_PATTERNS" | python3 -c "import json, sys; [print(f'--exclude={p}') for p in json.load(sys.stdin)]" 2> /dev/null || echo "")
+TAGS=$(echo "$BACKUP_TAGS" | python3 -c "import json, sys; [print(f'--tag={t}') for t in json.load(sys.stdin)]" 2> /dev/null || echo "")
+
+TAG_ARGS=(
+ "--tag=workspace-id:$RESTIC_WORKSPACE_ID"
+ "--tag=workspace-owner:$RESTIC_WORKSPACE_OWNER"
+ "--tag=workspace-name:$RESTIC_WORKSPACE_NAME"
+)
+
+if [ -n "$TAGS" ]; then
+ while IFS= read -r tag; do
+ [ -n "$tag" ] && TAG_ARGS+=("$tag")
+ done <<< "$TAGS"
+fi
+
+for tag in "${EXTRA_TAGS[@]}"; do
+ TAG_ARGS+=("--tag=$tag")
+done
+
+EXCLUDE_ARGS=()
+if [ -n "$EXCLUDES" ]; then
+ while IFS= read -r exclude; do
+ [ -n "$exclude" ] && EXCLUDE_ARGS+=("$exclude")
+ done <<< "$EXCLUDES"
+fi
+
+cd "$DIRECTORY" || {
+ echo "Error: Failed to change to directory: $DIRECTORY" >&2
+ exit 1
+}
+
+echo "Working directory: $(pwd)"
+echo "Backup paths: $PATHS"
+echo "Tags: ${TAG_ARGS[*]}"
+[ ${#EXCLUDE_ARGS[@]} -gt 0 ] && echo "Exclusions: ${EXCLUDE_ARGS[*]}"
+echo "Starting backup..."
+
+# shellcheck disable=SC2086
+if restic backup $PATHS "${TAG_ARGS[@]}" "${EXCLUDE_ARGS[@]}"; then
+ echo "Backup completed successfully"
+else
+ echo "Error: Backup failed" >&2
+ exit 1
+fi
+
+if [ "$AUTO_FORGET" = "true" ]; then
+ echo "Applying retention policies..."
+
+ FORGET_ARGS=(
+ "--tag=workspace-id:$RESTIC_WORKSPACE_ID"
+ "--keep-last=$RETENTION_LAST"
+ )
+
+ [ "$RETENTION_DAILY" -gt 0 ] && FORGET_ARGS+=("--keep-daily=$RETENTION_DAILY")
+ [ "$RETENTION_WEEKLY" -gt 0 ] && FORGET_ARGS+=("--keep-weekly=$RETENTION_WEEKLY")
+ [ "$RETENTION_MONTHLY" -gt 0 ] && FORGET_ARGS+=("--keep-monthly=$RETENTION_MONTHLY")
+
+ if [ "$AUTO_PRUNE" = "true" ]; then
+ FORGET_ARGS+=("--prune")
+ echo "Pruning unreferenced data..."
+ fi
+
+ if restic forget "${FORGET_ARGS[@]}"; then
+ echo "Retention policies applied"
+ else
+ echo "Warning: Failed to apply retention policies" >&2
+ fi
+fi
+
+echo "Backup process complete"
diff --git a/registry/coder/modules/restic/scripts/run.sh b/registry/coder/modules/restic/scripts/run.sh
new file mode 100644
index 000000000..5a2b44376
--- /dev/null
+++ b/registry/coder/modules/restic/scripts/run.sh
@@ -0,0 +1,296 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+: $${CODER_SCRIPT_BIN_DIR:=$HOME/.local/bin}
+: $${CODER_SCRIPT_DATA_DIR:=$HOME/.local/share/coder}
+
+mkdir -p "$CODER_SCRIPT_BIN_DIR"
+mkdir -p "$CODER_SCRIPT_DATA_DIR"
+
+export PATH="$HOME/.local/bin:$PATH"
+INSTALL_RESTIC="${INSTALL_RESTIC}"
+RESTIC_VERSION="${RESTIC_VERSION}"
+AUTO_INIT="${AUTO_INIT}"
+RESTORE_ON_START="${RESTORE_ON_START}"
+SNAPSHOT_ID="${SNAPSHOT_ID}"
+RESTORE_TARGET="${RESTORE_TARGET}"
+BACKUP_INTERVAL="${BACKUP_INTERVAL}"
+BACKUP_PATHS='${BACKUP_PATHS}'
+EXCLUDE_PATTERNS='${EXCLUDE_PATTERNS}'
+BACKUP_TAGS='${BACKUP_TAGS}'
+DIRECTORY="${DIRECTORY}"
+RETENTION_LAST="${RETENTION_LAST}"
+RETENTION_DAILY="${RETENTION_DAILY}"
+RETENTION_WEEKLY="${RETENTION_WEEKLY}"
+RETENTION_MONTHLY="${RETENTION_MONTHLY}"
+AUTO_FORGET="${AUTO_FORGET}"
+AUTO_PRUNE="${AUTO_PRUNE}"
+BACKUP_SCRIPT_B64='${BACKUP_SCRIPT_B64}'
+
+echo "--------------------------------"
+echo "Restic Backup Module Setup"
+echo "--------------------------------"
+
+detect_os_arch() {
+ OS=$(uname -s | tr '[:upper:]' '[:lower:]')
+ ARCH=$(uname -m)
+
+ case "$ARCH" in
+ x86_64)
+ ARCH="amd64"
+ ;;
+ aarch64 | arm64)
+ ARCH="arm64"
+ ;;
+ armv7l)
+ ARCH="arm"
+ ;;
+ *)
+ echo "Unsupported architecture: $ARCH"
+ exit 1
+ ;;
+ esac
+
+ case "$OS" in
+ linux | darwin) ;;
+ *)
+ echo "Unsupported OS: $OS"
+ exit 1
+ ;;
+ esac
+
+ echo "Detected OS: $OS, Architecture: $ARCH"
+}
+
+install_restic() {
+ if [ "$INSTALL_RESTIC" != "true" ]; then
+ echo "Skipping Restic installation (install_restic=false)"
+ return
+ fi
+
+ if command -v restic > /dev/null 2>&1; then
+ INSTALLED_VERSION=$(restic version | head -n1 | awk '{print $2}')
+ echo "Restic already installed: $INSTALLED_VERSION"
+
+ if [ "$RESTIC_VERSION" != "latest" ] && [ "$INSTALLED_VERSION" != "$RESTIC_VERSION" ]; then
+ echo "Warning: Version mismatch (installed: $INSTALLED_VERSION, requested: $RESTIC_VERSION)"
+ fi
+ return
+ fi
+
+ echo "Installing Restic..."
+
+ detect_os_arch
+
+ if [ "$RESTIC_VERSION" = "latest" ]; then
+ echo "Fetching latest version..."
+ LATEST_VERSION=$(curl -fsSL https://api.github.com/repos/restic/restic/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
+
+ if [ -z "$LATEST_VERSION" ]; then
+ echo "Error: Failed to fetch latest version"
+ exit 1
+ fi
+
+ echo "Version: $LATEST_VERSION"
+ DOWNLOAD_URL="https://github.com/restic/restic/releases/download/v$${LATEST_VERSION}/restic_$${LATEST_VERSION}_$${OS}_$${ARCH}.bz2"
+ else
+ DOWNLOAD_URL="https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_$${OS}_$${ARCH}.bz2"
+ fi
+
+ echo "Downloading Restic..."
+
+ mkdir -p "$HOME/.local/bin"
+
+ TMP_FILE=$(mktemp)
+ if curl -fsSL "$DOWNLOAD_URL" -o "$TMP_FILE"; then
+ bunzip2 -c "$TMP_FILE" > "$HOME/.local/bin/restic"
+ chmod +x "$HOME/.local/bin/restic"
+ rm "$TMP_FILE"
+ echo "Restic installed: $($HOME/.local/bin/restic version)"
+ else
+ echo "Error: Download failed"
+ rm -f "$TMP_FILE"
+ exit 1
+ fi
+}
+
+verify_installation() {
+ if ! command -v restic > /dev/null 2>&1; then
+ echo "Error: restic command not found in PATH"
+ echo "PATH: $PATH"
+
+ if [ "$INSTALL_RESTIC" = "true" ]; then
+ exit 1
+ else
+ echo "Warning: restic not found but install_restic=false, continuing anyway"
+ return
+ fi
+ fi
+
+ echo "Restic verified: $(restic version | head -n1)"
+}
+
+init_repository() {
+ if [ "$AUTO_INIT" != "true" ]; then
+ echo "Skipping repository initialization (auto_init_repo=false)"
+ return
+ fi
+
+ echo "Checking repository..."
+
+ if restic snapshots > /dev/null 2>&1; then
+ echo "Repository already initialized"
+ return
+ fi
+
+ echo "Initializing repository..."
+ if restic init; then
+ echo "Repository initialized"
+ else
+ echo "Error: Failed to initialize repository"
+ exit 1
+ fi
+}
+
+install_backup_helper() {
+ echo "Installing backup helper script..."
+
+ HELPER_SCRIPT="$CODER_SCRIPT_BIN_DIR/restic-backup"
+
+ echo -n "$BACKUP_SCRIPT_B64" | base64 -d > "$HELPER_SCRIPT"
+ chmod +x "$HELPER_SCRIPT"
+
+ cat > "$CODER_SCRIPT_DATA_DIR/restic-backup.conf" << EOF
+BACKUP_PATHS='$BACKUP_PATHS'
+EXCLUDE_PATTERNS='$EXCLUDE_PATTERNS'
+BACKUP_TAGS='$BACKUP_TAGS'
+DIRECTORY='$DIRECTORY'
+RETENTION_LAST='$RETENTION_LAST'
+RETENTION_DAILY='$RETENTION_DAILY'
+RETENTION_WEEKLY='$RETENTION_WEEKLY'
+RETENTION_MONTHLY='$RETENTION_MONTHLY'
+AUTO_FORGET='$AUTO_FORGET'
+AUTO_PRUNE='$AUTO_PRUNE'
+EOF
+
+ if [ ! -x "$HELPER_SCRIPT" ]; then
+ echo "Error: Backup helper is not executable"
+ exit 1
+ fi
+
+ echo "Backup helper installed: $HELPER_SCRIPT"
+ echo "Backup helper verified as executable"
+}
+
+find_latest_snapshot() {
+ local TAG_FILTER="$1"
+
+ SNAPSHOTS_JSON=$(restic snapshots --tag "$TAG_FILTER" --json 2> /dev/null || echo "[]")
+
+ LATEST_SNAPSHOT=$(echo "$SNAPSHOTS_JSON" | python3 -c "
+import json, sys
+snapshots = json.load(sys.stdin)
+if snapshots:
+ latest = max(snapshots, key=lambda s: s['time'])
+ print(latest['short_id'])
+else:
+ print('')
+" 2> /dev/null || echo "")
+
+ echo "$LATEST_SNAPSHOT"
+}
+
+restore_on_start() {
+ if [ "$RESTORE_ON_START" != "true" ]; then
+ echo "Skipping restore (restore_on_start=false)"
+ return
+ fi
+
+ echo "--------------------------------"
+ echo "Restore Configuration"
+ echo "--------------------------------"
+
+ SNAPSHOT_TO_RESTORE=""
+
+ if [ -n "$SNAPSHOT_ID" ]; then
+ echo "Restoring specific snapshot: $SNAPSHOT_ID"
+ SNAPSHOT_TO_RESTORE="$SNAPSHOT_ID"
+ else
+ echo "Finding latest backup for this workspace..."
+ SNAPSHOT_TO_RESTORE=$(find_latest_snapshot "workspace-id:$RESTIC_WORKSPACE_ID")
+
+ if [ -z "$SNAPSHOT_TO_RESTORE" ]; then
+ echo "No previous backup found"
+ echo "Starting with fresh workspace"
+ return
+ fi
+
+ echo "Found snapshot: $SNAPSHOT_TO_RESTORE"
+ fi
+
+ echo "Restoring to $RESTORE_TARGET..."
+
+ if restic restore "$SNAPSHOT_TO_RESTORE" --target "$RESTORE_TARGET"; then
+ echo "Restore completed successfully"
+ else
+ echo "Error: Restore failed"
+ exit 1
+ fi
+}
+
+setup_interval_backup() {
+ if [ "$BACKUP_INTERVAL" -eq 0 ]; then
+ return
+ fi
+
+ echo "Setting up interval backup (every $BACKUP_INTERVAL minutes)..."
+
+ cat > "$CODER_SCRIPT_DATA_DIR/interval-backup.sh" << 'EOFSCRIPT'
+#!/usr/bin/env bash
+set -euo pipefail
+
+INTERVAL_MINUTES="$1"
+INTERVAL_SECONDS=$((INTERVAL_MINUTES * 60))
+
+echo "Starting interval backup loop (every $INTERVAL_MINUTES minutes)"
+
+while true; do
+ sleep "$INTERVAL_SECONDS"
+
+ echo "Running scheduled backup..."
+ if "$CODER_SCRIPT_BIN_DIR/restic-backup" --tag "interval-backup"; then
+ echo "Scheduled backup completed"
+ else
+ echo "Scheduled backup failed"
+ fi
+done
+EOFSCRIPT
+
+ chmod +x "$CODER_SCRIPT_DATA_DIR/interval-backup.sh"
+
+ nohup "$CODER_SCRIPT_DATA_DIR/interval-backup.sh" "$BACKUP_INTERVAL" \
+ >> "$CODER_SCRIPT_DATA_DIR/interval-backup.log" 2>&1 &
+
+ echo "Interval backup started in background (PID: $!)"
+}
+
+main() {
+ install_restic
+ verify_installation
+ init_repository
+ install_backup_helper
+ restore_on_start
+ setup_interval_backup
+
+ echo "--------------------------------"
+ echo "Restic setup complete"
+ echo "--------------------------------"
+ echo "Available commands:"
+ echo " restic-backup - Run manual backup"
+ echo " restic snapshots - List all snapshots"
+ echo " restic restore - Restore specific snapshot"
+ echo ""
+ echo "Repository: $${RESTIC_REPOSITORY:-not set}"
+}
+
+main