✨feature: Add docker support
This commit is contained in:
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.git
|
||||||
|
.github
|
||||||
|
.ferrite
|
||||||
|
|
||||||
|
backend/target
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/dist
|
||||||
|
|
||||||
|
data
|
||||||
|
docs
|
||||||
|
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
FROM node:20-bookworm-slim AS frontend-builder
|
||||||
|
WORKDIR /src/frontend
|
||||||
|
|
||||||
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY frontend/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM rust:1.85-bookworm AS backend-builder
|
||||||
|
WORKDIR /src/backend
|
||||||
|
|
||||||
|
COPY backend/ ./
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates ripgrep \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=backend-builder /src/backend/target/release/ironpad /app/ironpad
|
||||||
|
COPY --from=frontend-builder /src/frontend/dist /app/static
|
||||||
|
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
ENV IRONPAD_HOST=0.0.0.0
|
||||||
|
ENV IRONPAD_PORT=3000
|
||||||
|
ENV IRONPAD_DISABLE_TRAY=1
|
||||||
|
ENV IRONPAD_DATA_DIR=/app/data
|
||||||
|
ENV RUST_LOG=info
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
VOLUME ["/app/data"]
|
||||||
|
|
||||||
|
CMD ["./ironpad"]
|
||||||
28
README.md
28
README.md
@@ -68,6 +68,31 @@ npm run dev
|
|||||||
|
|
||||||
Open http://localhost:5173 in your browser.
|
Open http://localhost:5173 in your browser.
|
||||||
|
|
||||||
|
### Option 3: Run with Docker (Centralized Server)
|
||||||
|
|
||||||
|
This runs Ironpad as a single container that serves both API and frontend on port `3000`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and start in the background
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open:
|
||||||
|
|
||||||
|
- `http://localhost:3000` from the same machine, or
|
||||||
|
- `http://<your-server-ip>:3000` from another device on your network.
|
||||||
|
|
||||||
|
Data persists in `./data` on the host via the compose volume mapping.
|
||||||
|
|
||||||
|
To stop:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||

|

|
||||||
@@ -121,6 +146,9 @@ Read about the method:
|
|||||||
|---------|---------|-------------|
|
|---------|---------|-------------|
|
||||||
| Data directory | `data/` next to executable | Override with `IRONPAD_DATA_DIR` env var |
|
| Data directory | `data/` next to executable | Override with `IRONPAD_DATA_DIR` env var |
|
||||||
| Backend port | 3000 (auto-increments to 3010) | Dynamic port selection |
|
| Backend port | 3000 (auto-increments to 3010) | Dynamic port selection |
|
||||||
|
| Backend host | `127.0.0.1` | Override with `IRONPAD_HOST` (use `0.0.0.0` for Docker/server access) |
|
||||||
|
| Fixed port | disabled | Set `IRONPAD_PORT` to force a specific port |
|
||||||
|
| Disable tray mode | `false` | Set `IRONPAD_DISABLE_TRAY=1` to run headless in production static mode |
|
||||||
| Auto-commit | Every 60 seconds | Git commits when changes exist |
|
| Auto-commit | Every 60 seconds | Git commits when changes exist |
|
||||||
| Auto-save | 1 second debounce | Frontend saves after typing stops |
|
| Auto-save | 1 second debounce | Frontend saves after typing stops |
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Hide console window on Windows in release builds (production mode)
|
// Hide console window on Windows in release builds (production mode)
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{routing::get, Router};
|
use axum::{routing::get, Router};
|
||||||
@@ -20,22 +20,58 @@ mod websocket;
|
|||||||
/// Find an available port and return the bound listener.
|
/// Find an available port and return the bound listener.
|
||||||
/// Avoids TOCTOU race by keeping the listener alive.
|
/// Avoids TOCTOU race by keeping the listener alive.
|
||||||
async fn find_available_port() -> (TcpListener, u16) {
|
async fn find_available_port() -> (TcpListener, u16) {
|
||||||
|
let host = std::env::var("IRONPAD_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||||
|
let bind_ip = match host.parse::<IpAddr>() {
|
||||||
|
Ok(ip) => ip,
|
||||||
|
Err(_) => {
|
||||||
|
warn!("Invalid IRONPAD_HOST '{}', falling back to 127.0.0.1", host);
|
||||||
|
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(port_str) = std::env::var("IRONPAD_PORT") {
|
||||||
|
match port_str.parse::<u16>() {
|
||||||
|
Ok(port) => {
|
||||||
|
let addr = SocketAddr::new(bind_ip, port);
|
||||||
|
let listener = TcpListener::bind(addr)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e| panic!("Failed to bind to {addr}: {e}"));
|
||||||
|
return (listener, port);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
warn!("Invalid IRONPAD_PORT '{}', falling back to 3000-3010", port_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for port in 3000..=3010 {
|
for port in 3000..=3010 {
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
let addr = SocketAddr::new(bind_ip, port);
|
||||||
if let Ok(listener) = TcpListener::bind(addr).await {
|
if let Ok(listener) = TcpListener::bind(addr).await {
|
||||||
return (listener, port);
|
return (listener, port);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
panic!("No available ports in range 3000–3010");
|
panic!("No available ports in range 3000-3010");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_flag(name: &str) -> bool {
|
||||||
|
std::env::var(name)
|
||||||
|
.map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
|
||||||
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tracing_subscriber::fmt().init();
|
tracing_subscriber::fmt().init();
|
||||||
config::init_data_dir();
|
config::init_data_dir();
|
||||||
|
|
||||||
if config::is_production() {
|
let production = config::is_production();
|
||||||
|
let disable_tray = env_flag("IRONPAD_DISABLE_TRAY");
|
||||||
|
|
||||||
|
if production && !disable_tray {
|
||||||
run_with_tray();
|
run_with_tray();
|
||||||
} else {
|
} else {
|
||||||
|
if production && disable_tray {
|
||||||
|
info!("Production static mode detected; running headless (IRONPAD_DISABLE_TRAY=1)");
|
||||||
|
}
|
||||||
// Development mode: normal tokio runtime, no tray
|
// Development mode: normal tokio runtime, no tray
|
||||||
let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
|
let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
|
||||||
rt.block_on(run_server(None));
|
rt.block_on(run_server(None));
|
||||||
@@ -130,7 +166,11 @@ async fn run_server(port_tx: Option<std::sync::mpsc::Sender<u16>>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
info!("Ironpad running on http://localhost:{port}");
|
let bound_addr = listener
|
||||||
|
.local_addr()
|
||||||
|
.map(|a| a.to_string())
|
||||||
|
.unwrap_or_else(|_| format!("127.0.0.1:{port}"));
|
||||||
|
info!("Ironpad running on http://{bound_addr}");
|
||||||
|
|
||||||
axum::serve(listener, app).await.expect("Server failed");
|
axum::serve(listener, app).await.expect("Server failed");
|
||||||
}
|
}
|
||||||
|
|||||||
21
docker-compose.yml
Normal file
21
docker-compose.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
services:
|
||||||
|
ironpad:
|
||||||
|
container_name: ironpad
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
|
||||||
|
environment:
|
||||||
|
IRONPAD_HOST: "0.0.0.0"
|
||||||
|
IRONPAD_PORT: "3000"
|
||||||
|
IRONPAD_DISABLE_TRAY: "1"
|
||||||
|
IRONPAD_DATA_DIR: "/app/data"
|
||||||
|
RUST_LOG: "info"
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
Reference in New Issue
Block a user