Skip to content

Development Guide

This guide covers setting up a local development environment for backupctl, including Docker networking for cross-container database access, the socat relay for Hetzner Storage Box connectivity, running tests, static analysis, and working with TypeORM migrations.


Prerequisites

RequirementNotes
Docker + Docker Compose v2Via Colima (macOS) or Docker Engine (Linux)
Node.js 20 LTSFor running tests and linting on the host (optional)
socatmacOS only — for Hetzner SSH relay (see Hetzner Relay)
GitFor version control

Quick Start

bash
# 1. Clone and enter the project
git clone <repo-url> backupctl && cd backupctl

# 2. Copy environment template and configure
cp .env.example .env
# Edit .env with your credentials

# 3. Start the dev environment
scripts/dev.sh up

# 4. Run health check
scripts/dev.sh cli health

# 5. Validate config
scripts/dev.sh cli config validate

Dev Script (scripts/dev.sh)

The scripts/dev.sh script is the single entry point for managing the dev environment.

bash
scripts/dev.sh help    # Show all available commands

Command Reference

CommandDescription
scripts/dev.sh upStart dev environment (build + hot reload)
scripts/dev.sh downStop dev environment
scripts/dev.sh restartRebuild and restart
scripts/dev.sh statusShow container status and health
scripts/dev.sh logs [db]Tail logs (all services, or just DB)
scripts/dev.sh shellOpen shell in dev container
scripts/dev.sh resetDestroy volumes and recreate (fresh DB)
scripts/dev.sh cli <cmd>Run backupctl CLI command
scripts/dev.sh test [watch|cov|e2e]Run tests
scripts/dev.sh lint [fix]Run linter
scripts/dev.sh analyze [target]Static analysis (dead-code, duplicates, strict, all)
scripts/dev.sh db:shellOpen psql shell to audit database
scripts/dev.sh migrate:runRun pending migrations
scripts/dev.sh migrate:revertRevert the last migration
scripts/dev.sh migrate:showShow migration status
scripts/dev.sh migrate:generate <Name>Generate migration from entity diff
scripts/dev.sh migrate:create <Name>Create an empty migration file

Docker Dev Environment

Architecture

The dev environment uses docker-compose.dev.yml with three containers:

ContainerImagePurposePort
backupctl-devDockerfile.devApp with hot reload3100
backupctl-audit-dbpostgres:16-alpineAudit database5432
backupctl-pgadmindpage/pgadmin4Database browser5050

The dev Dockerfile:

  • Installs all dependencies (including devDependencies)
  • Mounts src/ and test/ as volumes for instant code changes
  • Runs npm run start:dev (NestJS watch mode) for hot reload
  • Waits for the audit database to be healthy before starting

Starting the Environment

bash
scripts/dev.sh up

On startup, the script:

  1. Builds and starts all three containers
  2. Auto-connects project Docker networks (reads docker_network from config/projects.yml)
  3. Prints service URLs
  Services
  → App:      http://localhost:3100
  → pgAdmin:  http://localhost:5050
  → Audit DB: localhost:5432

pgAdmin

Access pgAdmin at http://localhost:5050 — no login required (server mode disabled). The audit database is pre-configured as a server connection.

Docker Network Auto-Connect

If a project's database runs in a separate Docker Compose stack (e.g., your application's own docker-compose.yml), backupctl needs to join that network to reach the database.

Add the docker_network field to your project in config/projects.yml:

yaml
projects:
  - name: my-project
    docker_network: myapp_default    # Docker network where the DB lives
    database:
      host: postgres                 # hostname on that network
      # ...

On scripts/dev.sh up or restart, the script automatically runs docker network connect for each project's network. If no docker_network is specified, the database is assumed reachable directly (host machine or already-connected network).

To see available Docker networks:

bash
docker network ls

Volume Mounts

