Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Dockerfile.langflow
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ FROM langflowai/langflow:1.8.0
RUN pip install uv \
&& mkdir -p /app/langflow-data

# Copy the entrypoint before switching to root so the COPY runs as uid=1000
# (which owns /app), then switch to root so the entrypoint can correct
# bind-mount permissions at runtime before dropping back to uid=1000.
COPY --chmod=755 scripts/langflow-entrypoint.py /usr/local/bin/langflow-entrypoint
USER root

EXPOSE 7860

ENTRYPOINT ["langflow-entrypoint"]
CMD ["langflow", "run", "--host", "0.0.0.0", "--port", "7860"]
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ help_utils: ## Show utility commands

ensure-langflow-data: ## Create the langflow-data directory if it does not exist
@mkdir -p langflow-data
@chmod 777 langflow-data

dev: ensure-langflow-data ## Start full stack with GPU support
@echo "$(YELLOW)Starting OpenRAG with GPU support...$(NC)"
Expand Down
43 changes: 43 additions & 0 deletions scripts/langflow-entrypoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Entrypoint for the OpenRAG Langflow container.

Runs as root to correct /app/langflow-data bind-mount permissions, then drops
to uid/gid 1000 (langflow user) before exec-ing the main process.

On macOS with Podman the virtiofs layer does not faithfully propagate
host-side chmod into the container, so permissions must be fixed from
inside the container after the mount is established.
"""
import os
import pathlib
import pwd
import sys

data_dir = pathlib.Path("/app/langflow-data")

try:
data_dir.chmod(0o777)
except OSError:
pass

# Look up uid 1000's passwd entry so we can restore HOME and USER correctly
# after dropping privileges. Running as root (USER root in Dockerfile) sets
# HOME=/root; leaving it unchanged causes uv to try /root/.cache/uv, which
# uid 1000 cannot write to.
try:
pw = pwd.getpwuid(1000)
home = pw.pw_dir
user = pw.pw_name
except KeyError:
home = "/app"
user = "langflow"

# Drop from root to langflow (uid=1000, gid=1000).
os.setgid(1000)
os.setuid(1000)

# Restore environment variables to reflect the unprivileged user.
os.environ["HOME"] = home
os.environ["USER"] = user

os.execvp(sys.argv[1], sys.argv[1:])
113 changes: 83 additions & 30 deletions src/tui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,25 +64,25 @@ class OpenRAGTUI(App):
min-width: 20;
border: solid #3f3f46;
}

#config-header {
text-align: center;
margin-bottom: 2;
}

#config-scroll {
height: 1fr;
overflow-y: auto;
}

#config-form {
width: 80%;
max-width: 100;
margin: 0;
padding: 1;
height: auto;
}

#config-form Input {
margin-bottom: 1;
width: 100%;
Expand All @@ -99,7 +99,7 @@ class OpenRAGTUI(App):
width: auto;
min-width: 12;
}

#config-form Label {
margin-bottom: 0;
padding-left: 1;
Expand Down Expand Up @@ -145,18 +145,18 @@ class OpenRAGTUI(App):
}

/* Docs path actions row */

#services-content {
height: 100%;
}

#runtime-status {
background: $panel;
border: solid $primary;
padding: 1;
margin-bottom: 1;
}

#services-table {
height: auto;
max-height: 12;
Expand All @@ -169,52 +169,52 @@ class OpenRAGTUI(App):
margin-bottom: 1;
}



#logs-scroll {
height: 1fr;
border: solid $primary;
background: $surface;
}

.controls-row {
align: left middle;
height: auto;
margin: 1 0;
}

.controls-row > * {
margin-right: 1;
}

.label {
width: auto;
margin-right: 1;
text-style: bold;
}

#system-info {
background: $panel;
border: solid $primary;
padding: 2;
height: 1fr;
}

TabbedContent {
height: 1fr;
}

TabPane {
padding: 1;
height: 1fr;
}

.tab-header {
text-style: bold;
color: $accent;
margin-bottom: 1;
}

TabPane ScrollableContainer {
height: 100%;
padding: 1;
Expand Down Expand Up @@ -367,7 +367,7 @@ def __init__(self):
self.container_manager = ContainerManager()
self.env_manager = EnvManager()
self.docling_manager = DoclingManager() # Initialize singleton instance

def notify(
self,
message: str,
Expand Down Expand Up @@ -457,17 +457,17 @@ def _copy_assets(resource_tree, destination: Path, allowed_suffixes: Optional[It

def copy_sample_documents(*, force: bool = False) -> None:
"""Copy sample documents from package to host directory.

Uses the first path from OPENRAG_DOCUMENTS_PATHS env var.
Defaults to ~/.openrag/documents if not configured.
"""
from .managers.env_manager import EnvManager
from pathlib import Path

# Get the configured documents path from env
env_manager = EnvManager()
env_manager.load_existing_env()

# Parse the first path from the documents paths config
documents_path_str = env_manager.config.openrag_documents_paths
if documents_path_str:
Expand All @@ -478,7 +478,7 @@ def copy_sample_documents(*, force: bool = False) -> None:
else:
# Default fallback
documents_dir = Path.home() / ".openrag" / "documents"

documents_dir.mkdir(parents=True, exist_ok=True)

try:
Expand All @@ -504,11 +504,11 @@ def copy_sample_documents(*, force: bool = False) -> None:

def copy_sample_flows(*, force: bool = False) -> None:
"""Copy sample flows from package to host directory.

