Compare commits

...

5 Commits

Author SHA1 Message Date
Nicolas Meienberger
b289920720 fix: timeout statfs 2025-11-08 16:52:58 +01:00
Nicolas Meienberger
ef5d95d347 refactor: consolidate page titles 2025-11-08 14:04:06 +01:00
Nicolas Meienberger
a1ef34118c chore: update README 2025-11-08 13:23:09 +01:00
Nicolas Meienberger
59433f3686 chore: update README with docker & host propagation instructions 2025-11-08 13:21:59 +01:00
Nicolas Meienberger
3debd80e15 chore: update readme with detailed instructions 2025-11-08 12:55:39 +01:00
17 changed files with 219 additions and 25 deletions

168
README.md
View File

@@ -20,17 +20,14 @@
## Intro ## Intro
Ironmount is a backup automation platform that helps you protect your data across multiple storage backends. Built on top of Restic, it provides an intuitive web interface to schedule, manage, and monitor encrypted backups of your remote storage. With support for Docker integration, Ironmount makes it easy to backup your container volumes automatically. Ironmount is a backup automation tool that helps you save your data across multiple storage backends. Built on top of Restic, it provides an modern web interface to schedule, manage, and monitor encrypted backups of your remote storage.
### Features ### Features
- 💾  **Automated backups** with encryption, compression and retention policies powered by Restic -   **Automated backups** with encryption, compression and retention policies powered by Restic
- 📅  **Flexible scheduling** using cron expressions for automated backup jobs -   **Flexible scheduling** For automated backup jobs with fine-grained retention policies
- 🔐  **End-to-end encryption** ensuring your data is always protected -   **End-to-end encryption** ensuring your data is always protected
- 📦  **Snapshot management** with retention policies to optimize storage usage -   **Multi-protocol support**: Backup from NFS, SMB, WebDAV, or local directories
- 📊  **Monitoring and statistics** to track backup health and storage usage
- ✅  **Multi-protocol support**: Backup from NFS, SMB, WebDAV, or local directories
- 🔍  **Health checks** and automatic recovery to ensure backup reliability
## Installation ## Installation
@@ -48,7 +45,7 @@ services:
devices: devices:
- /dev/fuse:/dev/fuse - /dev/fuse:/dev/fuse
volumes: volumes:
- /var/lib/ironmount/:/var/lib/ironmount/ - /var/lib/ironmount:/var/lib/ironmount
``` ```
Then, run the following command to start Ironmount: Then, run the following command to start Ironmount:
@@ -59,9 +56,158 @@ docker compose up -d
Once the container is running, you can access the web interface at `http://<your-server-ip>:4096`. Once the container is running, you can access the web interface at `http://<your-server-ip>:4096`.
## Backups ## Adding your first volume
![Preview](https://github.com/nicotsx/ironmount/blob/main/screenshots/backups.png?raw=true) Ironmount supports multiple volume backends including NFS, SMB, WebDAV, and local directories. A volume represents the source data you want to back up and monitor.
To add your first volume, navigate to the "Volumes" section in the web interface and click on "Create volume". Fill in the required details such as volume name, type, and connection settings.
If you want to track a local directory on the same server where Ironmount is running, you'll first need to mount that directory into the Ironmount container. You can do this by adding a volume mapping in your `docker-compose.yml` file. For example, to mount `/path/to/your/directory` from the host to `/mydata` in the container, you would add the following line under the `volumes` section:
```diff
services:
ironmount:
image: ghcr.io/nicotsx/ironmount:v0.5.0
container_name: ironmount
restart: unless-stopped
cap_add:
- SYS_ADMIN
ports:
- "4096:4096"
devices:
- /dev/fuse:/dev/fuse
volumes:
- /var/lib/ironmount:/var/lib/ironmount
+ - /path/to/your/directory:/mydata
```
After updating the `docker-compose.yml` file, restart the Ironmount container to apply the changes:
```bash
docker compose down
docker compose up -d
```
Now, when adding a new volume in the Ironmount web interface, you can select "Directory" as the volume type and search for your mounted path (e.g., `/mydata`) as the source path.
![Preview](https://github.com/nicotsx/ironmount/blob/main/screenshots/add-volume.png?raw=true)
## Creating a repository
A repository is where your backups will be securely stored encrypted. Ironmount currently supports S3-compatible storage backends and local directories for storing your backup repositories.
Repositories are optimized for storage efficiency and data integrity, leveraging Restic's deduplication and encryption features.
To create a repository, navigate to the "Repositories" section in the web interface and click on "Create repository". Fill in the required details such as repository name, type, and connection settings. If you choose a local directory as the repository type, your backups will be stored at `/var/lib/ironmount/repositories/<repository-name>`.
## Your first backup job
Once you have added a volume and created a repository, you can create your first backup job. A backup job defines the schedule and parameters for backing up a specific volume to a designated repository.
When creating a backup job, you can specify the following settings:
- **Schedule**: Define how often the backup should run (e.g., daily, weekly)
- **Retention Policy**: Set rules for how long backups should be retained (e.g., keep daily backups for 7 days, weekly backups for 4 weeks)
- **Paths**: Specify which files or directories to include in the backup
After configuring the backup job, save it and Ironmount will automatically execute the backup according to the defined schedule.
You can monitor the progress and status of your backup jobs in the "Backups" section of the web interface.
![Preview](https://github.com/nicotsx/ironmount/blob/main/screenshots/backups-list.png?raw=true)
## Restoring data
Ironmount allows you to easily restore your data from backups. To restore data, navigate to the "Backups" section and select the backup job from which you want to restore data. You can then choose a specific backup snapshot and select the files or directories you wish to restore. The data you select will be restored to their original location.
![Preview](https://github.com/nicotsx/ironmount/blob/main/screenshots/restoring.png?raw=true)
## Propagating mounts to host
Ironmount is capable of propagating mounted volumes from within the container to the host system. This is particularly useful when you want to access the mounted data directly from the host to use it with other applications or services.
In order to enable this feature, you need to run Ironmount with privileged mode and mount /proc from the host. Here is an example of how to set this up in your `docker-compose.yml` file:
```diff
services:
ironmount:
image: ghcr.io/nicotsx/ironmount:v0.5.0
container_name: ironmount
restart: unless-stopped
- cap_add:
- - SYS_ADMIN
+ privileged: true
ports:
- "4096:4096"
devices:
- /dev/fuse:/dev/fuse
volumes:
- /var/lib/ironmount:/var/lib/ironmount
+ - /proc:/host/proc
```
Restart the Ironmount container to apply the changes:
```bash
docker compose down
docker compose up -d
```
## Docker plugin
Ironmount can also be used as a Docker volume plugin, allowing you to mount your volumes directly into other Docker containers. This enables seamless integration with your containerized applications.
In order to enable this feature, you need to run Ironmount with privileged mode and mount several items from the host. Here is an example of how to set this up in your `docker-compose.yml` file:
```diff
services:
ironmount:
image: ghcr.io/nicotsx/ironmount:v0.5.0
container_name: ironmount
restart: unless-stopped
- cap_add:
- - SYS_ADMIN
+ privileged: true
ports:
- "4096:4096"
devices:
- /dev/fuse:/dev/fuse
volumes:
- /var/lib/ironmount:/var/lib/ironmount
+ - /proc:/host/proc
+ - /run/docker/plugins:/run/docker/plugins
+ - /var/run/docker.sock:/var/run/docker.sock
```
Restart the Ironmount container to apply the changes:
```bash
docker compose down
docker compose up -d
```
Your Ironmount volumes will now be available as Docker volumes that you can mount into other containers using the `--volume` flag:
```bash
docker run -v im-nfs:/path/in/container nginx:latest
```
Or using Docker Compose:
```yaml
services:
myservice:
image: nginx:latest
volumes:
- im-nfs:/path/in/container
volumes:
im-nfs:
external: true
```
The volume name format is `im-<volume-name>` where `<volume-name>` is the name you assigned to the volume in Ironmount. You can verify that the volume is available by running:
```bash
docker volume ls
```
## Third-Party Software ## Third-Party Software

View File

@@ -10,9 +10,20 @@ import { Button } from "~/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { authMiddleware } from "~/middleware/auth"; import { authMiddleware } from "~/middleware/auth";
import type { Route } from "./+types/login";
export const clientMiddleware = [authMiddleware]; export const clientMiddleware = [authMiddleware];
export function meta(_: Route.MetaArgs) {
return [
{ title: "Login" },
{
name: "description",
content: "Sign in to your Ironmount account.",
},
];
}
const loginSchema = type({ const loginSchema = type({
username: "2<=string<=50", username: "2<=string<=50",
password: "string>=1", password: "string>=1",

View File

@@ -10,9 +10,20 @@ import { Button } from "~/components/ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { authMiddleware } from "~/middleware/auth"; import { authMiddleware } from "~/middleware/auth";
import type { Route } from "./+types/onboarding";
export const clientMiddleware = [authMiddleware]; export const clientMiddleware = [authMiddleware];
export function meta(_: Route.MetaArgs) {
return [
{ title: "Onboarding" },
{
name: "description",
content: "Welcome to Ironmount. Create your admin account to get started.",
},
];
}
const onboardingSchema = type({ const onboardingSchema = type({
username: "2<=string<=50", username: "2<=string<=50",
password: "string>=8", password: "string>=8",

View File

@@ -19,6 +19,16 @@ import type { Route } from "./+types/backup-details";
import { SnapshotFileBrowser } from "../components/snapshot-file-browser"; import { SnapshotFileBrowser } from "../components/snapshot-file-browser";
import { SnapshotTimeline } from "../components/snapshot-timeline"; import { SnapshotTimeline } from "../components/snapshot-timeline";
export function meta(_: Route.MetaArgs) {
return [
{ title: "Backup Job Details" },
{
name: "description",
content: "View and manage backup job configuration, schedule, and snapshots.",
},
];
}
export const clientLoader = async ({ params }: Route.LoaderArgs) => { export const clientLoader = async ({ params }: Route.LoaderArgs) => {
const { data } = await getBackupSchedule({ path: { scheduleId: params.id } }); const { data } = await getBackupSchedule({ path: { scheduleId: params.id } });

View File

@@ -11,10 +11,10 @@ import type { Route } from "./+types/backups";
export function meta(_: Route.MetaArgs) { export function meta(_: Route.MetaArgs) {
return [ return [
{ title: "Ironmount" }, { title: "Backup Jobs" },
{ {
name: "description", name: "description",
content: "Create, manage, monitor, and automate your Docker volumes with ease.", content: "Automate volume backups with scheduled jobs and retention policies.",
}, },
]; ];
} }

View File

@@ -20,10 +20,10 @@ import { listRepositories, listVolumes } from "~/api-client";
export function meta(_: Route.MetaArgs) { export function meta(_: Route.MetaArgs) {
return [ return [
{ title: "Ironmount" }, { title: "Create Backup Job" },
{ {
name: "description", name: "description",
content: "Create, manage, monitor, and automate your Docker volumes with ease.", content: "Create a new automated backup job for your volumes.",
}, },
]; ];
} }

View File

@@ -17,10 +17,10 @@ import { EmptyState } from "~/components/empty-state";
export function meta(_: Route.MetaArgs) { export function meta(_: Route.MetaArgs) {
return [ return [
{ title: "Ironmount - Repositories" }, { title: "Repositories" },
{ {
name: "description", name: "description",
content: "Manage your backup repositories", content: "Manage your backup repositories with encryption and compression.",
}, },
]; ];
} }

View File

@@ -27,10 +27,10 @@ import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
export function meta({ params }: Route.MetaArgs) { export function meta({ params }: Route.MetaArgs) {
return [ return [
{ title: `Ironmount - ${params.name}` }, { title: params.name },
{ {
name: "description", name: "description",
content: "Manage your restic backup repositories with ease.", content: "View repository configuration, status, and snapshots.",
}, },
]; ];
} }

View File

@@ -7,6 +7,16 @@ import { SnapshotFileBrowser } from "~/modules/backups/components/snapshot-file-
import { getSnapshotDetails } from "~/api-client"; import { getSnapshotDetails } from "~/api-client";
import type { Route } from "./+types/snapshot-details"; import type { Route } from "./+types/snapshot-details";
export function meta({ params }: Route.MetaArgs) {
return [
{ title: `Snapshot ${params.snapshotId}` },
{
name: "description",
content: "Browse and restore files from a backup snapshot.",
},
];
}
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => { export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
const snapshot = await getSnapshotDetails({ path: { name: params.name, snapshotId: params.snapshotId } }); const snapshot = await getSnapshotDetails({ path: { name: params.name, snapshotId: params.snapshotId } });
if (snapshot.data) return snapshot.data; if (snapshot.data) return snapshot.data;

View File

@@ -13,7 +13,7 @@ import type { Route } from "./+types/settings";
export function meta(_: Route.MetaArgs) { export function meta(_: Route.MetaArgs) {
return [ return [
{ title: "Settings - Ironmount" }, { title: "Settings" },
{ {
name: "description", name: "description",
content: "Manage your account settings and preferences.", content: "Manage your account settings and preferences.",

View File

@@ -32,10 +32,10 @@ import { DockerTabContent } from "../tabs/docker";
export function meta({ params }: Route.MetaArgs) { export function meta({ params }: Route.MetaArgs) {
return [ return [
{ title: `Ironmount - ${params.name}` }, { title: params.name },
{ {
name: "description", name: "description",
content: "Create, manage, monitor, and automate your Docker volumes with ease.", content: "View and manage volume details, configuration, and files.",
}, },
]; ];
} }

View File

@@ -17,7 +17,7 @@ import type { Route } from "./+types/volumes";
export function meta(_: Route.MetaArgs) { export function meta(_: Route.MetaArgs) {
return [ return [
{ title: "Ironmount" }, { title: "Volumes" },
{ {
name: "description", name: "description",
content: "Create, manage, monitor, and automate your Docker volumes with ease.", content: "Create, manage, monitor, and automate your Docker volumes with ease.",

View File

@@ -7,10 +7,12 @@ import { eq } from "drizzle-orm";
import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced"; import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced";
import slugify from "slugify"; import slugify from "slugify";
import { getCapabilities } from "../../core/capabilities"; import { getCapabilities } from "../../core/capabilities";
import { OPERATION_TIMEOUT } from "../../core/constants";
import { db } from "../../db/db"; import { db } from "../../db/db";
import { volumesTable } from "../../db/schema"; import { volumesTable } from "../../db/schema";
import { toMessage } from "../../utils/errors"; import { toMessage } from "../../utils/errors";
import { getStatFs, type StatFs } from "../../utils/mountinfo"; import { getStatFs, type StatFs } from "../../utils/mountinfo";
import { withTimeout } from "../../utils/timeout";
import { createVolumeBackend } from "../backends/backend"; import { createVolumeBackend } from "../backends/backend";
import type { UpdateVolumeBody } from "./volume.dto"; import type { UpdateVolumeBody } from "./volume.dto";
import { getVolumePath } from "./helpers"; import { getVolumePath } from "./helpers";
@@ -128,7 +130,11 @@ const getVolume = async (name: string) => {
let statfs: Partial<StatFs> = {}; let statfs: Partial<StatFs> = {};
if (volume.status === "mounted") { if (volume.status === "mounted") {
statfs = await getStatFs(getVolumePath(volume)).catch(() => ({})); statfs = await withTimeout(getStatFs(getVolumePath(volume)), OPERATION_TIMEOUT, "getStatFs")
.catch((error) => {
logger.warn(`Failed to get statfs for volume ${name}: ${toMessage(error)}`);
return {};
});
} }
return { volume, statfs }; return { volume, statfs };

View File

@@ -15,7 +15,7 @@ services:
ports: ports:
- "4096:4097" - "4096:4097"
volumes: volumes:
- /var/lib/ironmount/:/var/lib/ironmount/ - /var/lib/ironmount:/var/lib/ironmount
- ./apps/client/app:/app/apps/client/app - ./apps/client/app:/app/apps/client/app
- ./apps/server/src:/app/apps/server/src - ./apps/server/src:/app/apps/server/src

BIN
screenshots/add-volume.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
screenshots/restoring.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB