{
  "manifest_version": "1.0.0",
  "template": {
    "id": "c2b30f27-1ca2-42f0-a9d6-ac898281ab21",
    "slug": "odysseus-v1-private-ai-workspace",
    "name": "Odysseus v1 Private AI Workspace",
    "description": "Self-hosted AI workspace with web search, vector memory and alerts.",
    "url": "https://railway.com/deploy/odysseus-v1-private-ai-workspace",
    "upstream": {
      "repo_url": "https://github.com/baranberkay96/odysseus-railway"
    }
  },
  "services": [
    {
      "name": "Ollama",
      "source": {
        "repo": "https://github.com/baranberkay96/odysseus-railway"
      },
      "needs_volume": true,
      "volume_mount_path": "/root/.ollama",
      "http": false
    },
    {
      "name": "Odysseus",
      "source": {
        "repo": "https://github.com/baranberkay96/odysseus-railway"
      },
      "needs_volume": true,
      "volume_mount_path": "/app/data",
      "http": true
    },
    {
      "name": "SearXNG",
      "source": {
        "repo": "https://github.com/baranberkay96/odysseus-railway"
      },
      "needs_volume": false,
      "http": false
    },
    {
      "name": "ChromaDB",
      "source": {
        "image": "chromadb/chroma:1.5.9@sha256:1e0b73a187a28757c572acba508c46f48c9e8b0acaf5c20e6d95cdedce1acdf6"
      },
      "needs_volume": true,
      "volume_mount_path": "/data",
      "http": false
    },
    {
      "name": "ntfy",
      "source": {
        "repo": "https://github.com/baranberkay96/odysseus-railway"
      },
      "needs_volume": true,
      "volume_mount_path": "/var/lib/ntfy",
      "http": true
    }
  ],
  "required_inputs": [
    {
      "key": "PORT",
      "service": "Ollama",
      "description": "Ollama API port (private).",
      "secret": false,
      "strategy": "default",
      "default": "11434"
    },
    {
      "key": "OLLAMA_HOST",
      "service": "Ollama",
      "description": "Bind address (IPv4 + IPv6).",
      "secret": false,
      "strategy": "default",
      "default": "[::]:11434"
    },
    {
      "key": "OLLAMA_THINKING",
      "service": "Ollama",
      "description": "false: qwen3 answers directly, without a hidden reasoning phase (much faster on CPU). true: keep the model's own template, so qwen3 reasons before every answer. Redeploy Ollama after changing.",
      "secret": false,
      "strategy": "default",
      "default": "false"
    },
    {
      "key": "OLLAMA_BASE_MODEL",
      "service": "Ollama",
      "description": "Library model pulled on first boot (1.4 GB, Q4_K_M, tool calling, CPU-friendly).",
      "secret": false,
      "strategy": "default",
      "default": "qwen3:1.7b"
    },
    {
      "key": "OLLAMA_KEEP_ALIVE",
      "service": "Ollama",
      "description": "Unload the model after 30 idle minutes to free RAM.",
      "secret": false,
      "strategy": "default",
      "default": "30m"
    },
    {
      "key": "OLLAMA_GO_TEMPLATE",
      "service": "Ollama",
      "description": "Render chats with each model's Modelfile TEMPLATE (needed for the thinking-off template; otherwise Ollama may use the GGUF's embedded template).",
      "secret": false,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "OLLAMA_PULL_MODELS",
      "service": "Ollama",
      "description": "Models pulled on first boot (comma-separated).",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "OLLAMA_NUM_PARALLEL",
      "service": "Ollama",
      "description": "One request at a time (CPU); keeps memory predictable.",
      "secret": false,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "OLLAMA_DEFAULT_MODEL",
      "service": "Ollama",
      "description": "Tuned copy of OLLAMA_BASE_MODEL that Odysseus uses: CPU thread count, OLLAMA_CONTEXT_LENGTH and (for qwen3) thinking off. Recreated on every start.",
      "secret": false,
      "strategy": "default",
      "default": "qwen3-cpu:1.7b"
    },
    {
      "key": "OLLAMA_KV_CACHE_TYPE",
      "service": "Ollama",
      "description": "8-bit KV cache: halves context memory with little quality loss.",
      "secret": false,
      "strategy": "default",
      "default": "q8_0"
    },
    {
      "key": "OLLAMA_CONTEXT_LENGTH",
      "service": "Ollama",
      "description": "Context window (num_ctx): room for Odysseus' tool instructions, memory and search results.",
      "secret": false,
      "strategy": "default",
      "default": "16384"
    },
    {
      "key": "OLLAMA_FLASH_ATTENTION",
      "service": "Ollama",
      "description": "Flash attention, needed for the quantized KV cache below.",
      "secret": false,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "OLLAMA_MAX_LOADED_MODELS",
      "service": "Ollama",
      "description": "Keep one model in memory.",
      "secret": false,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "PORT",
      "service": "Odysseus",
      "description": "Port uvicorn listens on (0.0.0.0). Railway routes the public domain and the /api/health check here.",
      "secret": false,
      "strategy": "default",
      "default": "7000"
    },
    {
      "key": "NTFY_TOPIC",
      "service": "Odysseus",
      "description": "ntfy topic for reminders and alerts. Subscribe to it in the ntfy app.",
      "secret": false,
      "strategy": "default",
      "default": "odysseus"
    },
    {
      "key": "AUTH_ENABLED",
      "service": "Odysseus",
      "description": "Keep true: login required for the web UI and API. false would expose shell, file and agent tools to anyone with the URL.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "DATABASE_URL",
      "service": "Odysseus",
      "description": "SQLite database for sessions, messages, documents and model endpoints, on the /app/data volume.",
      "secret": false,
      "strategy": "default",
      "default": "sqlite:////app/data/app.db"
    },
    {
      "key": "OLLAMA_MODEL",
      "service": "Odysseus",
      "description": "Tuned Ollama model registered on first boot; it becomes the default chat model unless LLM_PROVIDER_MODEL is set or you picked another default.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "CHROMADB_HOST",
      "service": "Odysseus",
      "description": "Private hostname of the ChromaDB vector store (memory vectors, RAG, semantic skills).",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "CHROMADB_PORT",
      "service": "Odysseus",
      "description": "ChromaDB port.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "ALLOWED_ORIGINS",
      "service": "Odysseus",
      "description": "Exact origins allowed for cross-origin API clients.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "FASTEMBED_MODEL",
      "service": "Odysseus",
      "description": "Local ONNX embedding model for memory and documents (upstream default, runs on CPU).",
      "secret": false,
      "strategy": "default",
      "default": "sentence-transformers/all-MiniLM-L6-v2"
    },
    {
      "key": "OLLAMA_BASE_URL",
      "service": "Odysseus",
      "description": "Private URL of the bundled Ollama service. Registered as a model endpoint on first boot.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "LOCALHOST_BYPASS",
      "service": "Odysseus",
      "description": "Keep false: loopback requests must authenticate too (upstream: development only).",
      "secret": false,
      "strategy": "default",
      "default": "false"
    },
    {
      "key": "SEARXNG_INSTANCE",
      "service": "Odysseus",
      "description": "Private SearXNG URL used for web search and deep research.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "NTFY_INTERNAL_URL",
      "service": "Odysseus",
      "description": "Private ntfy URL. Added as the ntfy integration for reminders on first boot.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "NTFY_PUBLISH_TOKEN",
      "service": "Odysseus",
      "description": "Write-only ntfy access token Odysseus publishes reminders with (stored encrypted in Odysseus).",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "ODYSSEUS_ADMIN_USER",
      "service": "Odysseus",
      "description": "Username of the admin account created on first boot.",
      "secret": false,
      "strategy": "default",
      "default": "admin"
    },
    {
      "key": "FASTEMBED_CACHE_PATH",
      "service": "Odysseus",
      "description": "Embedding model cache on the volume, so it is downloaded once.",
      "secret": false,
      "strategy": "default",
      "default": "/app/data/fastembed"
    },
    {
      "key": "LLM_PROVIDER_BASE_URL",
      "service": "Odysseus",
      "description": "Optional. OpenAI-compatible base URL for LLM_PROVIDER_API_KEY, e.g. https://openrouter.ai/api/v1.",
      "secret": false,
      "strategy": "default",
      "default": "https://api.openai.com/v1"
    },
    {
      "key": "CLEANUP_INTERVAL_HOURS",
      "service": "Odysseus",
      "description": "Interval of the built-in cleanup job.",
      "secret": false,
      "strategy": "default",
      "default": "24"
    },
    {
      "key": "OAUTH_REDIRECT_BASE_URL",
      "service": "Odysseus",
      "description": "External origin for remote MCP server OAuth callbacks.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "ODYSSEUS_ADMIN_PASSWORD",
      "service": "Odysseus",
      "description": "Admin password, generated at deploy. Used only to create the admin on first boot (and by the one-time bootstrap); change it later in Settings.",
      "secret": true,
      "strategy": "generate",
      "generate": "strong_password"
    },
    {
      "key": "CHROMADB_CONNECT_TIMEOUT",
      "service": "Odysseus",
      "description": "Seconds for the ChromaDB connect probe at startup.",
      "secret": false,
      "strategy": "default",
      "default": "5.0"
    },
    {
      "key": "GOOGLE_OAUTH_REDIRECT_URI",
      "service": "Odysseus",
      "description": "Gmail OAuth redirect URI (Railway's proxy is not on a trusted address, so it is pinned here). Only used if you connect Gmail.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "WAIT_FOR_SERVICES_SECONDS",
      "service": "Odysseus",
      "description": "How long the start script waits for ChromaDB before starting Odysseus anyway.",
      "secret": false,
      "strategy": "default",
      "default": "300"
    },
    {
      "key": "RAILWAY_DEPLOYMENT_DRAINING_SECONDS",
      "service": "Odysseus",
      "description": "Grace period for in-flight requests and SQLite writes on redeploy.",
      "secret": false,
      "strategy": "default",
      "default": "20"
    },
    {
      "key": "PORT",
      "service": "SearXNG",
      "description": "SearXNG port (granian, dual-stack ::). Railway's healthcheck probes /healthz here.",
      "secret": false,
      "strategy": "default",
      "default": "8080"
    },
    {
      "key": "SEARXNG_SECRET",
      "service": "SearXNG",
      "description": "SearXNG secret key (overrides server.secret_key), generated at deploy.",
      "secret": true,
      "strategy": "generate",
      "generate": "random_base64_32"
    },
    {
      "key": "SEARXNG_BASE_URL",
      "service": "SearXNG",
      "description": "Base URL SearXNG uses for its own links (private network only).",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "PORT",
      "service": "ChromaDB",
      "description": "ChromaDB HTTP port (private). Railway's healthcheck probes /api/v2/heartbeat here.",
      "secret": false,
      "strategy": "default",
      "default": "8000"
    },
    {
      "key": "ANONYMIZED_TELEMETRY",
      "service": "ChromaDB",
      "description": "Chroma product telemetry off (as in upstream compose).",
      "secret": false,
      "strategy": "default",
      "default": "FALSE"
    },
    {
      "key": "PORT",
      "service": "ntfy",
      "description": "ntfy HTTP port. Railway routes the public domain and the /v1/health check here.",
      "secret": false,
      "strategy": "default",
      "default": "8080"
    },
    {
      "key": "NTFY_BASE_URL",
      "service": "ntfy",
      "description": "Public URL of this ntfy server: use it as the server in the ntfy app.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "NTFY_AUTH_FILE",
      "service": "ntfy",
      "description": "User and access-control database, on the volume.",
      "secret": false,
      "strategy": "default",
      "default": "/var/lib/ntfy/user.db"
    },
    {
      "key": "NTFY_ADMIN_USER",
      "service": "ntfy",
      "description": "ntfy login for the web app and the Android/iOS app.",
      "secret": false,
      "strategy": "default",
      "default": "admin"
    },
    {
      "key": "NTFY_CACHE_FILE",
      "service": "ntfy",
      "description": "Message cache on the volume, so phones that were offline still get alerts after a redeploy.",
      "secret": false,
      "strategy": "default",
      "default": "/var/lib/ntfy/cache.db"
    },
    {
      "key": "NTFY_LISTEN_HTTP",
      "service": "ntfy",
      "description": "ntfy listen address (Go, dual-stack).",
      "secret": false,
      "strategy": "default",
      "default": ":8080"
    },
    {
      "key": "NTFY_BEHIND_PROXY",
      "service": "ntfy",
      "description": "Trust Railway's proxy headers for client IPs (rate limiting).",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "NTFY_ENABLE_LOGIN",
      "service": "ntfy",
      "description": "Show the login in the ntfy web app.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "NTFY_REQUIRE_LOGIN",
      "service": "ntfy",
      "description": "The web app is only usable after login.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "NTFY_ADMIN_PASSWORD",
      "service": "ntfy",
      "description": "Password of NTFY_ADMIN_USER, generated at deploy.",
      "secret": true,
      "strategy": "generate",
      "generate": "strong_password"
    },
    {
      "key": "NTFY_CACHE_DURATION",
      "service": "ntfy",
      "description": "How long messages are kept for clients that were offline.",
      "secret": false,
      "strategy": "default",
      "default": "72h"
    },
    {
      "key": "NTFY_ODYSSEUS_TOKEN",
      "service": "ntfy",
      "description": "Access token of the write-only 'odysseus' publisher user (ntfy format tk_ + 29 chars). Odysseus reads it by reference.",
      "secret": true,
      "strategy": "generate",
      "generate": "random_base64_32"
    },
    {
      "key": "NTFY_AUTH_DEFAULT_ACCESS",
      "service": "ntfy",
      "description": "Anonymous users can neither read nor publish.",
      "secret": false,
      "strategy": "default",
      "default": "deny-all"
    },
    {
      "key": "NTFY_ATTACHMENT_CACHE_DIR",
      "service": "ntfy",
      "description": "Attachment storage on the volume.",
      "secret": false,
      "strategy": "default",
      "default": "/var/lib/ntfy/attachments"
    }
  ],
  "deploy": {
    "mcp": {
      "server": "railway",
      "tool": "deploy_template",
      "args": {
        "template_code": "odysseus-v1-private-ai-workspace"
      }
    },
    "cli": "railway deploy --template odysseus-v1-private-ai-workspace",
    "api": {
      "method": "POST",
      "path": "/graphql/v2",
      "body": {
        "query": "mutation templateDeploy($input: TemplateDeployV2Input!) { templateDeployV2(input: $input) { projectId workflowId } }",
        "variables": {
          "input": {
            "templateId": "c2b30f27-1ca2-42f0-a9d6-ac898281ab21",
            "serializedConfig": {
              "buckets": {},
              "services": {
                "22c4f167-a98d-504e-b42d-facdb2547274": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/ollama.svg",
                  "name": "Ollama",
                  "build": {
                    "builder": "DOCKERFILE"
                  },
                  "deploy": {
                    "healthcheckPath": "/",
                    "restartPolicyType": "ON_FAILURE",
                    "healthcheckTimeout": 600,
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "repo": "baranberkay96/odysseus-railway",
                    "branch": "main",
                    "rootDirectory": "/services/ollama"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": true,
                      "description": "Ollama API port (private).",
                      "defaultValue": "11434"
                    },
                    "OLLAMA_HOST": {
                      "isOptional": true,
                      "description": "Bind address (IPv4 + IPv6).",
                      "defaultValue": "[::]:11434"
                    },
                    "OLLAMA_THINKING": {
                      "isOptional": true,
                      "description": "false: qwen3 answers directly, without a hidden reasoning phase (much faster on CPU). true: keep the model's own template, so qwen3 reasons before every answer. Redeploy Ollama after changing.",
                      "defaultValue": "false"
                    },
                    "OLLAMA_BASE_MODEL": {
                      "isOptional": true,
                      "description": "Library model pulled on first boot (1.4 GB, Q4_K_M, tool calling, CPU-friendly).",
                      "defaultValue": "qwen3:1.7b"
                    },
                    "OLLAMA_KEEP_ALIVE": {
                      "isOptional": true,
                      "description": "Unload the model after 30 idle minutes to free RAM.",
                      "defaultValue": "30m"
                    },
                    "OLLAMA_NUM_THREAD": {
                      "isOptional": true,
                      "description": "Optional. CPU threads for inference. Empty: this service's CPU quota, at most 8 (more threads than granted CPU time stall generation on shared hosts).",
                      "defaultValue": ""
                    },
                    "OLLAMA_GO_TEMPLATE": {
                      "isOptional": true,
                      "description": "Render chats with each model's Modelfile TEMPLATE (needed for the thinking-off template; otherwise Ollama may use the GGUF's embedded template).",
                      "defaultValue": "1"
                    },
                    "OLLAMA_PULL_MODELS": {
                      "isOptional": true,
                      "description": "Models pulled on first boot (comma-separated).",
                      "defaultValue": "${{OLLAMA_BASE_MODEL}}"
                    },
                    "OLLAMA_NUM_PARALLEL": {
                      "isOptional": true,
                      "description": "One request at a time (CPU); keeps memory predictable.",
                      "defaultValue": "1"
                    },
                    "OLLAMA_DEFAULT_MODEL": {
                      "isOptional": true,
                      "description": "Tuned copy of OLLAMA_BASE_MODEL that Odysseus uses: CPU thread count, OLLAMA_CONTEXT_LENGTH and (for qwen3) thinking off. Recreated on every start.",
                      "defaultValue": "qwen3-cpu:1.7b"
                    },
                    "OLLAMA_KV_CACHE_TYPE": {
                      "isOptional": true,
                      "description": "8-bit KV cache: halves context memory with little quality loss.",
                      "defaultValue": "q8_0"
                    },
                    "OLLAMA_CONTEXT_LENGTH": {
                      "isOptional": true,
                      "description": "Context window (num_ctx): room for Odysseus' tool instructions, memory and search results.",
                      "defaultValue": "16384"
                    },
                    "OLLAMA_FLASH_ATTENTION": {
                      "isOptional": true,
                      "description": "Flash attention, needed for the quantized KV cache below.",
                      "defaultValue": "1"
                    },
                    "OLLAMA_MAX_LOADED_MODELS": {
                      "isOptional": true,
                      "description": "Keep one model in memory.",
                      "defaultValue": "1"
                    }
                  },
                  "networking": {
                    "tcpProxies": {},
                    "serviceDomains": {}
                  },
                  "volumeMounts": {
                    "c3c6c2ee-c306-58a4-81b7-b447d3104b01": {
                      "mountPath": "/root/.ollama"
                    }
                  }
                },
                "4a9bc904-47d0-5cd3-bfcb-a9ba350fe1a2": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/odysseus.svg",
                  "name": "Odysseus",
                  "build": {
                    "builder": "DOCKERFILE"
                  },
                  "deploy": {
                    "healthcheckPath": "/api/health",
                    "restartPolicyType": "ON_FAILURE",
                    "healthcheckTimeout": 900,
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "repo": "baranberkay96/odysseus-railway",
                    "branch": "main",
                    "rootDirectory": "/services/odysseus"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": true,
                      "description": "Port uvicorn listens on (0.0.0.0). Railway routes the public domain and the /api/health check here.",
                      "defaultValue": "7000"
                    },
                    "NTFY_TOPIC": {
                      "isOptional": true,
                      "description": "ntfy topic for reminders and alerts. Subscribe to it in the ntfy app.",
                      "defaultValue": "odysseus"
                    },
                    "AUTH_ENABLED": {
                      "isOptional": true,
                      "description": "Keep true: login required for the web UI and API. false would expose shell, file and agent tools to anyone with the URL.",
                      "defaultValue": "true"
                    },
                    "DATABASE_URL": {
                      "isOptional": true,
                      "description": "SQLite database for sessions, messages, documents and model endpoints, on the /app/data volume.",
                      "defaultValue": "sqlite:////app/data/app.db"
                    },
                    "OLLAMA_MODEL": {
                      "isOptional": true,
                      "description": "Tuned Ollama model registered on first boot; it becomes the default chat model unless LLM_PROVIDER_MODEL is set or you picked another default.",
                      "defaultValue": "${{Ollama.OLLAMA_DEFAULT_MODEL}}"
                    },
                    "CHROMADB_HOST": {
                      "isOptional": true,
                      "description": "Private hostname of the ChromaDB vector store (memory vectors, RAG, semantic skills).",
                      "defaultValue": "${{ChromaDB.RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "CHROMADB_PORT": {
                      "isOptional": true,
                      "description": "ChromaDB port.",
                      "defaultValue": "${{ChromaDB.PORT}}"
                    },
                    "ALLOWED_ORIGINS": {
                      "isOptional": true,
                      "description": "Exact origins allowed for cross-origin API clients.",
                      "defaultValue": "https://${{RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "FASTEMBED_MODEL": {
                      "isOptional": true,
                      "description": "Local ONNX embedding model for memory and documents (upstream default, runs on CPU).",
                      "defaultValue": "sentence-transformers/all-MiniLM-L6-v2"
                    },
                    "OLLAMA_BASE_URL": {
                      "isOptional": true,
                      "description": "Private URL of the bundled Ollama service. Registered as a model endpoint on first boot.",
                      "defaultValue": "http://${{Ollama.RAILWAY_PRIVATE_DOMAIN}}:${{Ollama.PORT}}"
                    },
                    "LOCALHOST_BYPASS": {
                      "isOptional": true,
                      "description": "Keep false: loopback requests must authenticate too (upstream: development only).",
                      "defaultValue": "false"
                    },
                    "SEARXNG_INSTANCE": {
                      "isOptional": true,
                      "description": "Private SearXNG URL used for web search and deep research.",
                      "defaultValue": "http://${{SearXNG.RAILWAY_PRIVATE_DOMAIN}}:${{SearXNG.PORT}}"
                    },
                    "NTFY_INTERNAL_URL": {
                      "isOptional": true,
                      "description": "Private ntfy URL. Added as the ntfy integration for reminders on first boot.",
                      "defaultValue": "http://${{ntfy.RAILWAY_PRIVATE_DOMAIN}}:${{ntfy.PORT}}"
                    },
                    "LLM_PROVIDER_MODEL": {
                      "isOptional": true,
                      "description": "Optional. Model id on that provider (e.g. gpt-5-mini, or openai/gpt-5-mini on OpenRouter). When set it becomes the default chat model instead of the local Ollama model; otherwise pick a model in the chat model menu.",
                      "defaultValue": ""
                    },
                    "NTFY_PUBLISH_TOKEN": {
                      "isOptional": true,
                      "description": "Write-only ntfy access token Odysseus publishes reminders with (stored encrypted in Odysseus).",
                      "defaultValue": "${{ntfy.NTFY_ODYSSEUS_TOKEN}}"
                    },
                    "ODYSSEUS_ADMIN_USER": {
                      "isOptional": true,
                      "description": "Username of the admin account created on first boot.",
                      "defaultValue": "admin"
                    },
                    "FASTEMBED_CACHE_PATH": {
                      "isOptional": true,
                      "description": "Embedding model cache on the volume, so it is downloaded once.",
                      "defaultValue": "/app/data/fastembed"
                    },
                    "LLM_PROVIDER_API_KEY": {
                      "isOptional": true,
                      "description": "Optional, recommended for fast answers. API key of an OpenAI-compatible provider (OpenAI, OpenRouter, Groq, ...). When set, the provider is registered as a model endpoint on boot. More providers can be added in Settings.",
                      "defaultValue": ""
                    },
                    "LLM_PROVIDER_BASE_URL": {
                      "isOptional": true,
                      "description": "Optional. OpenAI-compatible base URL for LLM_PROVIDER_API_KEY, e.g. https://openrouter.ai/api/v1.",
                      "defaultValue": "https://api.openai.com/v1"
                    },
                    "CLEANUP_INTERVAL_HOURS": {
                      "isOptional": true,
                      "description": "Interval of the built-in cleanup job.",
                      "defaultValue": "24"
                    },
                    "OAUTH_REDIRECT_BASE_URL": {
                      "isOptional": true,
                      "description": "External origin for remote MCP server OAuth callbacks.",
                      "defaultValue": "https://${{RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "ODYSSEUS_ADMIN_PASSWORD": {
                      "isOptional": true,
                      "description": "Admin password, generated at deploy. Used only to create the admin on first boot (and by the one-time bootstrap); change it later in Settings.",
                      "defaultValue": "{{ODYSSEUS_ADMIN_PASSWORD}}"
                    },
                    "CHROMADB_CONNECT_TIMEOUT": {
                      "isOptional": true,
                      "description": "Seconds for the ChromaDB connect probe at startup.",
                      "defaultValue": "5.0"
                    },
                    "GOOGLE_OAUTH_REDIRECT_URI": {
                      "isOptional": true,
                      "description": "Gmail OAuth redirect URI (Railway's proxy is not on a trusted address, so it is pinned here). Only used if you connect Gmail.",
                      "defaultValue": "https://${{RAILWAY_PUBLIC_DOMAIN}}/api/email/oauth/google/callback"
                    },
                    "WAIT_FOR_SERVICES_SECONDS": {
                      "isOptional": true,
                      "description": "How long the start script waits for ChromaDB before starting Odysseus anyway.",
                      "defaultValue": "300"
                    },
                    "RAILWAY_DEPLOYMENT_DRAINING_SECONDS": {
                      "isOptional": true,
                      "description": "Grace period for in-flight requests and SQLite writes on redeploy.",
                      "defaultValue": "20"
                    }
                  },
                  "networking": {
                    "tcpProxies": {},
                    "serviceDomains": {
                      "<hasDomain>:7000": {
                        "port": 7000
                      }
                    }
                  },
                  "volumeMounts": {
                    "155f5e84-36d5-5698-b9fa-6a2835a82e90": {
                      "mountPath": "/app/data"
                    }
                  }
                },
                "a8546c3f-a7c0-5849-ae4e-3eb527c40973": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/searxng.svg",
                  "name": "SearXNG",
                  "build": {
                    "builder": "DOCKERFILE"
                  },
                  "deploy": {
                    "healthcheckPath": "/healthz",
                    "restartPolicyType": "ON_FAILURE",
                    "healthcheckTimeout": 300,
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "repo": "baranberkay96/odysseus-railway",
                    "branch": "main",
                    "rootDirectory": "/services/searxng"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": true,
                      "description": "SearXNG port (granian, dual-stack ::). Railway's healthcheck probes /healthz here.",
                      "defaultValue": "8080"
                    },
                    "SEARXNG_SECRET": {
                      "isOptional": true,
                      "description": "SearXNG secret key (overrides server.secret_key), generated at deploy.",
                      "defaultValue": "{{SEARXNG_SECRET}}"
                    },
                    "SEARXNG_BASE_URL": {
                      "isOptional": true,
                      "description": "Base URL SearXNG uses for its own links (private network only).",
                      "defaultValue": "http://${{RAILWAY_PRIVATE_DOMAIN}}:8080/"
                    }
                  },
                  "networking": {
                    "tcpProxies": {},
                    "serviceDomains": {}
                  }
                },
                "e0ee6141-ba7e-5276-9510-1c782ed5adfd": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/chroma.svg",
                  "name": "ChromaDB",
                  "deploy": {
                    "healthcheckPath": "/api/v2/heartbeat",
                    "restartPolicyType": "ON_FAILURE",
                    "healthcheckTimeout": 300,
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "image": "chromadb/chroma:1.5.9@sha256:1e0b73a187a28757c572acba508c46f48c9e8b0acaf5c20e6d95cdedce1acdf6"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": true,
                      "description": "ChromaDB HTTP port (private). Railway's healthcheck probes /api/v2/heartbeat here.",
                      "defaultValue": "8000"
                    },
                    "ANONYMIZED_TELEMETRY": {
                      "isOptional": true,
                      "description": "Chroma product telemetry off (as in upstream compose).",
                      "defaultValue": "FALSE"
                    }
                  },
                  "networking": {
                    "tcpProxies": {},
                    "serviceDomains": {}
                  },
                  "volumeMounts": {
                    "c4afa45c-6d51-501f-a266-d9b838995220": {
                      "mountPath": "/data"
                    }
                  }
                },
                "f2273c53-e732-5857-aebf-a4b3f1966cbd": {
                  "icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/ntfy.svg",
                  "name": "ntfy",
                  "build": {
                    "builder": "DOCKERFILE"
                  },
                  "deploy": {
                    "healthcheckPath": "/v1/health",
                    "restartPolicyType": "ON_FAILURE",
                    "healthcheckTimeout": 300,
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "repo": "baranberkay96/odysseus-railway",
                    "branch": "main",
                    "rootDirectory": "/services/ntfy"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": true,
                      "description": "ntfy HTTP port. Railway routes the public domain and the /v1/health check here.",
                      "defaultValue": "8080"
                    },
                    "NTFY_BASE_URL": {
                      "isOptional": true,
                      "description": "Public URL of this ntfy server: use it as the server in the ntfy app.",
                      "defaultValue": "https://${{RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "NTFY_AUTH_FILE": {
                      "isOptional": true,
                      "description": "User and access-control database, on the volume.",
                      "defaultValue": "/var/lib/ntfy/user.db"
                    },
                    "NTFY_ADMIN_USER": {
                      "isOptional": true,
                      "description": "ntfy login for the web app and the Android/iOS app.",
                      "defaultValue": "admin"
                    },
                    "NTFY_CACHE_FILE": {
                      "isOptional": true,
                      "description": "Message cache on the volume, so phones that were offline still get alerts after a redeploy.",
                      "defaultValue": "/var/lib/ntfy/cache.db"
                    },
                    "NTFY_LISTEN_HTTP": {
                      "isOptional": true,
                      "description": "ntfy listen address (Go, dual-stack).",
                      "defaultValue": ":8080"
                    },
                    "NTFY_BEHIND_PROXY": {
                      "isOptional": true,
                      "description": "Trust Railway's proxy headers for client IPs (rate limiting).",
                      "defaultValue": "true"
                    },
                    "NTFY_ENABLE_LOGIN": {
                      "isOptional": true,
                      "description": "Show the login in the ntfy web app.",
                      "defaultValue": "true"
                    },
                    "NTFY_REQUIRE_LOGIN": {
                      "isOptional": true,
                      "description": "The web app is only usable after login.",
                      "defaultValue": "true"
                    },
                    "NTFY_ADMIN_PASSWORD": {
                      "isOptional": true,
                      "description": "Password of NTFY_ADMIN_USER, generated at deploy.",
                      "defaultValue": "{{NTFY_ADMIN_PASSWORD}}"
                    },
                    "NTFY_CACHE_DURATION": {
                      "isOptional": true,
                      "description": "How long messages are kept for clients that were offline.",
                      "defaultValue": "72h"
                    },
                    "NTFY_ODYSSEUS_TOKEN": {
                      "isOptional": true,
                      "description": "Access token of the write-only 'odysseus' publisher user (ntfy format tk_ + 29 chars). Odysseus reads it by reference.",
                      "defaultValue": "{{NTFY_ODYSSEUS_TOKEN}}"
                    },
                    "NTFY_UPSTREAM_BASE_URL": {
                      "isOptional": true,
                      "description": "Optional. Set https://ntfy.sh for instant iOS push (ntfy.sh then relays a wake-up poll with only the message ID). Android and web need nothing.",
                      "defaultValue": ""
                    },
                    "NTFY_AUTH_DEFAULT_ACCESS": {
                      "isOptional": true,
                      "description": "Anonymous users can neither read nor publish.",
                      "defaultValue": "deny-all"
                    },
                    "NTFY_ATTACHMENT_CACHE_DIR": {
                      "isOptional": true,
                      "description": "Attachment storage on the volume.",
                      "defaultValue": "/var/lib/ntfy/attachments"
                    }
                  },
                  "networking": {
                    "tcpProxies": {},
                    "serviceDomains": {
                      "<hasDomain>:8080": {
                        "port": 8080
                      }
                    }
                  },
                  "volumeMounts": {
                    "735531d2-4edd-57cd-94c2-7ad64cf27331": {
                      "mountPath": "/var/lib/ntfy"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "post_deploy": {
    "healthcheck": {
      "service": "Odysseus",
      "method": "GET",
      "path": "/api/health",
      "expect_status": 200
    }
  },
  "resources": {
    "expected_services": 5,
    "needs_volume": true
  },
  "generated_at": "2026-10-13T22:14:42.035Z",
  "generator_version": "0.1.0",
  "status": "degraded",
  "validated_at": "2026-10-13T19:29:03.115Z",
  "success_rate_30d": 0,
  "validation": {
    "last_run_id": "run_ecf7f8511ba7477588fe",
    "checks": [
      {
        "name": "workflow_completed",
        "passed": true
      },
      {
        "name": "all_services_deployed",
        "passed": false,
        "detail": "FAILED,QUEUED,SUCCESS,SUCCESS,SUCCESS"
      }
    ]
  }
}