Flows are placed in ~/.openrag/flows/ which will be volume-mounted to containers.
"""
from pathlib import Path

# Flows always go to ~/.openrag/flows/ - this will be volume-mounted
flows_dir = Path.home() / ".openrag" / "flows"
flows_dir.mkdir(parents=True, exist_ok=True)
Expand All @@ -524,7 +524,7 @@ def copy_sample_flows(*, force: bool = False) -> None:
def copy_compose_files(*, force: bool = False) -> None:
"""Copy docker-compose templates into the TUI workspace if they are missing."""
from utils.paths import get_tui_compose_file

try:
assets_root = files("tui._assets")
except Exception as e:
Expand All @@ -534,7 +534,7 @@ def copy_compose_files(*, force: bool = False) -> None:
for filename in ("docker-compose.yml", "docker-compose.gpu.yml"):
is_gpu = "gpu" in filename
destination = get_tui_compose_file(gpu=is_gpu)

if destination.exists() and not force:
continue

Expand Down Expand Up @@ -562,7 +562,7 @@ def migrate_legacy_data_directories():
"""Migrate data from CWD-based directories to ~/.openrag/.

This is a one-time migration for users upgrading from the old layout.
Migrates: documents, flows, keys, config, opensearch-data
Migrates: documents, flows, keys, config, opensearch-data, langflow-data

Prompts user for confirmation before migrating. If user declines,
exits with a message to downgrade to v1.52 or earlier.
Expand All @@ -585,6 +585,7 @@ def migrate_legacy_data_directories():
(cwd / "keys", target_base / "keys", "keys"),
(cwd / "config", target_base / "config", "config"),
(cwd / "opensearch-data", target_base / "data" / "opensearch-data", "OpenSearch data"),
(cwd / "langflow-data", target_base / "data" / "langflow-data", "Langflow data"),
]

# Check which sources exist and need migration
Expand All @@ -608,7 +609,8 @@ def migrate_legacy_data_directories():
env_manager.config.openrag_config_path = f"{home}/.openrag/config"
env_manager.config.openrag_data_path = f"{home}/.openrag/data"
env_manager.config.opensearch_data_path = f"{home}/.openrag/data/opensearch-data"
env_manager.save_env()
env_manager.config.langflow_data_path = f"{home}/.openrag/data/langflow-data"
env_manager.save_env_file()
logger.info("Updated .env file with centralized paths")
except Exception as e:
logger.warning(f"Failed to update .env paths: {e}")
Expand Down Expand Up @@ -687,7 +689,8 @@ def migrate_legacy_data_directories():
env_manager.config.openrag_config_path = f"{home}/.openrag/config"
env_manager.config.openrag_data_path = f"{home}/.openrag/data"
env_manager.config.opensearch_data_path = f"{home}/.openrag/data/opensearch-data"
env_manager.save_env()
env_manager.config.langflow_data_path = f"{home}/.openrag/data/langflow-data"
env_manager.save_env_file()
print(" Updated .env with centralized paths")
logger.info("Updated .env file with centralized paths")
except Exception as e:
Expand Down Expand Up @@ -753,6 +756,7 @@ def setup_host_directories():
- ~/.openrag/config/ (for configuration)
- ~/.openrag/data/ (for backend data: conversations, OAuth tokens, etc.)
- ~/.openrag/data/opensearch-data/ (for OpenSearch index)
- LANGFLOW_DATA_PATH (for Langflow database and state)
"""
base_dir = Path.home() / ".openrag"
directories = [
Expand All @@ -768,10 +772,59 @@ def setup_host_directories():
directory.mkdir(parents=True, exist_ok=True)
logger.debug(f"Ensured directory exists: {directory}")

# Resolve the configured LANGFLOW_DATA_PATH so we pre-create the exact
# directory that Docker/Podman will mount, regardless of user customisation.
langflow_data_dir = _resolve_langflow_data_path(base_dir)
langflow_data_dir.mkdir(parents=True, exist_ok=True)
logger.debug(f"Ensured directory exists: {langflow_data_dir}")

# langflow-data must be world-writable so the Langflow container user (uid 1000)
# can write into it on macOS where Podman's :U uid-remapping does not reliably
# update host directory ownership through the VM layer.
os.chmod(langflow_data_dir, 0o777)

# Generate JWT keys on host to avoid container permission issues
generate_jwt_keys(base_dir / "keys")


def _resolve_langflow_data_path(base_dir: Path) -> Path:
"""Return the absolute path for the Langflow data directory.

Reads LANGFLOW_DATA_PATH from the TUI .env file when available; falls back
to the default location (~/.openrag/data/langflow-data).

Relative paths are not valid in the TUI context because the process working
directory is unpredictable. If a relative path is found the default is used
and a warning is logged so the user can correct their configuration.
"""
default = base_dir / "data" / "langflow-data"
try:
from .managers.env_manager import EnvManager
env_manager = EnvManager()
env_manager.load_existing_env()
raw = env_manager.config.langflow_data_path
if not raw:
return default
expanded = raw.replace("$HOME", str(Path.home()))
resolved = Path(expanded).expanduser()
if not resolved.is_absolute():
logger.warning(
f"LANGFLOW_DATA_PATH='{raw}' is a relative path, which is not supported "
f"in the TUI. Resetting to default: {default}"
)
try:
env_manager.config.langflow_data_path = str(default)
env_manager.save_env_file()
logger.info(f"Updated LANGFLOW_DATA_PATH to {default} in {env_manager.env_file}")
except Exception as write_err:
logger.error(f"Could not update LANGFLOW_DATA_PATH in env file: {write_err}")
return default
return resolved
except Exception as e:
logger.error(f"Could not read LANGFLOW_DATA_PATH from env, using default: {e}")
return default


def _run_tui_app():
"""Run the existing Textual TUI application."""
app = None
Expand Down
Loading
Loading