Host PathContainer PathMode
./src/app/srcread-write (hot reload)
./test/app/testread-write
./config/app/configread-only
./ssh-keys/home/node/.sshread-only
./gpg-keys/app/gpg-keysread-only
./tmp/data/backupsread-write (backup data)

Environment Overrides

docker-compose.dev.yml overrides certain .env values for the dev environment:

yaml
environment:
  AUDIT_DB_HOST: backupctl-audit-db
  NODE_ENV: development
  # Hetzner relay (macOS only — see section below)
  HETZNER_SSH_HOST: host.docker.internal
  HETZNER_SSH_PORT: "2323"

These overrides only apply to the dev container. Production uses the values from .env directly.


Hetzner Storage Box Relay (macOS)

The Problem

On macOS, Docker runs inside a lightweight VM (Colima or Docker Desktop). The VM only has IPv4 outbound connectivity. Many ISPs block outbound TCP port 23 on IPv4 — and Hetzner Storage Boxes use port 23 for SSH/SFTP.

Your Mac connects to Hetzner over IPv6 (which bypasses the ISP block), but the Docker VM has no IPv6.

Symptoms:

  • scripts/dev.sh cli health shows SSH as failed
  • nc -z u547206.your-storagebox.de 23 works from the Mac terminal but fails from inside Docker
  • Restic commands hang or fail with "Connection refused"

The Solution: socat Relay

Run a socat process on the Mac host that bridges IPv4 traffic from Docker to Hetzner over IPv6:

Docker (IPv4) → host.docker.internal:2323 → socat (Mac) → IPv6 → Hetzner:23

Setup

1. Install socat (one-time):

bash
brew install socat

2. Start the relay:

bash
socat "TCP4-LISTEN:2323,fork,reuseaddr" "TCP6:[2a01:4f8:2b01:ac::2]:23" &

Replace 2a01:4f8:2b01:ac::2 with your storage box's IPv6 address. Find it with:

bash
dig AAAA u547206.your-storagebox.de +short

3. Verify:

bash
# From Docker — should succeed
docker exec backupctl-dev nc -z -w 3 host.docker.internal 2323

# Test SSH
docker exec backupctl-dev ssh -i /home/node/.ssh/id_ed25519 -p 2323 \
  -o StrictHostKeyChecking=no u547206@host.docker.internal ls

How It Works with docker-compose.dev.yml

The dev compose file already overrides the SSH host and port:

yaml
environment:
  HETZNER_SSH_HOST: host.docker.internal
  HETZNER_SSH_PORT: "2323"

And ssh-keys/config has an entry for the relay host:

Host host.docker.internal
    User u547206
    Port 2323
    IdentityFile /home/node/.ssh/id_ed25519
    StrictHostKeyChecking no

So once the socat relay is running, all backupctl commands (health, restic, run) work transparently.

Stopping the Relay

bash
pkill -f "socat.*2323"

Why This Isn't Needed on Linux

On Linux production servers, Docker shares the host's network stack. If the host has IPv6 or port 23 is not blocked, Docker containers can reach Hetzner directly. The socat relay is a macOS development-only workaround.


Running Tests

bash
scripts/dev.sh test           # All tests
scripts/dev.sh test watch     # Watch mode (interactive)
scripts/dev.sh test cov       # Coverage report
scripts/dev.sh test e2e       # Integration tests

On the Host

bash
npm test                      # All tests
npm test -- --watch           # Watch mode
npm run test:cov              # Coverage report
npm run test:e2e              # Integration tests

Test Structure

test/
├── unit/                     # Mocked dependencies
│   ├── domain/               # Domain models, policies
│   ├── application/          # Use cases, registries
│   ├── infrastructure/       # Adapters, CLI, scheduler
│   └── shared/               # Utilities
└── integration/              # Real DB, full flows
    ├── config/               # YAML + .env resolution
    ├── audit/                # TypeORM CRUD + migrations
    ├── flow/                 # Full backup pipeline
    └── cli/                  # End-to-end CLI

Static Analysis

