Docker-native backup orchestration for S3
Marina is a config-driven backup orchestrator that backs up Docker volumes and databases directly to any S3-compatible bucket, with optional end-to-end encryption. In addition, Marina supports custom Docker image backends for destinations S3 doesn't cover.
Warning
Marina is still beta software. While the core functionality is stable and basically production-ready, breaking changes may occur between releases. Migration paths are not guaranteed until version 1.0. Always review the CHANGELOG before upgrading.
- Config-driven: Define all backup targets in config.yml with YAML syntax
- S3-native: Backs up directly to any S3-compatible bucket (AWS, Hetzner, DigitalOcean Spaces, MinIO, ...) with no third-party backup tool involved
- Optional end-to-end encryption: Password-based encryption (age) applied before upload; omit it to store archives unencrypted
- Restore:
marinactllists and restores snapshots straight from the bucket - Custom backup backends: Use custom Docker images for destinations other than S3
- Database dumps: Native support for PostgreSQL, MySQL, MariaDB, MongoDB, and Redis with auto-detection
- Volume backups: Back up Docker volumes with optional container stop/start
- Runtime validation: Targets validated at backup time—missing containers/volumes are skipped with warnings
- Flexible scheduling: Per-instance cron schedules
- Retention policies: Configurable daily/weekly/monthly retention per instance
- Pre/post hooks: Execute commands before and after backups
- Web Interface: React-based dashboard for monitoring backup status and logs
- Peer Federation: Connect multiple Marina instances for unified monitoring across servers
- REST API: Query backup status, logs, and schedules programmatically
Create a config.yml file with your backup instances and targets:
instances:
- id: hetzner-s3
s3:
endpoint: fsn1.your-objectstorage.com # host[:port], no scheme
bucket: marina-backups # must already exist
accessKeyId: your-access-key
secretAccessKey: your-secret-key
encryption:
# Optional: omit this block to store archives unencrypted
password: your-encryption-password
schedule: "0 2 * * *" # Daily at 2 AM
retention: "7d:4w:6m" # 7 daily, 4 weekly, 6 monthly
# Optional: backup timeout (default: 60m)
timeout: "10m"
targets:
- volume: app-data
paths: ["/"] # Paths relative to volume root
stopAttached: false # Optional: stop containers during backup
- db: postgres # Container name
# dbKind auto-detected from container image
# dbKind: postgres # Override auto-detection if needed
# dumpArgs: ["--clean", "--if-exists"] # Optional dump arguments
beforeBackup: # Optional commands to run before backup starts
- container: app-postgres # run inside existing container
command: "psql -U myapp -c 'CHECKPOINT;'"
afterBackup: # Optional commands to run after backup completes
- image: ubuntu:latest # run in temporary container
command: "curl -X POST https://my.app/backup-complete"
# Optional global defaults
stopAttached: true # Stop containers when backing up volumes
timeout: "60m" # Global timeout for backend operations (default: 60m)
# Optional: Custom node name (defaults to hostname)
nodeName: ${NODE_NAME} # or "production-server"
# Optional: Authentication password for API/dashboard access
authPassword: ${MARINA_AUTH_PASSWORD} # Leave empty to disable auth
# Optional: Peer federation - connect multiple Marina instances
# When configured, the dashboard shows schedules from all connected nodes
peers:
- http://marina-node2:8080
- http://marina-node3:8080See config.example.yml for more examples, including MinIO and additional target options.
services:
# Marina backup orchestrator
marina:
image: ghcr.io/polarfoxdev/marina:latest
container_name: marina
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
# IMPORTANT: Must be a bind mount from the host, not a Docker volume
# Marina uses this to stage backup data and automatically detects
# the host path for creating temporary containers
- ./staging:/backup
- marina-data:/var/lib/marina
# Config file (defaults to /config.yml, override with CONFIG_FILE env var)
- ./config.yml:/config.yml:ro
ports:
- "8080:8080"
environment:
S3_ACCESS_KEY_ID: "${S3_ACCESS_KEY_ID}"
S3_SECRET_ACCESS_KEY: "${S3_SECRET_ACCESS_KEY}"
MARINA_ENC_PASSWORD: "${MARINA_ENC_PASSWORD}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
# Example: PostgreSQL database (container name must match config.yml)
postgres:
image: postgres:16-alpine
container_name: postgres # Referenced in config.yml targets
environment:
POSTGRES_DB: myapp
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
PGPASSWORD: "${POSTGRES_PASSWORD}"
volumes:
- postgres-data:/var/lib/postgresql/data
# Example: Application with volumes (volume name must match config.yml)
app:
image: myapp:latest
volumes:
- app-data:/app/data # Referenced in config.yml targets
volumes:
marina-data:
postgres-data:
app-data:Note: Container and volume names must match those specified in your config.yml targets.
docker-compose up -dMarina will schedule backups for all configured targets. Missing containers or volumes are skipped with warnings at backup time.
Access the web interface:
# Open in browser
open http://localhost:8080Or check the logs:
docker-compose logs -f marinaOr query the API directly:
# Health check
curl http://localhost:8080/api/health
# Get all schedules (includes mesh peers if configured)
curl http://localhost:8080/api/schedules | jq
# Get backup status for an instance
curl http://localhost:8080/api/status/local-backup | jq
# Get logs for a specific job
curl http://localhost:8080/api/logs/job/1 | jqMarina uses a single configuration file to define backup instances and their targets. By default, Marina looks for the config at /config.yml.
You can override this by setting the CONFIG_FILE environment variable to a different path.
Example: Mount your config file to /config.yml in the container:
volumes:
- ./config.yml:/config.yml:roOr use a custom path:
volumes:
- ./my-config.yml:/app/config.yml:ro
environment:
CONFIG_FILE: /app/config.ymlEach backup instance can have multiple targets configured using YAML object syntax:
targets:
- volume: app-data
paths: ["/"] # Optional: paths relative to volume root (default: ["/"])
stopAttached: false # Optional: stop containers during backup (default: false)
- db: postgres # Container name
dbKind: postgres # Optional: auto-detected if not specified
dumpArgs: ["--clean"] # Optional: additional dump arguments| Field | Required | Description | Example |
|---|---|---|---|
volume |
Yes | Volume name (as shown in docker volume ls) |
"app-data" |
paths |
No | Paths to backup (relative to volume root) | ["/", "/data"] |
stopAttached |
No | Stop attached containers during backup | true |
| Field | Required | Description | Example |
|---|---|---|---|
db |
Yes | Container name (as shown in docker ps) |
"postgres", "my-mysql" |
dbKind |
No* | Database type (auto-detected if not provided) | "postgres", "mysql", "mariadb", "mongo", "redis" |
dumpArgs |
No | Additional arguments for dump command | ["--clean", "--if-exists"] (PostgreSQL) |
*dbKind auto-detection: Marina automatically detects the database type from the container image name (e.g., postgres:16 → postgres).
You can override this by explicitly specifying dbKind. If detection fails and no dbKind is provided, the target will be skipped.
Important for MySQL/MariaDB: Pass credentials via dumpArgs using ["-uroot", "-pPASSWORD"] format. Do not set MYSQL_PWD
environment variable as it interferes with container initialization.
Note: Marina automatically generates a single tag for each backup in the format
type:name(e.g.,volume:mydatafor volume backups ordb:postgresfor database backups).
Marina uses a single configuration file (config.yml) that defines:
- Backup instances: S3 destinations (or custom images), credentials, schedules, retention policies, and targets
- Backup targets: Volumes and databases to back up, defined within each instance
- Global defaults: Optional default values for all instances
- Mesh networking: Optional multi-node federation for unified monitoring
All backup targets are defined in the config file. At backup time, Marina validates that the referenced volumes and containers exist. Missing targets are skipped with warnings in the logs.
Marina requires /backup to be mounted as a host bind mount (not a Docker volume). This directory is used for:
- Volume backups: Staging data from Docker volumes before sending to backup destination
- Database dumps: Temporarily storing database dumps before backup
- Custom backends: Providing backup data to custom Docker image backends
Marina automatically detects the actual host path where /backup is mounted by inspecting its own container.
This host path is then used to create bind mounts in temporary containers for:
- Volume copy operations (temporary Alpine containers)
- Custom image backend containers (scoped to
/backup/{instanceID})
Example mounting options:
volumes:
- ./staging:/backup # Relative path
- /var/lib/marina/staging:/backup # Absolute path
- $HOME/marina-staging:/backup # With environment variableNote: Each custom backend container only sees its own instance's data at /backup/{instanceID} for security and isolation.
Marina writes its own archive format directly to the bucket — see Archive Format for the exact layout. There is no
external backup tool involved, so restoring is done with marinactl, a small CLI built into the Marina image:
# List complete snapshots for an instance (decrypts manifests if a password is available)
docker exec marina marinactl list hetzner-s3
# Restore the latest snapshot
docker exec marina marinactl restore hetzner-s3 latest --out /tmp/restore
# Restore a specific snapshot, or just one target from it
docker exec marina marinactl restore hetzner-s3 20260823-020000 --out /tmp/restore --target volume:app-uploadsmarinactl reads S3 credentials and the encryption password from the same config file Marina itself uses (--config, default /config.yml);
--password or MARINA_ENC_PASSWORD override the password for restoring on a machine without that config. Every restored file's checksum is
verified against the manifest as it streams, so a truncated or corrupted object is caught immediately rather than silently restored.
If encryption is enabled, an archive can also be recovered without Marina at all, using the standalone age
CLI plus zstd and tar:
age -d -i key.txt backup.tar.zst.age | zstd -d | tar -x # with an age identity file
age -d backup.tar.zst.age | zstd -d | tar -x # with a password, interactivelyIn addition to S3, Marina supports custom Docker image backends that allow you to implement your own backup logic. This is useful for:
- Backing up to a destination other than S3 (e.g. SFTP, another cloud provider)
- Implementing custom backup formats or compression
- Integrating with proprietary backup systems
- Custom data transformation before backup
Configuration example:
instances:
- id: custom-s3
customImage: your-registry/your-backup-image:latest
schedule: "0 3 * * *"
env:
BACKUP_ENDPOINT: https://backup.example.com
BACKUP_TOKEN: ${BACKUP_TOKEN}How it works:
- Marina stages backup data in
/backup/{instanceID}on the host - Marina creates a container from your custom image
- Only that instance's subfolder is mounted at
/backupin the container (scoped access) - Your container's
/backup.shscript executes with access to the staged data - Marina captures the exit code (0 = success, non-zero = failure) and logs
Your custom image must:
- Have a
/backup.shscript (or configure a different entrypoint) - Read backup data from
/backupdirectory - Exit with code 0 on success, non-zero on failure
- Handle its own retention policy (Marina's retention config is informational only)
See the custom backup image example for a complete working example and custom backends documentation for detailed implementation guide.
See config.example.yml for a complete configuration example including mesh mode setup and docker-compose.example.yml for a full deployment example.
- Archive Format - Object layout, manifest schema, and manual recovery without Marina
- Custom Backends - Build your own backup backend using Docker images
- Architecture - System design and data flow
- Web Interface - React dashboard and mesh mode
- Logging - Job logging and status tracking
- Docker 20.10 or later
- Docker Compose v2 (optional, for easier deployment)
- An S3-compatible bucket (AWS, Hetzner, DigitalOcean Spaces, MinIO, ...) that already exists
# Clone the repository
git clone https://github.com/polarfoxDev/marina.git
cd marina
# Build the binaries
go build -o marina ./cmd/manager
go build -o marina-api ./cmd/api
go build -o marinactl ./cmd/marinactl
# Or build the Docker image
docker build -t marina:dev .go test ./...This project is open source. See the repository for license details.
Contributions are welcome! Please open an issue or pull request on GitHub.