<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Murat</title>
    <description>The latest articles on DEV Community by Murat (@muratmed).</description>
    <link>https://dev.to/muratmed</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4167524%2F54cb5cb1-c0bf-4cd9-a48e-7571c037fd24.jpg</url>
      <title>DEV Community: Murat</title>
      <link>https://dev.to/muratmed</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muratmed"/>
    <language>en</language>
    <item>
      <title>Self-Hosting an AI Assistant on CasaOS: A Step-by-Step OpenMuse Install Guide (Pitfalls Included)</title>
      <dc:creator>Murat</dc:creator>
      <pubDate>Wed, 07 Oct 2026 00:25:00 +0000</pubDate>
      <link>https://dev.to/muratmed/self-hosting-an-ai-assistant-on-casaos-a-step-by-step-openmuse-install-guide-pitfalls-included-228b</link>
      <guid>https://dev.to/muratmed/self-hosting-an-ai-assistant-on-casaos-a-step-by-step-openmuse-install-guide-pitfalls-included-228b</guid>
      <description>&lt;p&gt;&lt;em&gt;How to deploy &lt;a href="https://github.com/CopilotKit/openmuse" rel="noopener noreferrer"&gt;OpenMuse&lt;/a&gt; — a self-hosted AI chat assistant that can also manage your CasaOS server — using SSH and Docker Compose. Every step below exists because we did it the wrong way first.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What you're building
&lt;/h2&gt;

&lt;p&gt;A home server running &lt;a href="https://www.casaos.io/" rel="noopener noreferrer"&gt;CasaOS&lt;/a&gt; (this guide was tested on v0.4.9) gets a conversational AI assistant living on the same box — one that doesn't just chat, but can list your apps, read logs, and start/stop containers with your approval. Three containers do the work:&lt;/p&gt;

&lt;p&gt;| Container | What it is | Port |&lt;br&gt;
|---|---|&lt;br&gt;
| &lt;code&gt;api&lt;/code&gt; | The Node API server + agent brain | 8787 |&lt;br&gt;
| &lt;code&gt;web&lt;/code&gt; | The chat UI (static site on nginx) | 8081 |&lt;br&gt;
| &lt;code&gt;browser-worker&lt;/code&gt; | Sandboxed browser for web tasks | 8790 (loopback only) |&lt;/p&gt;

&lt;p&gt;OpenMuse ships no official Dockerfiles and no CasaOS guide, so this post provides both. We'll deploy the &lt;a href="https://github.com/Magrebi/openmuse" rel="noopener noreferrer"&gt;&lt;code&gt;casaos-manager-v3&lt;/code&gt;&lt;/a&gt; branch, which adds a CasaOS control layer, a document library, and OCR on top of upstream OpenMuse.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A CasaOS box with Docker and Docker Compose installed (any CasaOS install has both)&lt;/li&gt;
&lt;li&gt;SSH access to the box from your own machine (Mac/Linux/Windows terminal)&lt;/li&gt;
&lt;li&gt;An OpenRouter API key (or any OpenAI-compatible endpoint)&lt;/li&gt;
&lt;li&gt;A CopilotKit Intelligence project key — required in every mode (&lt;code&gt;npx copilotkit@latest login&lt;/code&gt;, then &lt;code&gt;npx copilotkit@latest project select&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;About 30 minutes, most of it waiting for the build&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Step 0: SSH in — don't fight the CasaOS web terminal
&lt;/h2&gt;

&lt;p&gt;The CasaOS dashboard has a built-in terminal, and it will betray you: its xterm.js input silently mangles keystrokes mid-session (typed text arrives as &lt;code&gt;-&lt;/code&gt; characters, commands never execute, and retrying doesn't fix it). &lt;strong&gt;Do all of the following over SSH from your own machine.&lt;/strong&gt; If you must use the web terminal, paste whole command blocks — never type long commands by hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &amp;lt;user&amp;gt;@&amp;lt;server-lan-ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything below runs on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Make sure ports 8787, 8081, 8790 are free
&lt;/h2&gt;

&lt;p&gt;A previous install attempt (or a half-dead &lt;code&gt;tsx&lt;/code&gt; dev server) can leave a stale process holding port 8787. &lt;code&gt;docker compose down&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; kill host processes or containers from a &lt;em&gt;different&lt;/em&gt; compose project, so check before you start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'8787|8081|8790'&lt;/span&gt;
docker ps &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Names}} {{.Status}}'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'openmuse|worker'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If anything shows up, stop/remove it now. Every "address already in use" mystery we've seen traced back to this step being skipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create the install tree
&lt;/h2&gt;