All Checks at Once

bash
scripts/dev.sh analyze

Runs ESLint, dead code detection (knip), and code duplication (jscpd).

Individual Checks

bash
scripts/dev.sh analyze dead-code    # Find unused exports/files (knip)
scripts/dev.sh analyze duplicates   # Detect copy-paste code (jscpd)
scripts/dev.sh analyze strict       # Strict type-safety (eslint)
scripts/dev.sh lint                 # Standard ESLint check
scripts/dev.sh lint fix             # ESLint with autofix

CI Equivalents

These same checks run in GitHub Actions CI:

WorkflowScript
.github/workflows/quality-dead-code.ymlnpm run lint:dead-code (knip)
.github/workflows/quality-dry.ymlnpm run lint:duplicates (jscpd)
.github/workflows/quality-strict-ts.ymlnpm run lint:strict (eslint)

TypeORM Migrations

Check Status

bash
scripts/dev.sh migrate:show

Run Pending Migrations

bash
scripts/dev.sh migrate:run

Generate from Entity Changes

After modifying a *.record.ts file (TypeORM entity):

bash
scripts/dev.sh migrate:generate AddNewColumn

Review the generated file in src/db/migrations/ before applying.

Create Empty Migration

For manual schema changes:

bash
scripts/dev.sh migrate:create SeedInitialData

Revert Last Migration

bash
scripts/dev.sh migrate:revert

See Migrations for detailed patterns and best practices.


Local Development (Without Docker)

If you prefer running NestJS directly on your machine:

Prerequisites

  • Node.js 20 LTS
  • PostgreSQL 16 (for the audit database)
  • restic, gpg, openssh-client (for adapter tests)

Setup

bash
npm ci
cp .env.example .env
# Edit .env — set AUDIT_DB_HOST=localhost

Run

bash
npm run start:dev              # HTTP server with watch mode
npx ts-node src/cli.ts health  # CLI commands

Lint & Format

bash
npm run lint            # Lint + autofix
npm run lint:check      # Lint without fixing (CI mode)
npm run format          # Prettier format

Switching Between Dev and Production

Dev and prod containers share port 3100, container names, and volumes. Never run both simultaneously.

bash
# Switch from dev to prod
scripts/dev.sh down
docker compose up -d --build

# Switch from prod to dev
docker compose down
scripts/dev.sh up

Quick check — which is running?

bash
docker ps --format '{{.Names}}' | grep backupctl
# backupctl-dev  → dev environment
# backupctl      → production

Environment Variables for Development

Key variables in .env relevant for dev:

VariableDocker DevLocal DevDescription
AUDIT_DB_HOSTbackupctl-audit-db (override)localhostAudit DB host
AUDIT_DB_PORT54325432Audit DB port
APP_PORT31003100HTTP server port
LOG_LEVELinfodebugSet to debug for verbose output
BACKUP_BASE_DIR/data/backups (mapped to ./tmp)/data/backupsBackup data directory

CLI Shortcuts

Install backupctl-dev so you can run dev CLI commands from any directory:

bash
./scripts/install-cli.sh

Then use:

bash
backupctl-dev health                   # instead of: scripts/dev.sh cli health
backupctl-dev run myproject --dry-run  # instead of: scripts/dev.sh cli run myproject --dry-run
backupctl-dev config show myproject    # instead of: scripts/dev.sh cli config show myproject

See Installation → CLI Shortcuts for details.


Useful Dev Commands

bash
# Health check
scripts/dev.sh cli health         # or: backupctl-dev health

# Validate config
scripts/dev.sh cli config validate

# Show project config (secrets masked)
scripts/dev.sh cli config show myproject

# Dry-run a backup
scripts/dev.sh cli run myproject --dry-run

# Open psql to audit DB
scripts/dev.sh db:shell

# Shell into container
scripts/dev.sh shell

# Full static analysis
scripts/dev.sh analyze

What's Next

Released under the MIT License.