mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
docs: update README
This commit is contained in:
84
README.md
84
README.md
@@ -1,5 +1,83 @@
|
|||||||
# ironmount
|
<div align="center">
|
||||||
|
<h1>Ironmount</h1>
|
||||||
|
<h3>Keep your volumes in check!<br />One interface to manage all your storage</h3>
|
||||||
|
<a href="https://github.com/nicotsx/ironmount/blob/main/LICENSE">
|
||||||
|
<img src="https://img.shields.io/github/license/nicotsx/ironmount" />
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<figure>
|
||||||
|
<img src="https://github.com/nicotsx/ironmount/blob/main/screenshots/volume-details.png?raw=true" alt="Demo" />
|
||||||
|
<figcaption>
|
||||||
|
<p align="center">
|
||||||
|
Volume details view with usage statistics and health check status
|
||||||
|
</p>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
|
||||||
docker run --rm -it -v nicolas:/data alpine sh -lc 'echo hello > /data/hi && cat /data/hi'
|
<br />
|
||||||
|
|
||||||
mount -t davfs http://192.168.2.42 /mnt/webdav
|
## Intro
|
||||||
|
|
||||||
|
Ironmount is an easy to use web interface to manage your remote storage and mount them as local volumes on your server. Docker as a first class citizen, Ironmount allows you to easily mount your remote storage directly into your containers with few lines of code.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
https://github.com/nicotsx/ironmount/blob/main/screenshots/volume-creation.png?raw=true
|
||||||
|
|
||||||
|
- ✅ Support for multiple protocols: NFS, SMB, FTP, Directory
|
||||||
|
- 📡 Mount your remote storage as local folders
|
||||||
|
- 🐳 Docker integration: mount your remote storage directly into your containers via a docker volume syntax
|
||||||
|
- 🔍 Keep an eye on your mounts with health checks and automatic remounting on error
|
||||||
|
- 📊 Monitor your mounts usage with detailed statistics and graphs
|
||||||
|
|
||||||
|
### Coming soon
|
||||||
|
|
||||||
|
- 🔐 User authentication and role management
|
||||||
|
- 💾 Automated backups and snapshots with encryption, strategies and retention policies
|
||||||
|
- 🔄 Re-exporting your mounts to other protocols (e.g. mount an FTP server as an SMB share with fine-grained permissions)
|
||||||
|
- ☁️ Integration with cloud storage providers (e.g. AWS S3, Google Drive, Dropbox)
|
||||||
|
- 🔀 Storage sharding and replication for high availability and performance
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
In order to run Ironmount, you need to have Docker and Docker Compose installed on your server. Then, you can use the provided `docker-compose.yml` file to start the application.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
ironmount:
|
||||||
|
image: nicotsx/ironmount:v0.0.1
|
||||||
|
container_name: ironmount
|
||||||
|
restart: unless-stopped
|
||||||
|
cap_add:
|
||||||
|
- SYS_ADMIN
|
||||||
|
ports:
|
||||||
|
- "4096:4096"
|
||||||
|
devices:
|
||||||
|
- /dev/fuse:/dev/fuse
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- /run/docker/plugins:/run/docker/plugins
|
||||||
|
- /var/lib/docker/volumes/:/var/lib/docker/volumes:rshared
|
||||||
|
- ironmount_data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ironmount_data:
|
||||||
|
driver: local
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, run the following command to start Ironmount:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the container is running, you can access the web interface at `http://<your-server-ip>:4096`.
|
||||||
|
|
||||||
|
## Docker volume usage
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Volume creation
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -52,25 +52,20 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||||||
form.reset({ name: watchedName, ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType] });
|
form.reset({ name: watchedName, ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType] });
|
||||||
}, [watchedBackend, watchedName, form.reset]);
|
}, [watchedBackend, watchedName, form.reset]);
|
||||||
|
|
||||||
const [testStatus, setTestStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
|
||||||
const [testMessage, setTestMessage] = useState<string>("");
|
const [testMessage, setTestMessage] = useState<string>("");
|
||||||
|
|
||||||
const testBackendConnection = useMutation({
|
const testBackendConnection = useMutation({
|
||||||
...testConnectionMutation(),
|
...testConnectionMutation(),
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
setTestMessage("");
|
setTestMessage("");
|
||||||
setTestStatus("loading");
|
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
setTestStatus("error");
|
|
||||||
setTestMessage("Failed to test connection. Please try again.");
|
setTestMessage("Failed to test connection. Please try again.");
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data?.success) {
|
if (data?.success) {
|
||||||
setTestStatus("success");
|
|
||||||
setTestMessage(data.message);
|
setTestMessage(data.message);
|
||||||
} else {
|
} else {
|
||||||
setTestStatus("error");
|
|
||||||
setTestMessage(data?.message || "Connection test failed");
|
setTestMessage(data?.message || "Connection test failed");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -435,30 +430,24 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleTestConnection}
|
onClick={handleTestConnection}
|
||||||
disabled={
|
disabled={testBackendConnection.isPending}
|
||||||
testStatus === "loading" ||
|
|
||||||
!form.watch("server") ||
|
|
||||||
!form.watch("share") ||
|
|
||||||
!form.watch("username") ||
|
|
||||||
!form.watch("password")
|
|
||||||
}
|
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
{testStatus === "loading" && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{testBackendConnection.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
{testStatus === "success" && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
|
{testBackendConnection.isSuccess && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
|
||||||
{testStatus === "error" && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
|
{testBackendConnection.isError && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
|
||||||
{testStatus === "idle" && "Test Connection"}
|
{testBackendConnection.isIdle && "Test Connection"}
|
||||||
{testStatus === "loading" && "Testing..."}
|
{testBackendConnection.isPending && "Testing..."}
|
||||||
{testStatus === "success" && "Connection Successful"}
|
{testBackendConnection.isSuccess && "Connection Successful"}
|
||||||
{testStatus === "error" && "Test Failed"}
|
{testBackendConnection.isError && "Test Failed"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{testMessage && (
|
{testMessage && (
|
||||||
<div
|
<div
|
||||||
className={`text-sm p-2 rounded-md ${
|
className={`text-sm p-2 rounded-md ${
|
||||||
testStatus === "success"
|
testBackendConnection.isSuccess
|
||||||
? "bg-green-50 text-green-700 border border-green-200"
|
? "bg-green-50 text-green-700 border border-green-200"
|
||||||
: testStatus === "error"
|
: testBackendConnection.isError
|
||||||
? "bg-red-50 text-red-700 border border-red-200"
|
? "bg-red-50 text-red-700 border border-red-200"
|
||||||
: "bg-gray-50 text-gray-700 border border-gray-200"
|
: "bg-gray-50 text-gray-700 border border-gray-200"
|
||||||
}`}
|
}`}
|
||||||
@@ -476,24 +465,24 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleTestConnection}
|
onClick={handleTestConnection}
|
||||||
disabled={testStatus === "loading" || !form.watch("server") || !form.watch("exportPath")}
|
disabled={testBackendConnection.isPending}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
{testStatus === "loading" && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{testBackendConnection.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
{testStatus === "success" && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
|
{testBackendConnection.isSuccess && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
|
||||||
{testStatus === "error" && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
|
{testBackendConnection.isError && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
|
||||||
{testStatus === "idle" && "Test Connection"}
|
{testBackendConnection.isIdle && "Test Connection"}
|
||||||
{testStatus === "loading" && "Testing..."}
|
{testBackendConnection.isPending && "Testing..."}
|
||||||
{testStatus === "success" && "Connection Successful"}
|
{testBackendConnection.isSuccess && "Connection Successful"}
|
||||||
{testStatus === "error" && "Test Failed"}
|
{testBackendConnection.isError && "Test Failed"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{testMessage && (
|
{testMessage && (
|
||||||
<div
|
<div
|
||||||
className={`text-sm p-2 rounded-md ${
|
className={`text-sm p-2 rounded-md ${
|
||||||
testStatus === "success"
|
testBackendConnection.isSuccess
|
||||||
? "bg-green-50 text-green-700 border border-green-200"
|
? "bg-green-50 text-green-700 border border-green-200"
|
||||||
: testStatus === "error"
|
: testBackendConnection.isError
|
||||||
? "bg-red-50 text-red-700 border border-red-200"
|
? "bg-red-50 text-red-700 border border-red-200"
|
||||||
: "bg-gray-50 text-gray-700 border border-gray-200"
|
: "bg-gray-50 text-gray-700 border border-gray-200"
|
||||||
}`}
|
}`}
|
||||||
@@ -511,24 +500,24 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleTestConnection}
|
onClick={handleTestConnection}
|
||||||
disabled={testStatus === "loading" || !form.watch("server") || !form.watch("path")}
|
disabled={testBackendConnection.isPending}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
{testStatus === "loading" && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{testBackendConnection.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
{testStatus === "success" && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
|
{testBackendConnection.isSuccess && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
|
||||||
{testStatus === "error" && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
|
{testBackendConnection.isError && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
|
||||||
{testStatus === "idle" && "Test Connection"}
|
{testBackendConnection.isIdle && "Test Connection"}
|
||||||
{testStatus === "loading" && "Testing..."}
|
{testBackendConnection.isPending && "Testing..."}
|
||||||
{testStatus === "success" && "Connection Successful"}
|
{testBackendConnection.isSuccess && "Connection Successful"}
|
||||||
{testStatus === "error" && "Test Failed"}
|
{testBackendConnection.isError && "Test Failed"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{testMessage && (
|
{testMessage && (
|
||||||
<div
|
<div
|
||||||
className={`text-sm p-2 rounded-md ${
|
className={`text-sm p-2 rounded-md ${
|
||||||
testStatus === "success"
|
testBackendConnection.isSuccess
|
||||||
? "bg-green-50 text-green-700 border border-green-200"
|
? "bg-green-50 text-green-700 border border-green-200"
|
||||||
: testStatus === "error"
|
: testBackendConnection.isError
|
||||||
? "bg-red-50 text-red-700 border border-red-200"
|
? "bg-red-50 text-red-700 border border-red-200"
|
||||||
: "bg-gray-50 text-gray-700 border border-gray-200"
|
: "bg-gray-50 text-gray-700 border border-gray-200"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { MutationCache, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { MutationCache, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|
||||||
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
|
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
|
||||||
import { Toaster } from "~/components/ui/sonner";
|
import { Toaster } from "~/components/ui/sonner";
|
||||||
|
|
||||||
@@ -52,7 +51,6 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
<Scripts />
|
<Scripts />
|
||||||
</body>
|
</body>
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
4
notes.md
Normal file
4
notes.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
docker run --rm -it -v nicolas:/data alpine sh -lc 'echo hello > /data/hi && cat /data/hi'
|
||||||
|
|
||||||
|
mount -t davfs http://192.168.2.42 /mnt/webdav
|
||||||
|
|
||||||
BIN
screenshots/docker-instructions.png
Normal file
BIN
screenshots/docker-instructions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
screenshots/volume-creation.png
Normal file
BIN
screenshots/volume-creation.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
screenshots/volume-details.png
Normal file
BIN
screenshots/volume-details.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Reference in New Issue
Block a user