&lt;p&gt;We'll keep everything under &lt;code&gt;/DATA/AppData/openmuse&lt;/code&gt; — the conventional CasaOS app-data location, so backups and future you know where to look.&lt;/p&gt;

&lt;p&gt;The Dockerfiles below expect the OpenMuse source tree in a &lt;code&gt;repo/&lt;/code&gt; subdirectory, so clone it there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /DATA/AppData/openmuse
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nv"&gt;$USER&lt;/span&gt;:&lt;span class="nv"&gt;$USER&lt;/span&gt; /DATA/AppData/openmuse
&lt;span class="nb"&gt;cd&lt;/span&gt; /DATA/AppData/openmuse
git clone &lt;span class="nt"&gt;--branch&lt;/span&gt; casaos-manager-v3 https://github.com/Magrebi/openmuse.git repo
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why not CasaOS's "Custom Install" button?&lt;/strong&gt; CasaOS passes the custom-install &lt;code&gt;command&lt;/code&gt; field to the container as &lt;strong&gt;one single argument&lt;/strong&gt;. A multi-flag server command (e.g. &lt;code&gt;llama-server -m model.gguf --port 8080&lt;/code&gt;) crashes with &lt;code&gt;error: invalid argument&lt;/code&gt;. This stack needs real Compose orchestration anyway — SSH + &lt;code&gt;docker compose&lt;/code&gt; is the reliable path.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your tree should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/DATA/AppData/openmuse/
├── docker-compose.yaml   # you write this (Step 3)
├── Dockerfile.api        # you write this (Step 3)
├── Dockerfile.web        # you write this (Step 3)
├── repo/                 # the OpenMuse source (cloned above)
├── .env                  # you write this (Step 4)
└── data/                 # app database + library files (created at runtime)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: The three build files
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;docker-compose.yaml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openmuse&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile.api&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openmuse-api:local&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8787:8787"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data:/app/.openmuse&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-e"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch('http://127.0.0.1:8787/api/health').then(r=&amp;gt;process.exit(r.ok?0:1)).catch(()=&amp;gt;process.exit(1))"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;60s&lt;/span&gt;

  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile.web&lt;/span&gt;
      &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# ⚠️ Baked into the static site at BUILD time.&lt;/span&gt;
        &lt;span class="c1"&gt;# Changing the API address later = rebuilding web. Pick something stable.&lt;/span&gt;
        &lt;span class="na"&gt;EXPO_PUBLIC_API_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://&amp;lt;server-lan-ip&amp;gt;:8787&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openmuse-web:local&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8081:80"&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;

  &lt;span class="na"&gt;browser-worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./repo/apps/worker&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openmuse-worker:local&lt;/span&gt;
    &lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;WORKER_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0&lt;/span&gt;
      &lt;span class="na"&gt;WORKER_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;same-value-as-WORKER_TOKEN-in-.env&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:8790:8790"&lt;/span&gt;   &lt;span class="c1"&gt;# loopback only — the browser never needs LAN exposure&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;worker-data:/data&lt;/span&gt;
    &lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/tmp:size=256m,mode=1777&lt;/span&gt;
    &lt;span class="na"&gt;shm_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256mb&lt;/span&gt;
    &lt;span class="na"&gt;mem_limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2g&lt;/span&gt;
    &lt;span class="na"&gt;pids_limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;256&lt;/span&gt;
    &lt;span class="na"&gt;read_only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;cap_drop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ALL&lt;/span&gt;
    &lt;span class="na"&gt;security_opt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;no-new-privileges:true&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-e"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch('http://127.0.0.1:8790/health').then(r=&amp;gt;process.exit(r.ok?0:1)).catch(()=&amp;gt;process.exit(1))"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;worker-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth noticing: the API reaches the worker over Compose's internal DNS (&lt;code&gt;http://browser-worker:8790&lt;/code&gt;, set in &lt;code&gt;.env&lt;/code&gt; below), and the worker's published port is bound to &lt;code&gt;127.0.0.1&lt;/code&gt; — browser automation has no business being on your LAN.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Dockerfile.api&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:24-bookworm-slim&lt;/span&gt;
&lt;span class="c"&gt;# Document OCR + PDF text extraction for the library (Tesseract + Poppler).&lt;/span&gt;
&lt;span class="c"&gt;# Don't pin apt versions: pins that don't exist in this Debian release&lt;/span&gt;
&lt;span class="c"&gt;# fail the build (we learned this the hard way with tesseract on bookworm).&lt;/span&gt;
&lt;span class="c"&gt;# If you must pin, verify the version first with `apt-cache policy &amp;lt;pkg&amp;gt;`&lt;/span&gt;
&lt;span class="c"&gt;# inside the base image.&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    tesseract-ocr tesseract-ocr-eng tesseract-ocr-tur poppler-utils &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists
&lt;span class="k"&gt;RUN &lt;/span&gt;corepack &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; corepack prepare pnpm@11.19.0 &lt;span class="nt"&gt;--activate&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; repo ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pnpm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--frozen-lockfile&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pnpm build:server
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PORT=8787 HOST=0.0.0.0 DATA_DIR=/app/.openmuse&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8787&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/apps/server/src/index.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;Dockerfile.web&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:24-bookworm-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;corepack &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; corepack prepare pnpm@11.19.0 &lt;span class="nt"&gt;--activate&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; repo ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pnpm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--frozen-lockfile&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; EXPO_PUBLIC_API_URL&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; EXPO_PUBLIC_API_URL=$EXPO_PUBLIC_API_URL&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pnpm &lt;span class="nt"&gt;--dir&lt;/span&gt; apps/mobile build:web

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; nginx:alpine&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/apps/mobile/dist/web /usr/share/nginx/html&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: The &lt;code&gt;.env&lt;/code&gt; file — every field explained
&lt;/h2&gt;

&lt;p&gt;Create it &lt;strong&gt;on the server with &lt;code&gt;nano&lt;/code&gt;&lt;/strong&gt; (or your editor of choice). Secrets go into this file on the host — never into a chat window, a prompt, or a screenshot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano /DATA/AppData/openmuse/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;WORKSPACE_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;live
&lt;span class="nv"&gt;AGENT_BACKEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;model

&lt;span class="c"&gt;# --- Model (OpenRouter example) ---&lt;/span&gt;
&lt;span class="c"&gt;# "openai/" is OpenMuse's internal routing prefix: everything after it&lt;/span&gt;
&lt;span class="c"&gt;# is the model id sent to OPENAI_BASE_URL.&lt;/span&gt;
&lt;span class="nv"&gt;MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;openai/anthropic/claude-sonnet-4-5
&lt;span class="nv"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://openrouter.ai/api/v1
&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;

&lt;span class="nv"&gt;CPK_INTELLIGENCE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;   &lt;span class="c"&gt;# required in every mode&lt;/span&gt;

&lt;span class="c"&gt;# --- Server ---&lt;/span&gt;
&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8787
&lt;span class="c"&gt;# ⚠️ HOST=0.0.0.0, not your Tailscale or LAN IP.&lt;/span&gt;
&lt;span class="c"&gt;# Binding to a specific IP broke restarts with EADDRNOTAVAIL when that&lt;/span&gt;
&lt;span class="c"&gt;# interface wasn't up yet. 0.0.0.0 is the safe choice.&lt;/span&gt;
&lt;span class="nv"&gt;HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.0.0.0
&lt;span class="nv"&gt;PUBLIC_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://&amp;lt;server-lan-ip&amp;gt;:8787
&lt;span class="nv"&gt;DATA_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/app/.openmuse
&lt;span class="nv"&gt;ALLOWED_ORIGINS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://&amp;lt;server-lan-ip&amp;gt;:8081
&lt;span class="nv"&gt;TASK_WORKER_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;COMPUTER_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;

&lt;span class="c"&gt;# --- Secrets: generate, don't invent ---&lt;/span&gt;
&lt;span class="nv"&gt;OPENMUSE_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;        &lt;span class="c"&gt;# ≥24 chars — you type this at first login&lt;/span&gt;
&lt;span class="nv"&gt;TOKEN_ENCRYPTION_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;       &lt;span class="c"&gt;# 32 random bytes, base64&lt;/span&gt;
&lt;span class="nv"&gt;BROWSER_WORKER_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://browser-worker:8790
&lt;span class="nv"&gt;WORKER_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;               &lt;span class="c"&gt;# must MATCH the value in docker-compose.yaml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate the three secrets on the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 24     &lt;span class="c"&gt;# → OPENMUSE_ACCESS_KEY&lt;/span&gt;
openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 32  &lt;span class="c"&gt;# → TOKEN_ENCRYPTION_KEY&lt;/span&gt;
openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 24     &lt;span class="c"&gt;# → WORKER_TOKEN (paste into BOTH .env and docker-compose.yaml)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Compose reads &lt;code&gt;.env&lt;/code&gt; only when containers are (re)created.&lt;/strong&gt; Editing &lt;code&gt;.env&lt;/code&gt; and running &lt;code&gt;docker compose restart&lt;/code&gt; does nothing — you need &lt;code&gt;docker compose up -d&lt;/code&gt; so the changed containers are recreated.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: Build and start
&lt;/h2&gt;

&lt;p&gt;The first build compiles the server and the web UI — expect 10–20 minutes on a modest box. Go make coffee.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /DATA/AppData/openmuse
docker compose build
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Verify it's healthy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:8787/api/health&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo
&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:8790/health&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want: &lt;code&gt;api&lt;/code&gt; healthy, &lt;code&gt;web&lt;/code&gt; up, &lt;code&gt;browser-worker&lt;/code&gt; healthy, and both &lt;code&gt;curl&lt;/code&gt; calls answering. If the API container keeps restarting, check &lt;code&gt;docker compose logs api --tail 50&lt;/code&gt; — the usual suspects are a wrong &lt;code&gt;HOST&lt;/code&gt; value (Step 4) or a port squatter (Step 1).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: First login and the CasaOS connection
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;http://&amp;lt;server-lan-ip&amp;gt;:8081&lt;/code&gt; in your browser.&lt;/li&gt;
&lt;li&gt;When asked for the access key, enter your &lt;code&gt;OPENMUSE_ACCESS_KEY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;To let the assistant manage CasaOS, connect it &lt;strong&gt;in the app's UI&lt;/strong&gt; (Connections → CasaOS) with your CasaOS username and password. These credentials are encrypted server-side (AES-256-GCM) and verified with a live login before saving — they are &lt;strong&gt;never&lt;/strong&gt; put in &lt;code&gt;.env&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What the CasaOS layer gives you: read-only tools (&lt;code&gt;casaos_list_apps&lt;/code&gt;, &lt;code&gt;casaos_app_status&lt;/code&gt;, &lt;code&gt;casaos_app_logs&lt;/code&gt;, &lt;code&gt;casaos_system_status&lt;/code&gt;) plus &lt;code&gt;casaos_start_app&lt;/code&gt; / &lt;code&gt;casaos_stop_app&lt;/code&gt; / &lt;code&gt;casaos_restart_app&lt;/code&gt; — but mutations never execute directly. The tool call only &lt;em&gt;prepares&lt;/em&gt; an action; you approve it in the Activity panel, approvals are single-use, and protected apps (OpenMuse itself and anything hosting its network access) are refused outright. Try &lt;em&gt;"which apps are installed?"&lt;/em&gt; first, then &lt;em&gt;"stop HandBrake"&lt;/em&gt; to see the approval card in action.&lt;/p&gt;

&lt;p&gt;The same branch also ships a document library with OCR (Tesseract runs fully on your box — nothing is sent to the cloud) and full-text search over your uploads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Back up before every update
&lt;/h2&gt;

&lt;p&gt;Future updates are: &lt;code&gt;git -C repo pull&lt;/code&gt;, &lt;code&gt;docker compose build&lt;/code&gt;, &lt;code&gt;docker compose up -d&lt;/code&gt;. But &lt;strong&gt;back up first, every time&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /DATA/AppData/openmuse-backup-&lt;span class="nv"&gt;$TS&lt;/span&gt;
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /DATA/AppData/openmuse/docker-compose.yaml &lt;span class="se"&gt;\&lt;/span&gt;
           /DATA/AppData/openmuse/Dockerfile.api &lt;span class="se"&gt;\&lt;/span&gt;
           /DATA/AppData/openmuse/Dockerfile.web &lt;span class="se"&gt;\&lt;/span&gt;
           /DATA/AppData/openmuse/.env &lt;span class="se"&gt;\&lt;/span&gt;
           /DATA/AppData/openmuse/data &lt;span class="se"&gt;\&lt;/span&gt;
           /DATA/AppData/openmuse-backup-&lt;span class="nv"&gt;$TS&lt;/span&gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two backup lessons from our own scars:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CasaOS's uninstall dialog has a "Delete userdata (config folder)" checkbox.&lt;/strong&gt; Ticked, it wipes &lt;code&gt;/DATA/AppData/&amp;lt;app&amp;gt;&lt;/code&gt; entirely — we lost a whole Jellyfin config this way. Read that checkbox twice before confirming any uninstall.&lt;/li&gt;
&lt;li&gt;Rollback is just &lt;code&gt;docker compose down&lt;/code&gt;, restoring the backup tree, and &lt;code&gt;docker compose up -d&lt;/code&gt;. Because you backed up &lt;code&gt;data/&lt;/code&gt;, the app database and library survive the round trip.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting cheat sheet
&lt;/h2&gt;

&lt;p&gt;| Symptom | Root cause | Fix |&lt;br&gt;
|---|---|&lt;br&gt;
| &lt;code&gt;address already in use&lt;/code&gt; on 8787 | Stale &lt;code&gt;tsx&lt;/code&gt; process or orphan container from an earlier attempt | &lt;code&gt;ss -tlnp \| grep 8787&lt;/code&gt;, kill it; &lt;code&gt;docker ps -a&lt;/code&gt; for orphans |&lt;br&gt;
| API exits with &lt;code&gt;EADDRNOTAVAIL&lt;/code&gt; | &lt;code&gt;HOST&lt;/code&gt; bound to an IP that wasn't up at start | &lt;code&gt;HOST=0.0.0.0&lt;/code&gt;, then &lt;code&gt;docker compose up -d&lt;/code&gt; |&lt;br&gt;
| &lt;code&gt;.env&lt;/code&gt; change had no effect | Env is captured at container creation | &lt;code&gt;docker compose up -d&lt;/code&gt; (recreate), not &lt;code&gt;restart&lt;/code&gt; |&lt;br&gt;
| Web UI talks to the wrong API URL | &lt;code&gt;EXPO_PUBLIC_API_URL&lt;/code&gt; is baked at build time | Rebuild &lt;code&gt;web&lt;/code&gt; after changing the arg |&lt;br&gt;
| CasaOS dashboard shows OpenMuse as "unknown" | CasaOS control-plane HTTP 500, not your app | Check &lt;code&gt;docker compose ps&lt;/code&gt; directly — it's usually fine |&lt;br&gt;
| Script gets &lt;code&gt;Session expired&lt;/code&gt; | Raw access key used as a Bearer token | &lt;code&gt;POST /api/session&lt;/code&gt; with the key first, use the returned session token |&lt;br&gt;
| &lt;code&gt;apt&lt;/code&gt; build fails on a pinned package | That version doesn't exist in bookworm | Drop the pin, or verify with &lt;code&gt;apt-cache policy&lt;/code&gt; in the base image |&lt;br&gt;
| &lt;code&gt;error: invalid argument&lt;/code&gt; in a container | CasaOS custom-install &lt;code&gt;command&lt;/code&gt; passed as one argument | Use &lt;code&gt;docker compose&lt;/code&gt; via SSH instead of the CasaOS dialog |&lt;/p&gt;

&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Remote access:&lt;/strong&gt; the plan we're settling on is Tailscale Serve with HTTPS (stays on your tailnet, no port forwarding, browser-trusted cert), with the LAN address kept working alongside. That's a post of its own once it's verified end to end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Second opinions:&lt;/strong&gt; because this gives an AI a path to mutate server state, we put the CasaOS-manager layer through two independent security reviews before going live. If you extend the tools, get a hostile review before you trust it.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Tested on CasaOS v0.4.9 with OpenMuse built from upstream &lt;code&gt;main&lt;/code&gt;. If a step breaks on your box, the troubleshooting table above is where the bodies are buried.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>casaos</category>
      <category>docker</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
