docs: update README

This commit is contained in:
Nicolas Meienberger
2025-10-01 21:34:36 +02:00
parent 85a7e12dd8
commit a511a9ebc1
7 changed files with 115 additions and 46 deletions

View File

@@ -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
- ✅&nbsp; Support for multiple protocols: NFS, SMB, FTP, Directory
- 📡&nbsp; Mount your remote storage as local folders
- 🐳&nbsp; Docker integration: mount your remote storage directly into your containers via a docker volume syntax
- 🔍&nbsp; Keep an eye on your mounts with health checks and automatic remounting on error
- 📊&nbsp; Monitor your mounts usage with detailed statistics and graphs
### Coming soon
- 🔐&nbsp; User authentication and role management
- 💾&nbsp; Automated backups and snapshots with encryption, strategies and retention policies
- 🔄&nbsp; Re-exporting your mounts to other protocols (e.g. mount an FTP server as an SMB share with fine-grained permissions)
- ☁️&nbsp; Integration with cloud storage providers (e.g. AWS S3, Google Drive, Dropbox)
- 🔀&nbsp; 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
![Preview](https://github.com/nicotsx/ironmount/blob/main/screenshots/docker-instructions.png?raw=true)
## Volume creation
![Preview](https://github.com/nicotsx/ironmount/blob/main/screenshots/volume-creation.png?raw=true)

View File

@@ -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"
}`} }`}

View File

@@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB