<?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: DeployLark</title>
    <description>The latest articles on DEV Community by DeployLark (@deploylark).</description>
    <link>https://dev.to/deploylark</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%2F4188014%2Feff5e0ad-288f-4605-8094-070bce7d34ad.png</url>
      <title>DEV Community: DeployLark</title>
      <link>https://dev.to/deploylark</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/deploylark"/>
    <language>en</language>
    <item>
      <title>Nginx Proxy Manager with Docker: Proxy Hosts and Free SSL</title>
      <dc:creator>DeployLark</dc:creator>
      <pubDate>Sat, 17 Oct 2026 11:20:05 +0000</pubDate>
      <link>https://dev.to/deploylark/nginx-proxy-manager-with-docker-proxy-hosts-and-free-ssl-18h3</link>
      <guid>https://dev.to/deploylark/nginx-proxy-manager-with-docker-proxy-hosts-and-free-ssl-18h3</guid>
      <description>&lt;p&gt;Once you run more than one self-hosted app, you hit the same problem every time. Each app wants its own port, and none of them has HTTPS. Nginx Proxy Manager (NPM) fixes both. It sits in front of your apps, answers on ports 80 and 443, sends each domain to the right container, and gets free Let's Encrypt certificates for you, all from a web UI.&lt;/p&gt;

&lt;p&gt;It's a popular choice. On October 14, 2026, the &lt;a href="https://github.com/NginxProxyManager/nginx-proxy-manager" rel="noopener noreferrer"&gt;project's GitHub repository&lt;/a&gt; had about 34,400 stars, and the &lt;a href="https://hub.docker.com/r/jc21/nginx-proxy-manager" rel="noopener noreferrer"&gt;&lt;code&gt;jc21/nginx-proxy-manager&lt;/code&gt; image&lt;/a&gt; had been pulled more than 285 million times from Docker Hub.&lt;/p&gt;

&lt;p&gt;This guide covers a clean install, the first-run setup (it changed in recent versions, so a lot of older guides are wrong), your first proxy host, both ways to get a certificate, and the errors you're most likely to hit.&lt;/p&gt;

&lt;p&gt;TL;DR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;jc21/nginx-proxy-manager:2.16.0&lt;/code&gt; with ports 80 and 443 public, bind the admin UI to &lt;code&gt;127.0.0.1:81&lt;/code&gt;, and keep both the &lt;code&gt;/data&lt;/code&gt; and &lt;code&gt;/etc/letsencrypt&lt;/code&gt; volumes.&lt;/li&gt;
&lt;li&gt;There's no &lt;code&gt;admin@example.com&lt;/code&gt; / &lt;code&gt;changeme&lt;/code&gt; login on 2.16. You create the admin account on the first-run screen, so use a real email, because NPM also uses it for Let's Encrypt.&lt;/li&gt;
&lt;li&gt;Put NPM and your apps on one shared Docker network and forward to the service name and the container's internal port. &lt;code&gt;localhost&lt;/code&gt; there means NPM itself and gives you a 502.&lt;/li&gt;
&lt;li&gt;Request the certificate in the SSL tab. HTTP-01 needs DNS pointing at the server and port 80 open. DNS-01 with a scoped Cloudflare token also covers wildcards.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Install Nginx Proxy Manager with Docker Compose
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Before you start
&lt;/h3&gt;

&lt;p&gt;Without a reverse proxy, you reach apps like this: &lt;code&gt;http://203.0.113.10:3001&lt;/code&gt;, &lt;code&gt;http://203.0.113.10:8080&lt;/code&gt;, and so on. That's one open port per app and no TLS.&lt;/p&gt;

&lt;p&gt;With a reverse proxy, only ports 80 and 443 are open. A request for &lt;code&gt;https://status.example.com&lt;/code&gt; reaches NPM. NPM reads the hostname and passes the request to the container that serves it (for example &lt;code&gt;uptime-kuma:3001&lt;/code&gt;) over the internal Docker network. NPM handles HTTPS, so the app behind it doesn't need to know anything about certificates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5msbsichnw614pwyjsai.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5msbsichnw614pwyjsai.webp" alt="Diagram of the setup in this guide. A browser request for https://status.example.com reaches Nginx Proxy Manager on ports 80 and 443. NPM picks the app by hostname and forwards over the proxy Docker network to uptime-kuma:3001 or n8n:5678. The admin UI listens on 127.0.0.1:81 only and is reached through an SSH tunnel." width="799" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Linux server with Docker and the Docker Compose plugin.&lt;/li&gt;
&lt;li&gt;A domain, with an &lt;code&gt;A&lt;/code&gt; record for each hostname (or a wildcard &lt;code&gt;*.example.com&lt;/code&gt;) pointing at the server's public IP.&lt;/li&gt;
&lt;li&gt;Ports 80 and 443 reachable from the internet (cloud firewall, router port forwarding). Port 80 matters even if you only want HTTPS, because Let's Encrypt's HTTP-01 check &lt;a href="https://letsencrypt.org/docs/challenge-types/" rel="noopener noreferrer"&gt;only works on port 80&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Nothing else already listening on 80, 443 or 81 on the host.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The compose file and first start
&lt;/h3&gt;

&lt;p&gt;Create a folder and a compose file. This is the &lt;a href="https://nginxproxymanager.com/guide/" rel="noopener noreferrer"&gt;official quick-setup file&lt;/a&gt; with three changes. The image is pinned to the current release, there's a timezone you'll want to change, and the admin port is bound to &lt;code&gt;127.0.0.1&lt;/code&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="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/npm &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/npm
nano compose.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;jc21/nginx-proxy-manager:2.16.0'&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;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;80:80'&lt;/span&gt;              &lt;span class="c1"&gt;# public HTTP&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;443:443'&lt;/span&gt;            &lt;span class="c1"&gt;# public HTTPS&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:81:81'&lt;/span&gt;    &lt;span class="c1"&gt;# admin UI, only reachable from the server itself&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;TZ&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Europe/London"&lt;/span&gt;   &lt;span class="c1"&gt;# change to your timezone&lt;/span&gt;
      &lt;span class="c1"&gt;# DISABLE_IPV6: 'true'  # uncomment if your host has no IPv6&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:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./letsencrypt:/etc/letsencrypt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first start runs database migrations and takes a little while. When you see &lt;code&gt;Backend PID ... listening on port 3000&lt;/code&gt;, it's ready. Check from the server itself, since port 81 only answers there now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:81/api/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a fresh install that returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"OK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"setup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"major"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"minor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"revision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;"setup":false&lt;/code&gt; means no admin account exists yet. That's the next step.&lt;/p&gt;

&lt;p&gt;Why bind port 81 to localhost? It's your admin panel, and upstream's file publishes &lt;code&gt;'81:81'&lt;/code&gt; on every interface. A host firewall won't save you here. Docker's &lt;a href="https://docs.docker.com/engine/network/packet-filtering-firewalls/" rel="noopener noreferrer"&gt;firewall docs&lt;/a&gt; say that when you publish a container's ports, "traffic to and from that container gets diverted before it goes through the ufw firewall settings." On my test box, with the &lt;code&gt;127.0.0.1&lt;/code&gt; binding, the API answered on 127.0.0.1 but not on the server's network address. To use the admin UI from your laptop, open an SSH tunnel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-L&lt;/span&gt; 8181:127.0.0.1:81 you@your-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then browse to &lt;code&gt;http://localhost:8181&lt;/code&gt;. Once NPM works, you can also put the admin UI behind its own proxy host with an access list.&lt;/p&gt;

&lt;p&gt;A few more things I learned in testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep both volumes. When I started the image without &lt;code&gt;/etc/letsencrypt&lt;/code&gt; mounted, it refused to start and logged &lt;code&gt;ERROR: /etc/letsencrypt is not mounted! Check your docker configuration.&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;SQLite is the default and is fine for a single server. The &lt;a href="https://nginxproxymanager.com/setup/" rel="noopener noreferrer"&gt;full setup docs&lt;/a&gt; also have MariaDB, MySQL and Postgres variants.&lt;/li&gt;
&lt;li&gt;The project doesn't publish minimum requirements. On my test box the idle container used about 110 to 130 MB of RAM.&lt;/li&gt;
&lt;li&gt;Images are built for amd64 and arm64. The setup docs say armv7 was dropped in 2.14 and point 32-bit ARM users to the &lt;code&gt;2.13.7&lt;/code&gt; tag.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Create the admin account
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The setup screen replaced the default login
&lt;/h3&gt;

&lt;p&gt;Open the admin UI through the SSH tunnel at &lt;code&gt;http://localhost:8181&lt;/code&gt;. If you kept upstream's &lt;code&gt;'81:81'&lt;/code&gt;, use &lt;code&gt;http://&amp;lt;server-ip&amp;gt;:81&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you've read an older guide, you're expecting to log in with &lt;code&gt;admin@example.com&lt;/code&gt; and &lt;code&gt;changeme&lt;/code&gt;. That was true up to the 2.12.x releases (it's in the &lt;a href="https://github.com/NginxProxyManager/nginx-proxy-manager/blob/v2.12.6/README.md" rel="noopener noreferrer"&gt;v2.12.6 README&lt;/a&gt;), but it was removed from the README in 2.13.0. On 2.16.0 there is no default account. You get a "Welcome! Get started by creating your admin account" screen with Full Name, Email address and New Password.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fducn28mohu31s7vf51j5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fducn28mohu31s7vf51j5.webp" alt="Nginx Proxy Manager 2.16 first-run setup screen, where you create the admin account with a full name, email address and password" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use a real email address here. NPM uses your account email as the Let's Encrypt account email. In my test the certificate request ran &lt;code&gt;certbot ... -m admin@lab.test ...&lt;/code&gt;, and Let's Encrypt rejected it with &lt;code&gt;The ACME server believes admin@lab.test is an invalid email address&lt;/code&gt;. A made-up address here means certificate requests fail later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scripted setup and two-factor login
&lt;/h3&gt;

&lt;p&gt;If you'd rather create the admin account without the setup screen (useful for scripted installs), the &lt;a href="https://nginxproxymanager.com/advanced-config/" rel="noopener noreferrer"&gt;advanced config docs&lt;/a&gt; list two variables for it. Don't type the password into &lt;code&gt;compose.yaml&lt;/code&gt;. Put it in a &lt;code&gt;.env&lt;/code&gt; file next to it that only you can read:&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;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF2&lt;/span&gt;&lt;span class="sh"&gt;'
INITIAL_ADMIN_EMAIL=you@example.com
INITIAL_ADMIN_PASSWORD=a-long-random-password
&lt;/span&gt;&lt;span class="no"&gt;EOF2
&lt;/span&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reference the variables in &lt;code&gt;compose.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;INITIAL_ADMIN_EMAIL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${INITIAL_ADMIN_EMAIL}&lt;/span&gt;
      &lt;span class="na"&gt;INITIAL_ADMIN_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${INITIAL_ADMIN_PASSWORD}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the first start, delete both lines from &lt;code&gt;compose.yaml&lt;/code&gt;, delete &lt;code&gt;.env&lt;/code&gt;, and run &lt;code&gt;docker compose up -d&lt;/code&gt;. I tested this. The account was created, &lt;code&gt;/api/&lt;/code&gt; reported &lt;code&gt;"setup":true&lt;/code&gt;, and after removing the variables and recreating the container I could still log in with the same password. Change the password in the UI if it was ever stored anywhere else.&lt;/p&gt;

&lt;p&gt;NPM has supported TOTP two-factor authentication since 2.13.6. The 2.16 UI includes an "Enable Two-Factor Authentication" option for your account. Turn it on, because this panel controls all your traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a proxy host
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Connect NPM to your apps with a shared network
&lt;/h3&gt;

&lt;p&gt;The easiest way to connect NPM to your apps is a shared Docker network. NPM then reaches each app by its service name, and the apps don't need to publish any ports to the host. This is the approach the &lt;a href="https://nginxproxymanager.com/advanced-config/" rel="noopener noreferrer"&gt;NPM docs recommend&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network create proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this to the bottom of NPM's &lt;code&gt;compose.yaml&lt;/code&gt; and to each app's compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proxy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;docker compose up -d&lt;/code&gt; again in each folder. If an app's compose file already defines its own networks, attach just the web service to &lt;code&gt;proxy&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;In the NPM UI, go to Hosts &amp;gt; Proxy Hosts &amp;gt; Add Proxy Host. The Details tab has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domain Names: &lt;code&gt;status.example.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Scheme: &lt;code&gt;http&lt;/code&gt;. This is the protocol between NPM and the app, which is almost always plain HTTP.&lt;/li&gt;
&lt;li&gt;Forward Hostname / IP: the container's service name, for example &lt;code&gt;uptime-kuma&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Forward Port: the port the app listens on inside its container (for example &lt;code&gt;3001&lt;/code&gt;), not a host port.&lt;/li&gt;
&lt;li&gt;Access List: leave it as Publicly Accessible for now.&lt;/li&gt;
&lt;li&gt;Options: Block Common Exploits is a sensible default. Turn on Websockets Support for any app with a live UI (see below).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the Details tab from my lab run, pointing a test hostname at a container named &lt;code&gt;whoami&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Firm95q4r91efc0b3cyo0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Firm95q4r91efc0b3cyo0.webp" alt="Add Proxy Host dialog in Nginx Proxy Manager 2.16, Details tab: domain status.lab.test, scheme http, forward hostname whoami, forward port 80, access list Publicly Accessible, with Block Common Exploits and Websockets Support switched on" width="541" height="657"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Save, then test from any machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://status.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my lab, a proxy host pointing at a container named &lt;code&gt;whoami&lt;/code&gt; on the shared network returned that container's page, and a hostname with no proxy host got NPM's "Congratulations!" default site. You can change that default under Settings &amp;gt; Default Site.&lt;/p&gt;

&lt;p&gt;The saved host shows up in the Proxy Hosts list:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgdbowwbulw4ftstifma5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgdbowwbulw4ftstifma5.webp" alt="Nginx Proxy Manager Proxy Hosts list with one entry: source status.lab.test, destination http://whoami:80, SSL HTTP Only, access Public, status Online" width="799" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a full example app behind NPM, see &lt;a href="https://deploylark.com/posts/uptime-kuma-docker-setup/" rel="noopener noreferrer"&gt;Uptime Kuma with Docker&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Websockets, access lists and custom locations
&lt;/h3&gt;

&lt;p&gt;Some apps won't work at all without HTTPS. Vaultwarden's web vault is one, as the &lt;a href="https://deploylark.com/posts/vaultwarden-https-required-fix/" rel="noopener noreferrer"&gt;Vaultwarden HTTPS guide&lt;/a&gt; shows. I tested that fix with Caddy, and an NPM proxy host with a Let's Encrypt certificate should fix it the same way.&lt;/p&gt;

&lt;p&gt;Websockets Support (Details tab) matters for apps with a live UI, such as Uptime Kuma, n8n, Home Assistant or Open WebUI. They break in strange ways without it, with "connection lost" banners and dashboards that never update. The &lt;a href="https://deploylark.com/posts/n8n-docker-webhook-url-connection-lost/" rel="noopener noreferrer"&gt;n8n guide&lt;/a&gt; and the &lt;a href="https://deploylark.com/posts/open-webui-cannot-connect-ollama-docker/" rel="noopener noreferrer"&gt;Open WebUI guide&lt;/a&gt; show what that looks like. When I turned the toggle on, NPM added these lines to the generated nginx config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="nv"&gt;$http_connection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you aren't sure whether an app needs it, turn it on. It doesn't hurt plain HTTP apps.&lt;/p&gt;

&lt;p&gt;Access lists (Access Lists menu) hold HTTP basic-auth users and allow or deny IP rules. Pick one in a proxy host's Access List field. It's a cheap way to lock down admin panels that have weak or no auth of their own. NPM 2.16.0 also added per-path access lists inside the proxy host modal.&lt;/p&gt;

&lt;p&gt;Custom Locations (a tab in the proxy host) send a path such as &lt;code&gt;/api&lt;/code&gt; to a different container or port than the rest of the host. Most single-app hosts don't need them.&lt;/p&gt;

&lt;p&gt;The Advanced tab (gear icon) takes raw nginx directives for the server block. Use it sparingly. If a snippet breaks the config, 2.16.0 keeps the failed file as &lt;code&gt;.conf.err&lt;/code&gt; so you can see what went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get a free Let's Encrypt certificate
&lt;/h2&gt;

&lt;p&gt;Edit the proxy host and open the SSL tab. Under SSL Certificate, pick "Request a new Certificate (with Let's Encrypt)". The other toggles are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Force SSL redirects HTTP to HTTPS. Turn it on once the certificate works.&lt;/li&gt;
&lt;li&gt;HTTP/2 Support is safe to enable.&lt;/li&gt;
&lt;li&gt;HSTS Enabled and HSTS Sub-domains tell browsers to use only HTTPS for this host (and its subdomains) for a long time. Leave these off until you're sure HTTPS works everywhere, because they're hard to undo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkmoerakgk7cifwi6pnff.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkmoerakgk7cifwi6pnff.webp" alt="SSL tab of the Add Proxy Host dialog with the SSL Certificate dropdown open, offering None (this host will not use HTTPS) or Request a new Certificate with Let's Encrypt" width="541" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then pick a challenge type.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP-01 (the default)
&lt;/h3&gt;

&lt;p&gt;Per &lt;a href="https://letsencrypt.org/docs/challenge-types/" rel="noopener noreferrer"&gt;Let's Encrypt's docs&lt;/a&gt;, HTTP-01 asks for a token at &lt;code&gt;http://&amp;lt;your-domain&amp;gt;/.well-known/acme-challenge/...&lt;/code&gt; and checks that your server returns it. NPM serves that path itself, from a &lt;code&gt;letsencrypt-acme-challenge&lt;/code&gt; folder in its data volume. The challenge can only succeed when DNS for the hostname already points at this server and port 80 is reachable from the internet. On Oct 14, 2026, I verified this domain and HTTP-01 path by getting a real Let's Encrypt certificate over HTTP-01 on a public Debian 13 server running Caddy, while NPM's own certificate request screen follows NPM's documentation.&lt;/p&gt;

&lt;p&gt;HTTP-01 can't issue wildcard certificates, and it fails if your ISP blocks port 80.&lt;/p&gt;

&lt;p&gt;NPM 2.16's code also includes a reachability check for HTTP-01. It writes a test file and asks an outside service (site24x7.com) to fetch it from your domain. Run it before you request a real certificate, because failed attempts count toward rate limits (covered below).&lt;/p&gt;

&lt;h3&gt;
  
  
  DNS-01 with Cloudflare
&lt;/h3&gt;

&lt;p&gt;With DNS-01, according to Let's Encrypt's docs, certbot proves you control the domain by creating a TXT record at &lt;code&gt;_acme-challenge.&amp;lt;your-domain&amp;gt;&lt;/code&gt;. Use it when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you want a wildcard certificate (&lt;code&gt;*.example.com&lt;/code&gt;),&lt;/li&gt;
&lt;li&gt;the server isn't reachable on port 80 (a home lab behind CGNAT, or an internal-only service), or&lt;/li&gt;
&lt;li&gt;you have several servers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the SSL tab, turn on Use DNS Challenge, choose your DNS Provider, and paste the credentials. NPM 2.16.0 ships certbot plugins for a long list of providers, Cloudflare included. For Cloudflare, NPM's template is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# Cloudflare API token
&lt;/span&gt;&lt;span class="py"&gt;dns_cloudflare_api_token&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your-cloudflare-api-token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create an API token (not the Global API Key) in the Cloudflare dashboard. The &lt;a href="https://certbot-dns-cloudflare.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;certbot-dns-cloudflare docs&lt;/a&gt; say it needs &lt;code&gt;Zone:DNS:Edit&lt;/code&gt; permission, only for the zones you need certificates for.&lt;/p&gt;

&lt;p&gt;Two cautions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;That token can change your DNS. Let's Encrypt's challenge docs warn that keeping DNS API credentials on a web server raises the stakes if the server is compromised, so scope the token as narrowly as you can.&lt;/li&gt;
&lt;li&gt;2.16.0 updated certbot, and the &lt;a href="https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/v2.16.0" rel="noopener noreferrer"&gt;release notes&lt;/a&gt; warn that some DNS plugins may need dependency tweaks. If your provider's plugin errors after an upgrade, check the issue tracker before you assume your token is wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fix common SSL errors
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Certificate requests that fail
&lt;/h3&gt;

&lt;h4&gt;
  
  
  "Internal Error" when requesting a certificate
&lt;/h4&gt;

&lt;p&gt;This is NPM's generic message. The real reason is in the certbot log, which sits in your data volume:&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 tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 50 ~/npm/data/logs/letsencrypt.log   &lt;span class="c"&gt;# the data folder is owned by root&lt;/span&gt;
docker compose logs app | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-A5&lt;/span&gt; &lt;span class="s2"&gt;"SSL&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;certbot"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The usual causes are DNS not pointing at the server yet, port 80 blocked, or an invalid account email (see above).&lt;/p&gt;

&lt;h4&gt;
  
  
  Challenge fails with "Timeout", "Connection refused" or the wrong IP
&lt;/h4&gt;

&lt;p&gt;Let's Encrypt couldn't fetch the token. Check these from outside your network (mobile data works):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short status.example.com       &lt;span class="c"&gt;# should print your server's public IP&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://status.example.com   &lt;span class="c"&gt;# should answer from NPM, not time out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the IP is wrong, fix DNS and wait for it to update. If you get a timeout, open port 80 in the cloud firewall or router. If you use Cloudflare's proxy (orange cloud) and HTTP-01 keeps failing, switch to the DNS challenge, which doesn't depend on the HTTP path.&lt;/p&gt;

&lt;h4&gt;
  
  
  Rate limited
&lt;/h4&gt;

&lt;p&gt;Retrying over and over burns Let's Encrypt's &lt;a href="https://letsencrypt.org/docs/rate-limits/" rel="noopener noreferrer"&gt;rate limits&lt;/a&gt;. The ones you're most likely to hit while troubleshooting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5 failed validations per hostname, per account, per hour.&lt;/li&gt;
&lt;li&gt;5 certificates for the exact same set of hostnames every 7 days. Reinstalling and requesting again is the classic way to hit this one.&lt;/li&gt;
&lt;li&gt;50 certificates per registered domain every 7 days.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Revoking certificates doesn't reset these limits. Fix the cause first, test it, then request once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Errors after the certificate is issued
&lt;/h3&gt;

&lt;h4&gt;
  
  
  ERR_TOO_MANY_REDIRECTS behind Cloudflare
&lt;/h4&gt;

&lt;p&gt;If Cloudflare's SSL/TLS mode is Flexible, Cloudflare talks to your server over plain HTTP. With Force SSL on, NPM redirects that to HTTPS, and Cloudflare requests HTTP again, in a loop. &lt;a href="https://developers.cloudflare.com/ssl/troubleshooting/too-many-redirects/" rel="noopener noreferrer"&gt;Cloudflare's fix&lt;/a&gt; is to set the mode to Full or higher, which needs a valid certificate on the origin (NPM's Let's Encrypt certificate works here), or else to stop redirecting at the origin.&lt;/p&gt;

&lt;h4&gt;
  
  
  502 Bad Gateway after a "successful" setup
&lt;/h4&gt;

&lt;p&gt;The certificate is fine, but NPM can't reach the app. The most common mistake is putting &lt;code&gt;localhost&lt;/code&gt; or &lt;code&gt;127.0.0.1&lt;/code&gt; in Forward Hostname. Inside the NPM container, that address means NPM itself. In my lab, a proxy host pointed at &lt;code&gt;127.0.0.1:8080&lt;/code&gt; returned 502, and the host's error log said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;connect() failed (111: Connection refused) while connecting to upstream ... upstream: "http://127.0.0.1:8080/"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the app's service name on the shared Docker network, with the container's internal port. If the app runs directly on the host instead of in Docker, use the host's LAN IP. Per-host logs are in &lt;code&gt;~/npm/data/logs/proxy-host-&amp;lt;id&amp;gt;_error.log&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Logging in and updating
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Why doesn't &lt;a href="mailto:admin@example.com"&gt;admin@example.com&lt;/a&gt; / changeme work?
&lt;/h4&gt;

&lt;p&gt;Because 2.16 has no default account. Since 2.13.0, a fresh install shows a setup screen where you create the admin account yourself.&lt;/p&gt;

&lt;h4&gt;
  
  
  How do I update Nginx Proxy Manager?
&lt;/h4&gt;

&lt;p&gt;The &lt;a href="https://nginxproxymanager.com/upgrading/" rel="noopener noreferrer"&gt;upgrading page&lt;/a&gt; says to run &lt;code&gt;docker compose pull&lt;/code&gt; and &lt;code&gt;docker compose up -d&lt;/code&gt;, and adds: "This project will automatically update any databases or other requirements so you don't have to follow any crazy instructions." Some releases do add migrations, so I'd back up first. With a pinned tag, change it in &lt;code&gt;compose.yaml&lt;/code&gt;, then:&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; ~/npm
docker compose stop                       &lt;span class="c"&gt;# don't copy the SQLite file while it's in use&lt;/span&gt;
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; data data.bak-&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;
docker compose pull &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the &lt;a href="https://github.com/NginxProxyManager/nginx-proxy-manager/releases" rel="noopener noreferrer"&gt;release notes&lt;/a&gt; before major bumps. 2.15.0 and 2.16.0 both updated certbot and warned that DNS plugins may need changes. Version 2.13.3 added a migration and warned that you can't easily downgrade after it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why are my settings gone after Docker restarts?
&lt;/h4&gt;

&lt;p&gt;NPM keeps its database and config in &lt;code&gt;/data&lt;/code&gt; and its certificates in &lt;code&gt;/etc/letsencrypt&lt;/code&gt;. If those aren't mounted to folders or volumes on the host, recreating the container starts from scratch. Use both volume lines from the compose file above. The official image won't even start without &lt;code&gt;/etc/letsencrypt&lt;/code&gt; mounted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Certificates and errors
&lt;/h3&gt;

&lt;h4&gt;
  
  
  How do I get a wildcard certificate?
&lt;/h4&gt;

&lt;p&gt;Use the DNS challenge. HTTP-01 can't issue wildcards. Request a certificate for &lt;code&gt;*.example.com&lt;/code&gt; (and &lt;code&gt;example.com&lt;/code&gt; if you want the bare domain too), turn on Use DNS Challenge, pick your provider and paste a scoped API token, as in the Cloudflare example above.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why do I get 502 Bad Gateway?
&lt;/h4&gt;

&lt;p&gt;NPM can't reach the app at the Forward Hostname and port you gave it. Don't use &lt;code&gt;localhost&lt;/code&gt; or &lt;code&gt;127.0.0.1&lt;/code&gt;, which mean the NPM container itself. Put NPM and the app on the same Docker network and use the service name and the container's internal port.&lt;/p&gt;

&lt;h4&gt;
  
  
  How do I protect the admin panel on port 81?
&lt;/h4&gt;

&lt;p&gt;Don't publish it to the internet. Port 81 serves the login form over plain HTTP. The compose file above binds it to &lt;code&gt;127.0.0.1&lt;/code&gt;, and on my test box it then answered only on the server itself. Reach it through an SSH tunnel (&lt;code&gt;ssh -L 8181:127.0.0.1:81 you@your-server&lt;/code&gt;). A reply in &lt;a href="https://github.com/NginxProxyManager/nginx-proxy-manager/issues/481" rel="noopener noreferrer"&gt;issue #481&lt;/a&gt; suggests another option: a proxy host such as &lt;code&gt;npm.example.com&lt;/code&gt; that forwards to &lt;code&gt;127.0.0.1&lt;/code&gt; port 81 with SSL, after which you remove the port 81 mapping from the compose file. If you go that way, I'd also add an access list to that proxy host so only your own IP addresses can reach it. Turn on two-factor login either way.&lt;/p&gt;




&lt;p&gt;Originally published on DeployLark: &lt;a href="https://deploylark.com/posts/nginx-proxy-manager-docker-ssl/" rel="noopener noreferrer"&gt;Nginx Proxy Manager with Docker: Proxy Hosts and Free SSL&lt;/a&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>docker</category>
      <category>nginx</category>
      <category>ssl</category>
    </item>
    <item>
      <title>Fix n8n 'Connection Lost' and Webhook URLs Behind a Proxy</title>
      <dc:creator>DeployLark</dc:creator>
      <pubDate>Fri, 16 Oct 2026 06:48:05 +0000</pubDate>
      <link>https://dev.to/deploylark/fix-n8n-connection-lost-and-webhook-urls-behind-a-proxy-3lal</link>
      <guid>https://dev.to/deploylark/fix-n8n-connection-lost-and-webhook-urls-behind-a-proxy-3lal</guid>
      <description>&lt;p&gt;You put n8n behind a reverse proxy, open &lt;code&gt;https://n8n.example.com&lt;/code&gt;, and two things go wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A red "Connection lost" badge sits in the editor. Hovering it says: "You have a connection issue or the server is down. n8n should reconnect automatically once the issue is resolved." It never reconnects. Executions don't update live.&lt;/li&gt;
&lt;li&gt;The Webhook node shows URLs like &lt;code&gt;http://localhost:5678/webhook/...&lt;/code&gt;, or &lt;code&gt;https://n8n.example.com:5678/webhook/...&lt;/code&gt;, that nobody outside the server can call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;They're separate problems with separate fixes. The first is the proxy. The second is n8n's environment variables. A third, quieter problem (n8n not trusting the proxy's headers) usually shows up in the logs at the same time. Each one was reproduced on a test box, using a test hostname over plain HTTP through the proxy, and checked every fix below.&lt;/p&gt;

&lt;p&gt;Short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Webhook URL shows &lt;code&gt;localhost:5678&lt;/code&gt;: set &lt;code&gt;N8N_WEBHOOK_URL=https://n8n.example.com/&lt;/code&gt; (or &lt;code&gt;WEBHOOK_URL&lt;/code&gt; before n8n 2.30.0) and recreate the container with &lt;code&gt;docker compose up -d&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"Connection lost" in the editor: the proxy has to pass WebSocket upgrades (&lt;code&gt;Upgrade&lt;/code&gt; and &lt;code&gt;Connection&lt;/code&gt; headers over HTTP/1.1) and keep the original &lt;code&gt;Host&lt;/code&gt;. In Nginx Proxy Manager, that's the Websockets Support toggle.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;N8N_PROXY_HOPS=1&lt;/code&gt; for one proxy, so n8n trusts the &lt;code&gt;X-Forwarded-*&lt;/code&gt; headers.&lt;/li&gt;
&lt;li&gt;Test with a Webhook node. Both of its URLs should start with your domain, with no &lt;code&gt;:5678&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why n8n breaks behind a reverse proxy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Symptoms and causes at a glance
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Webhook URL starts with &lt;code&gt;http://localhost:5678/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;n8n doesn't know its public URL&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;N8N_WEBHOOK_URL&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhook URL is &lt;code&gt;https://your-domain:5678/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;N8N_HOST&lt;/code&gt; and &lt;code&gt;N8N_PROTOCOL&lt;/code&gt; set, but no webhook URL&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;N8N_WEBHOOK_URL&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Connection lost" in the editor, browser console shows &lt;code&gt;[WebSocketClient] Connection lost, code=...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Proxy isn't passing WebSocket upgrades, or is rewriting the &lt;code&gt;Host&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;Fix the proxy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs show &lt;code&gt;ERR_ERL_UNEXPECTED_X_FORWARDED_FOR&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;n8n doesn't trust the proxy&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;N8N_PROXY_HOPS=1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Login page warns about a secure cookie&lt;/td&gt;
&lt;td&gt;You're on plain HTTP&lt;/td&gt;
&lt;td&gt;See the FAQ&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  A minimal n8n Docker Compose file
&lt;/h3&gt;

&lt;p&gt;This is the setup the rest of the article fixes up. It follows the shape of the &lt;a href="https://docs.n8n.io/deploy/host-n8n/install-options/use-a-cloud-provider/use-docker-compose" rel="noopener noreferrer"&gt;official Docker Compose docs&lt;/a&gt;: the image from n8n's own registry, a named volume for &lt;code&gt;/home/node/.n8n&lt;/code&gt;, and the port bound to localhost only, so the proxy is the only way in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n&lt;/span&gt;&lt;span class="pi"&gt;:&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;docker.n8n.io/n8nio/n8n:2.43.4&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;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:5678:5678"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GENERIC_TIMEZONE=Europe/Berlin&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TZ=Europe/Berlin&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true&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;n8n_data:/home/node/.n8n&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;n8n_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pin a version tag rather than &lt;code&gt;latest&lt;/code&gt;, so an upgrade only happens when you choose. &lt;code&gt;2.43.4&lt;/code&gt; was the current stable release at the time of testing. Check &lt;a href="https://docs.n8n.io/changelog/release-notes" rel="noopener noreferrer"&gt;n8n's release notes&lt;/a&gt; for newer ones.&lt;/p&gt;

&lt;p&gt;If your proxy also runs in Docker (Nginx Proxy Manager, Traefik, Caddy), put both containers on a shared network so the proxy can reach &lt;code&gt;http://n8n:5678&lt;/code&gt; by service name. You can then drop the &lt;code&gt;ports:&lt;/code&gt; section entirely. The &lt;a href="https://deploylark.com/posts/nginx-proxy-manager-docker-ssl/" rel="noopener noreferrer"&gt;Nginx Proxy Manager guide&lt;/a&gt; shows the shared-network setup.&lt;/p&gt;

&lt;p&gt;At idle, n8n used about 690 MiB of RAM on the test box (&lt;code&gt;docker stats&lt;/code&gt;). n8n's docs don't list an official minimum.&lt;/p&gt;

&lt;p&gt;Back up the &lt;code&gt;n8n_data&lt;/code&gt; volume. Per &lt;a href="https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/configuration-examples/set-a-custom-encryption-key" rel="noopener noreferrer"&gt;n8n's docs&lt;/a&gt;, n8n generates a random encryption key on first launch, saves it in that folder, and uses it to encrypt your saved credentials. Lose the volume and you lose the key, and with it access to every credential. If you'd rather manage the key yourself, set &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; before the first start.&lt;/p&gt;

&lt;p&gt;n8n is one of the most popular self-hosted automation tools. On October 14, 2026, its &lt;a href="https://github.com/n8n-io/n8n" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; had about 207,300 stars, and the &lt;a href="https://hub.docker.com/r/n8nio/n8n" rel="noopener noreferrer"&gt;&lt;code&gt;n8nio/n8n&lt;/code&gt; image&lt;/a&gt; had been pulled more than 271 million times from Docker Hub. So the fixes below get searched a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: tell n8n its public URL
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Set N8N_WEBHOOK_URL
&lt;/h3&gt;

&lt;p&gt;With the file above, open any Webhook node and the URL is &lt;code&gt;http://localhost:5678/webhook/...&lt;/code&gt;. n8n builds webhook URLs from what it knows about itself, and by default that's &lt;code&gt;localhost&lt;/code&gt;, port 5678, plain HTTP. It can't learn your domain from the proxy for this.&lt;/p&gt;

&lt;p&gt;This is the Webhook node on a default install in the lab:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fegrxicozirtqj0qj8llv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fegrxicozirtqj0qj8llv.webp" alt="n8n 2.43.4 Webhook node settings on a default install, with the Test URL shown as http://localhost:5678/webhook-test/deploylark-test" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The common half-fix is to set &lt;code&gt;N8N_HOST&lt;/code&gt; and &lt;code&gt;N8N_PROTOCOL&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_HOST=n8n.example.com&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PROTOCOL=https&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On n8n 2.43.4 that produced &lt;code&gt;https://n8n.example.com:5678/webhook/...&lt;/code&gt; in testing. The internal port leaks into the URL, because n8n doesn't know the proxy is listening on 443.&lt;/p&gt;

&lt;p&gt;The full fix is to set the webhook base URL explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GENERIC_TIMEZONE=Europe/Berlin&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TZ=Europe/Berlin&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_HOST=n8n.example.com&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PROTOCOL=https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_WEBHOOK_URL=https://n8n.example.com/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PROXY_HOPS=1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then recreate the container (a restart doesn't pick up new env vars):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Webhook URLs now read &lt;code&gt;https://n8n.example.com/webhook/...&lt;/code&gt;, with no port.&lt;/p&gt;

&lt;p&gt;In the lab, with &lt;code&gt;n8n.lab.test&lt;/code&gt; standing in for the real domain, the Production URL looked like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjx2ry3y01faztrm0vd7s.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjx2ry3y01faztrm0vd7s.webp" alt="n8n Webhook node after setting N8N_WEBHOOK_URL, with the Production URL shown as https://n8n.lab.test/webhook/deploylark-test and no port number" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  N8N_WEBHOOK_URL or WEBHOOK_URL?
&lt;/h3&gt;

&lt;p&gt;Most older guides and forum answers say &lt;code&gt;WEBHOOK_URL&lt;/code&gt;. n8n's current docs use &lt;code&gt;N8N_WEBHOOK_URL&lt;/code&gt;. The &lt;a href="https://docs.n8n.io/deploy/host-n8n/install-options/use-a-cloud-provider/use-docker-compose" rel="noopener noreferrer"&gt;Docker Compose page&lt;/a&gt; says it's "available from n8n 2.30.0" and to use &lt;code&gt;WEBHOOK_URL&lt;/code&gt; on earlier versions. The &lt;a href="https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/configuration-examples/configure-webhook-urls-with-reverse-proxy" rel="noopener noreferrer"&gt;webhook URL page&lt;/a&gt; says &lt;code&gt;WEBHOOK_URL&lt;/code&gt; "is deprecated from n8n 2.35.0". On 2.43.4, &lt;code&gt;WEBHOOK_URL&lt;/code&gt; still works but prints this at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WEBHOOK_URL -&amp;gt; Use N8N_WEBHOOK_URL instead, which sets the base URL for both test and production webhooks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So: on 2.30.0 or later, use &lt;code&gt;N8N_WEBHOOK_URL&lt;/code&gt;. On anything older, use &lt;code&gt;WEBHOOK_URL&lt;/code&gt;. Don't set both.&lt;/p&gt;

&lt;p&gt;If the editor itself lives at a different URL than the webhooks, there's also &lt;code&gt;N8N_EDITOR_BASE_URL&lt;/code&gt;. For a normal single-domain setup you don't need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: pass WebSockets and the Host header
&lt;/h2&gt;

&lt;p&gt;Other apps with a live UI need the same thing from a proxy. &lt;a href="https://deploylark.com/posts/uptime-kuma-docker-setup/" rel="noopener noreferrer"&gt;Uptime Kuma&lt;/a&gt; and &lt;a href="https://deploylark.com/posts/open-webui-cannot-connect-ollama-docker/" rel="noopener noreferrer"&gt;Open WebUI&lt;/a&gt; both break without WebSocket support.&lt;/p&gt;

&lt;p&gt;"Connection lost" is about the editor's live connection to the server, called the push connection. The browser opens it at &lt;code&gt;/rest/push&lt;/code&gt;. By default it's a WebSocket (&lt;code&gt;N8N_PUSH_BACKEND&lt;/code&gt; defaults to &lt;code&gt;websocket&lt;/code&gt;), so the proxy has to do two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pass the WebSocket upgrade. Forward the &lt;code&gt;Upgrade&lt;/code&gt; and &lt;code&gt;Connection&lt;/code&gt; headers, over HTTP/1.1.&lt;/li&gt;
&lt;li&gt;Keep the original Host. n8n compares the browser's &lt;code&gt;Origin&lt;/code&gt; header with the host the request was sent to. It checks &lt;code&gt;Forwarded&lt;/code&gt;, then &lt;code&gt;X-Forwarded-Host&lt;/code&gt;, then &lt;code&gt;Host&lt;/code&gt;. If they don't match, it rejects the connection.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxd9amb8oavunwv3l5zm5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxd9amb8oavunwv3l5zm5.webp" alt="Diagram of the request path. The browser sends HTTPS pages and webhooks and the WebSocket at /rest/push to the reverse proxy (NPM or nginx), which forwards plain HTTP to n8n on n8n:5678. The WebSocket leg needs the Upgrade and Connection headers and the original Host header. Webhook URLs come from N8N_WEBHOOK_URL, not from the request. Missing upgrade headers or a rewritten Host make the editor show Connection lost." width="798" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what each broken setup did in testing, so you can match it to your own:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Proxy setup&lt;/th&gt;
&lt;th&gt;Result on &lt;code&gt;/rest/push&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nginx Proxy Manager, Websockets Support off&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;401 Unauthorized&lt;/code&gt; (the request reaches n8n as a plain GET, not an upgrade)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nginx with only &lt;code&gt;proxy_pass&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HTTP 500&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nginx with &lt;code&gt;Upgrade&lt;/code&gt;/&lt;code&gt;Connection&lt;/code&gt; headers but no &lt;code&gt;Host&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;Connection opens, then closes with code 1008 and &lt;code&gt;Invalid origin!&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Either proxy configured as below&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For the third case, n8n's log spells it out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Origin header does NOT match the expected origin. (Origin: "http://n8n.lab.test" -&amp;gt; "n8n.lab.test", Expected: "n8n:5678" -&amp;gt; "n8n:5678", Protocol: "http")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;nginx sent &lt;code&gt;Host: n8n:5678&lt;/code&gt; (its upstream name) instead of the domain the browser used. n8n saw the mismatch and refused.&lt;/p&gt;

&lt;p&gt;In the browser, that setup left the editor with the "Connection lost" badge in the top right corner:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6xl2lbwuium1unhvr1ib.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6xl2lbwuium1unhvr1ib.webp" alt="n8n workflow editor with a single Webhook node and a Connection lost warning in the top right corner, loaded through an nginx block that did not pass the Host header" width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Nginx Proxy Manager
&lt;/h3&gt;

&lt;p&gt;Edit the proxy host and, on the Details tab, turn on Websockets Support. That's the whole fix. NPM already sends the original &lt;code&gt;Host&lt;/code&gt; and the &lt;code&gt;X-Forwarded-*&lt;/code&gt; headers. Use these settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domain Names: &lt;code&gt;n8n.example.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Scheme: &lt;code&gt;http&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Forward Hostname / IP: &lt;code&gt;n8n&lt;/code&gt; (the service name, if both are on the same Docker network)&lt;/li&gt;
&lt;li&gt;Forward Port: &lt;code&gt;5678&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Websockets Support: on&lt;/li&gt;
&lt;li&gt;SSL tab: your certificate, with Force SSL on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After turning it on, the push connection through NPM stayed open and the badge went away.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plain nginx
&lt;/h3&gt;

&lt;p&gt;This is the server block that worked in testing. The &lt;code&gt;map&lt;/code&gt; goes in the &lt;code&gt;http {}&lt;/code&gt; context, for example at the top of a file in &lt;code&gt;/etc/nginx/conf.d/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;map&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt; &lt;span class="nv"&gt;$connection_upgrade&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;default&lt;/span&gt; &lt;span class="s"&gt;upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;''&lt;/span&gt;      &lt;span class="s"&gt;close&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;n8n.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;# for HTTPS, use your usual "listen 443 ssl;" and ssl_certificate lines instead&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://n8n:5678&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;# or http://127.0.0.1:5678 if nginx runs on the host&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="nv"&gt;$connection_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_buffering&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt; &lt;span class="s"&gt;3600s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;proxy_buffering off&lt;/code&gt; keeps streamed responses flowing instead of nginx holding them back. The long &lt;code&gt;proxy_read_timeout&lt;/code&gt; stops nginx from closing an idle push connection after its default 60 seconds.&lt;/p&gt;

&lt;p&gt;Reload nginx (&lt;code&gt;nginx -t &amp;amp;&amp;amp; nginx -s reload&lt;/code&gt;), hard-refresh the editor, and the badge should be gone.&lt;/p&gt;

&lt;p&gt;Through the working server block in the lab, the same workflow loaded with no badge:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F89r0vw9c6dmrzkn7ctvq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F89r0vw9c6dmrzkn7ctvq.webp" alt="The same n8n workflow editor loaded through the corrected nginx server block, with no Connection lost warning in the top bar" width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you reach n8n at something like &lt;code&gt;https://n8n.example.com:8443&lt;/code&gt;, the browser's &lt;code&gt;Origin&lt;/code&gt; includes &lt;code&gt;:8443&lt;/code&gt;. If your proxy forwards only the hostname, n8n sees &lt;code&gt;n8n.example.com&lt;/code&gt; vs &lt;code&gt;n8n.example.com:8443&lt;/code&gt; and rejects it with &lt;code&gt;Invalid origin!&lt;/code&gt;. This reproduces through NPM. The simplest fix is to serve n8n on the standard port 443. If you can't, look at what the check compares. n8n's origin check (in its &lt;code&gt;origin-validator&lt;/code&gt; code) compares the Origin's host and port with the forwarded host, so the &lt;code&gt;Host&lt;/code&gt; / &lt;code&gt;X-Forwarded-Host&lt;/code&gt; your proxy sends has to include the port too. In nginx, &lt;code&gt;$http_host&lt;/code&gt; passes on the host exactly as the browser sent it, port included. &lt;code&gt;$host&lt;/code&gt; drops the port.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: trust the proxy, then test a webhook
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Set N8N_PROXY_HOPS
&lt;/h3&gt;

&lt;p&gt;Behind a proxy, log in once and check &lt;code&gt;docker compose logs n8n&lt;/code&gt;. Without &lt;code&gt;N8N_PROXY_HOPS&lt;/code&gt;, this appeared on every login through NPM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValidationError: The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default). ... ERR_ERL_UNEXPECTED_X_FORWARDED_FOR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;n8n's web server (Express) doesn't trust &lt;code&gt;X-Forwarded-*&lt;/code&gt; headers unless told how many proxies are in front of it. &lt;code&gt;N8N_PROXY_HOPS&lt;/code&gt; defaults to &lt;code&gt;0&lt;/code&gt;. Set it to the number of reverse proxies between the internet and n8n. For one Nginx Proxy Manager or one nginx, that's &lt;code&gt;1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PROXY_HOPS=1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that set, the warning went away. If you also have Cloudflare proxying in front of your own proxy, that's two hops.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify with a test webhook
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a workflow and add a Webhook trigger. Set HTTP Method to &lt;code&gt;POST&lt;/code&gt; and give it a fixed Path, like &lt;code&gt;deploylark-test&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The node panel has two URLs, Test URL and Production URL. Both should start with &lt;code&gt;https://n8n.example.com/&lt;/code&gt;. If either shows &lt;code&gt;localhost&lt;/code&gt; or &lt;code&gt;:5678&lt;/code&gt;, go back to Fix 1.&lt;/li&gt;
&lt;li&gt;Test URL. Click Listen for test event (or Execute workflow), then from any machine:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://n8n.example.com/webhook-test/deploylark-test &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'hello=world'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The data should show up in the editor right away. If it only shows up after a refresh, the push connection is still broken (Fix 2).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Production URL. Publish the workflow, then:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://n8n.example.com/webhook/deploylark-test &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'hello=world'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the default "Respond: Immediately", the response was &lt;code&gt;{"message":"Workflow was started"}&lt;/code&gt;. Production runs don't appear on the canvas. Check the workflow's Executions tab.&lt;/p&gt;

&lt;p&gt;The two 404s you'll hit while testing mean different things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test URL, not listening: &lt;code&gt;The requested webhook "deploylark-test" is not registered.&lt;/code&gt; with a hint to click Execute workflow first. The test URL only works for one call after each click.&lt;/li&gt;
&lt;li&gt;Production URL, workflow not published: &lt;code&gt;The requested webhook "POST deploylark-test" is not registered.&lt;/code&gt; The workflow must be published (the hint text still says "active" and "toggle", from older versions).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both 404s came through the proxy with the right domain, so they mean the proxy is fine and n8n just isn't listening on that path.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Webhooks and login
&lt;/h3&gt;

&lt;h4&gt;
  
  
  How do you change localhost:5678 in the webhook URL?
&lt;/h4&gt;

&lt;p&gt;Set &lt;code&gt;N8N_WEBHOOK_URL=https://n8n.example.com/&lt;/code&gt; (or &lt;code&gt;WEBHOOK_URL&lt;/code&gt; before n8n 2.30.0) and recreate the container with &lt;code&gt;docker compose up -d&lt;/code&gt;. Setting only &lt;code&gt;N8N_HOST&lt;/code&gt; and &lt;code&gt;N8N_PROTOCOL&lt;/code&gt; gave &lt;code&gt;https://n8n.example.com:5678/&lt;/code&gt;, with the internal port still in it. See Fix 1.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why does the login page say the n8n server is configured to use a secure cookie?
&lt;/h4&gt;

&lt;p&gt;The full message is "Your n8n server is configured to use a secure cookie, however you are either visiting this via an insecure URL, or using Safari." &lt;code&gt;N8N_SECURE_COOKIE&lt;/code&gt; defaults to &lt;code&gt;true&lt;/code&gt;, so the login cookie only works over HTTPS. Put a certificate on the proxy and use the &lt;code&gt;https://&lt;/code&gt; URL. Only set &lt;code&gt;N8N_SECURE_COOKIE=false&lt;/code&gt; for a throwaway local test, never on an instance reachable from the internet.&lt;/p&gt;

&lt;h4&gt;
  
  
  Changed the env vars and nothing happened?
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;docker compose restart&lt;/code&gt; reuses the old container config. Run &lt;code&gt;docker compose up -d&lt;/code&gt; so Compose recreates the container with the new environment. If you keep the values in a &lt;code&gt;.env&lt;/code&gt; file, Compose only uses them to fill in &lt;code&gt;${...}&lt;/code&gt; placeholders in &lt;code&gt;compose.yaml&lt;/code&gt;, so each variable still needs a line under &lt;code&gt;environment:&lt;/code&gt;. Then check that it took effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;n8n &lt;span class="nb"&gt;printenv&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;N8N_
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Proxies and Cloudflare
&lt;/h3&gt;

&lt;h4&gt;
  
  
  My proxy can't do WebSockets at all. Is there another option?
&lt;/h4&gt;

&lt;p&gt;Yes. Set &lt;code&gt;N8N_PUSH_BACKEND=sse&lt;/code&gt; and n8n uses Server-Sent Events, which is ordinary HTTP streaming. This was tested through Nginx Proxy Manager with Websockets Support off. &lt;code&gt;/rest/push&lt;/code&gt; returned &lt;code&gt;200&lt;/code&gt; with &lt;code&gt;Content-Type: text/event-stream&lt;/code&gt; and stayed open. On plain nginx, the server block above already has &lt;code&gt;proxy_buffering off&lt;/code&gt; and a long &lt;code&gt;proxy_read_timeout&lt;/code&gt;, both sensible for a long-lived stream. WebSockets is n8n's default, so only switch if you have to.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why do you get "Connection lost" behind Cloudflare?
&lt;/h4&gt;

&lt;p&gt;According to &lt;a href="https://developers.cloudflare.com/network/websockets/" rel="noopener noreferrer"&gt;Cloudflare's docs&lt;/a&gt;, proxied WebSockets are supported on all plans, with a WebSockets toggle on the zone's Network page. Make sure it's on. Then check three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If n8n's log shows &lt;code&gt;Invalid origin!&lt;/code&gt;, the host that reaches n8n doesn't match the browser's &lt;code&gt;Origin&lt;/code&gt;. Fix the &lt;code&gt;Host&lt;/code&gt; and &lt;code&gt;X-Forwarded-Host&lt;/code&gt; headers as in Fix 2.&lt;/li&gt;
&lt;li&gt;Cloudflare's proxy sits in front of yours, so count it in &lt;code&gt;N8N_PROXY_HOPS&lt;/code&gt; (see Fix 3).&lt;/li&gt;
&lt;li&gt;Cloudflare documents redirect loops with SSL mode "Flexible" in front of an origin that forces HTTPS. The &lt;a href="https://deploylark.com/posts/nginx-proxy-manager-docker-ssl/" rel="noopener noreferrer"&gt;Nginx Proxy Manager guide&lt;/a&gt; covers that loop.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published on DeployLark: &lt;a href="https://deploylark.com/posts/n8n-docker-webhook-url-connection-lost/" rel="noopener noreferrer"&gt;Fix n8n 'Connection Lost' and Webhook URLs Behind a Proxy&lt;/a&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>docker</category>
      <category>n8n</category>
      <category>nginx</category>
    </item>
    <item>
      <title>Open WebUI Can't Connect to Ollama in Docker: The Fixes</title>
      <dc:creator>DeployLark</dc:creator>
      <pubDate>Fri, 16 Oct 2026 06:42:41 +0000</pubDate>
      <link>https://dev.to/deploylark/open-webui-cant-connect-to-ollama-in-docker-the-fixes-3m41</link>
      <guid>https://dev.to/deploylark/open-webui-cant-connect-to-ollama-in-docker-the-fixes-3m41</guid>
      <description>&lt;p&gt;You start Open WebUI, open the model picker, and it's empty. Or the admin connection check fails. Ollama is clearly running, because &lt;code&gt;curl localhost:11434&lt;/code&gt; on the server answers &lt;code&gt;Ollama is running&lt;/code&gt;. So why can't Open WebUI see it?&lt;/p&gt;

&lt;p&gt;Almost always, Open WebUI is trying to reach Ollama at an address that doesn't lead to Ollama from inside its container. This guide gives you the URL that works for each common setup, a quick test you can run from inside the container, and the saved-settings trap that makes a correct fix look like it did nothing.&lt;/p&gt;

&lt;p&gt;Lots of people run this pair. On October 14, 2026, the &lt;a href="https://github.com/open-webui/open-webui" rel="noopener noreferrer"&gt;Open WebUI repository&lt;/a&gt; had about 155,600 GitHub stars, and the &lt;a href="https://hub.docker.com/r/ollama/ollama" rel="noopener noreferrer"&gt;&lt;code&gt;ollama/ollama&lt;/code&gt; image&lt;/a&gt; had been pulled more than 186 million times from Docker Hub.&lt;/p&gt;

&lt;p&gt;One warning before you start. Open WebUI's &lt;a href="https://docs.openwebui.com/features/authentication-access/rbac/roles/" rel="noopener noreferrer"&gt;roles documentation&lt;/a&gt; says: "The very first account created on a fresh installation is automatically assigned the Admin role." If port 3000 is reachable from the internet before you sign up, a stranger can create that account and take over the instance. Docker-published ports also skip UFW, according to &lt;a href="https://docs.docker.com/engine/network/packet-filtering-firewalls/" rel="noopener noreferrer"&gt;Docker's firewall docs&lt;/a&gt;. So every example below binds the UI to &lt;code&gt;127.0.0.1:3000&lt;/code&gt;. Reach it through a reverse proxy such as &lt;a href="https://deploylark.com/posts/nginx-proxy-manager-docker-ssl/" rel="noopener noreferrer"&gt;Nginx Proxy Manager&lt;/a&gt; or an SSH tunnel (&lt;code&gt;ssh -L 3000:127.0.0.1:3000 you@server&lt;/code&gt;), and create your admin account right away.&lt;/p&gt;

&lt;p&gt;Short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inside the Open WebUI container, &lt;code&gt;localhost&lt;/code&gt; is the container itself, so &lt;code&gt;http://localhost:11434&lt;/code&gt; never reaches Ollama.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;http://ollama:11434&lt;/code&gt; when both run in one Compose file. With Ollama on a Linux host, use &lt;code&gt;http://host.docker.internal:11434&lt;/code&gt; plus an &lt;code&gt;extra_hosts&lt;/code&gt; entry &lt;code&gt;host.docker.internal:host-gateway&lt;/code&gt;, and make Ollama listen on more than &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Test from inside the container, for example &lt;code&gt;docker exec open-webui curl -s http://ollama:11434&lt;/code&gt;. You want &lt;code&gt;Ollama is running&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Changed &lt;code&gt;OLLAMA_BASE_URL&lt;/code&gt; and nothing happened? Open WebUI saved the first value in its database. Change it under Admin Panel &amp;gt; Settings &amp;gt; Connections.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The right Ollama URL for your setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Quick answer
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where Ollama runs&lt;/th&gt;
&lt;th&gt;&lt;code&gt;OLLAMA_BASE_URL&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Also needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same Compose file as Open WebUI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;http://ollama:11434&lt;/code&gt; (the service name)&lt;/td&gt;
&lt;td&gt;Nothing else&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Directly on the Docker host (install script, systemd service)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://host.docker.internal:11434&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;On Linux, an &lt;code&gt;extra_hosts&lt;/code&gt; entry &lt;code&gt;- host.docker.internal:host-gateway&lt;/code&gt;, and Ollama listening on something other than &lt;code&gt;127.0.0.1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Another machine&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://&amp;lt;that-machine-ip&amp;gt;:11434&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ollama listening on &lt;code&gt;0.0.0.0&lt;/code&gt; there, port reachable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you changed &lt;code&gt;OLLAMA_BASE_URL&lt;/code&gt; and nothing happened, skip to the section on saved settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why localhost fails inside a container
&lt;/h3&gt;

&lt;p&gt;Each container has its own network namespace. Inside the Open WebUI container, &lt;code&gt;localhost&lt;/code&gt; and &lt;code&gt;127.0.0.1&lt;/code&gt; mean the Open WebUI container itself, not your server. Nothing in that container listens on 11434, so the connection is refused.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffhopoe9cronkwd7jv8oq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffhopoe9cronkwd7jv8oq.webp" alt="Diagram of the Ollama URLs seen from the Open WebUI container. http://localhost:11434 loops back into the Open WebUI container itself and is refused. http://ollama:11434 reaches an ollama container on the same Compose network by service name. http://host.docker.internal:11434 reaches Ollama on the Docker host, which needs the host-gateway entry and Ollama listening beyond 127.0.0.1." width="799" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The problem reproduces with &lt;code&gt;OLLAMA_BASE_URL=http://localhost:11434&lt;/code&gt; and Ollama running in a separate container. The model list came back as &lt;code&gt;{"models":[]}&lt;/code&gt;, with no error in the UI. The Open WebUI log had the real message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR | open_webui.routers.ollama:send_get_request:88 - Connection error: Cannot connect to host localhost:11434 ssl:default [Connect call failed ('127.0.0.1', 11434)]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the browser, the model picker just said "No models available":&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ejknyqflv0jakehmaxd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ejknyqflv0jakehmaxd.webp" alt="Open WebUI 0.12.0 chat screen with the model picker open, showing No models available and a Manage Connections link" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The connection check in the admin settings did show a problem. Clicking the verify button in the Edit Connection dialog for &lt;code&gt;http://localhost:11434&lt;/code&gt; gave a red "Ollama: Network Problem" message:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9dru5ch8909trwumono4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9dru5ch8909trwumono4.webp" alt="Open WebUI admin Connections settings with the Edit Connection dialog for http://localhost:11434 open and a red Ollama: Network Problem notification in the corner" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check your own log first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs open-webui 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"ollama&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;11434"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test the connection from inside the container
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Run curl where Open WebUI runs
&lt;/h3&gt;

&lt;p&gt;Don't test from your laptop or the host shell. Test from where Open WebUI actually runs. The official image includes &lt;code&gt;curl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;open-webui curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://ollama:11434
docker &lt;span class="nb"&gt;exec &lt;/span&gt;open-webui curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://host.docker.internal:11434/api/version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want &lt;code&gt;Ollama is running&lt;/code&gt; for the first and something like &lt;code&gt;{"version":"0.40.4"}&lt;/code&gt; for the second. Once it works, you can keep an eye on it with an HTTP monitor in &lt;a href="https://deploylark.com/posts/uptime-kuma-docker-setup/" rel="noopener noreferrer"&gt;Uptime Kuma&lt;/a&gt; pointed at the same URL, from a container that has the same network access. Replace &lt;code&gt;open-webui&lt;/code&gt; with your container name. &lt;code&gt;docker ps&lt;/code&gt; shows it, and Compose names look like &lt;code&gt;myproject-open-webui-1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the same test from the lab, first in a container set to &lt;code&gt;localhost&lt;/code&gt;, then in one set to the service name:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F53cin25wnrk7ia699wks.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F53cin25wnrk7ia699wks.webp" alt="Terminal output: docker exec curl to http://localhost:11434 from the misconfigured Open WebUI container fails with curl error 7, could not connect to server. The same curl to http://ollama:11434 from the working container prints Ollama is running, and /api/version returns version 0.40.4." width="800" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What the curl errors mean
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;curl: (7) Failed to connect ... Couldn't connect to server&lt;/code&gt;: the name resolved, but nothing accepted the connection on that IP and port. Ollama is probably listening on &lt;code&gt;127.0.0.1&lt;/code&gt; only (see the next section).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Could not resolve host&lt;/code&gt;: the name doesn't exist inside the container. On Linux, &lt;code&gt;host.docker.internal&lt;/code&gt; needs &lt;code&gt;extra_hosts&lt;/code&gt;. A Compose service name only resolves inside the same Compose network.&lt;/li&gt;
&lt;li&gt;A hang until timeout: something is dropping packets, usually a firewall.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ollama installed on the host
&lt;/h2&gt;

&lt;p&gt;This is the setup where you ran Ollama's install script on the server, so Ollama runs as a systemd service, and Open WebUI runs in Docker. It needs two fixes, and most people only make the first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add host.docker.internal on Linux
&lt;/h3&gt;

&lt;p&gt;On Docker Desktop (Mac and Windows), &lt;code&gt;host.docker.internal&lt;/code&gt; works out of the box. On Linux it doesn't exist unless you add it. Docker's special &lt;code&gt;host-gateway&lt;/code&gt; value maps it to the host's address on the default bridge network. On the test box that was &lt;code&gt;172.17.0.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's why Open WebUI's own quick-start command includes &lt;code&gt;--add-host&lt;/code&gt;. Here it is with the port bound to localhost. The docs use &lt;code&gt;-p 3000:8080&lt;/code&gt;, which listens on every interface.&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; 32 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.open-webui-secret &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.open-webui-secret
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:3000:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--add-host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host.docker.internal:host-gateway &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; open-webui:/app/backend/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;WEBUI_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.open-webui-secret&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; open-webui &lt;span class="nt"&gt;--restart&lt;/span&gt; always &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/open-webui/open-webui:v0.12.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or in Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;open-webui&lt;/span&gt;&lt;span class="pi"&gt;:&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;ghcr.io/open-webui/open-webui:v0.12.0&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:3000:8080"&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;open-webui:/app/backend/data&lt;/span&gt;
    &lt;span class="na"&gt;extra_hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;host.docker.internal:host-gateway&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}&lt;/span&gt;
      &lt;span class="c1"&gt;# - OLLAMA_BASE_URL=http://host.docker.internal:11434  # this is already the image's default&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;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;open-webui&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open WebUI's &lt;a href="https://docs.openwebui.com/reference/env-configuration/" rel="noopener noreferrer"&gt;environment variable reference&lt;/a&gt; lists &lt;code&gt;http://host.docker.internal:11434&lt;/code&gt; as the Docker default for &lt;code&gt;OLLAMA_BASE_URL&lt;/code&gt;. So for this setup, &lt;code&gt;extra_hosts&lt;/code&gt; is often the only thing missing from the compose file.&lt;/p&gt;

&lt;p&gt;Put the secret in a &lt;code&gt;.env&lt;/code&gt; file next to &lt;code&gt;compose.yaml&lt;/code&gt; instead of typing it into the file. Compose reads &lt;code&gt;.env&lt;/code&gt; automatically:&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"WEBUI_SECRET_KEY=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why bother with a fixed key? Without one, Open WebUI generates a key file inside the container. The same reference says that when the container is recreated and that file isn't in a persisted volume, a new key is generated. Every user gets logged out, OAuth sessions stop working, MCP tools fail to decrypt their tokens, and Open WebUI refuses to start if MFA is on (unless &lt;code&gt;MFA_ENCRYPTION_KEY&lt;/code&gt; is set).&lt;/p&gt;

&lt;p&gt;Run either key-generating line once. Running it again overwrites the key with a new one, and a new key has exactly those effects: everyone is logged out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make Ollama listen on more than 127.0.0.1
&lt;/h3&gt;

&lt;p&gt;This one catches nearly everyone with Ollama on the host. &lt;a href="https://docs.ollama.com/faq" rel="noopener noreferrer"&gt;Ollama's FAQ&lt;/a&gt; puts it plainly: "Ollama binds 127.0.0.1 port 11434 by default." That's loopback only. A request from a container arrives on the Docker bridge address (&lt;code&gt;172.17.0.1&lt;/code&gt;), not on loopback, so Ollama never sees it. The &lt;a href="https://docs.openwebui.com/troubleshooting/connection-error/" rel="noopener noreferrer"&gt;Open WebUI connection guide&lt;/a&gt; names the same cause first.&lt;/p&gt;

&lt;p&gt;To reproduce this, Ollama ran Ollama on the host network bound to &lt;code&gt;127.0.0.1:11434&lt;/code&gt;. From a container with &lt;code&gt;host-gateway&lt;/code&gt; set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ getent hosts host.docker.internal
172.17.0.1      host.docker.internal
$ curl http://host.docker.internal:11434
curl: (7) Failed to connect to host.docker.internal port 11434 after 0 ms: Couldn't connect to server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The name resolves correctly, but the connection is refused. Meanwhile &lt;code&gt;curl localhost:11434&lt;/code&gt; on the host works fine, which is why this is so confusing.&lt;/p&gt;

&lt;p&gt;The fix is to make Ollama listen on more than loopback. For the Linux systemd install, the FAQ's method is:&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 &lt;/span&gt;systemctl edit ollama.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"OLLAMA_HOST=0.0.0.0:11434"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&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 &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart ollama
ss &lt;span class="nt"&gt;-ltn&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;11434     &lt;span class="c"&gt;# should now show *:11434 or 0.0.0.0:11434, not 127.0.0.1:11434&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that change, the same &lt;code&gt;curl&lt;/code&gt; from the container returned &lt;code&gt;Ollama is running&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Be careful with &lt;code&gt;0.0.0.0&lt;/code&gt;. It means every interface, including your public one, and Ollama's API has no authentication. On a VPS, block 11434 from the internet in your cloud firewall. Or bind Ollama to the Docker bridge address only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"OLLAMA_HOST=172.17.0.1:11434"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was tested too. Containers on the default bridge and on a Compose network could both reach it through &lt;code&gt;host.docker.internal&lt;/code&gt;, and nothing outside the host could. The catch is that &lt;code&gt;localhost:11434&lt;/code&gt; on the host stops working, so the &lt;code&gt;ollama&lt;/code&gt; CLI needs &lt;code&gt;OLLAMA_HOST=172.17.0.1:11434&lt;/code&gt; set in your shell as well.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;172.17.0.1&lt;/code&gt; belongs to Docker's &lt;code&gt;docker0&lt;/code&gt; bridge, so it only exists while Docker is running. To have systemd start Ollama after Docker at boot, also put &lt;code&gt;After=docker.service&lt;/code&gt; under a &lt;code&gt;[Unit]&lt;/code&gt; section in the same override.&lt;/p&gt;

&lt;p&gt;UFW can also get in the way here. Traffic from a container to &lt;code&gt;172.17.0.1:11434&lt;/code&gt; is addressed to the host itself, not to a published container port, so UFW's normal input rules apply to it. If your UFW default is &lt;code&gt;deny incoming&lt;/code&gt; and requests from the container hang, a rule that allows only Docker's subnets to that port looks like this:&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 &lt;/span&gt;ufw allow from 172.16.0.0/12 to any port 11434 proto tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;172.16.0.0/12&lt;/code&gt; covers 172.16.0.0 to 172.31.255.255, where all of the test box's Docker networks were (172.17.0.0/16 and 172.27.0.0/16, for example). Yours may differ, so check the subnets you actually have with &lt;code&gt;docker network inspect bridge &amp;lt;your-compose-network&amp;gt; --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'&lt;/code&gt; and adjust the rule.&lt;/p&gt;

&lt;p&gt;There's also a shortcut. Open WebUI's connection guide suggests running the container with &lt;code&gt;--network=host&lt;/code&gt;, which puts it directly on the host's network. &lt;code&gt;127.0.0.1&lt;/code&gt; then points at the host, so a loopback-bound Ollama works without changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="nt"&gt;-v&lt;/span&gt; open-webui:/app/backend/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;OLLAMA_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://127.0.0.1:11434 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; open-webui &lt;span class="nt"&gt;--restart&lt;/span&gt; always ghcr.io/open-webui/open-webui:v0.12.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With host networking, &lt;code&gt;-p&lt;/code&gt; mappings don't apply, and the UI is on port 8080, not 3000. It's a fine shortcut on a single-purpose box, but you give up network isolation, and the app listens on all host interfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ollama in the same Compose file or on another machine
&lt;/h2&gt;

&lt;h3&gt;
  
  
  One Compose file for both
&lt;/h3&gt;

&lt;p&gt;This is the cleanest setup. Containers in the same Compose project share a user-defined network, and Docker resolves service names on those networks. So Open WebUI reaches Ollama at &lt;code&gt;http://ollama:11434&lt;/code&gt;. You don't need &lt;code&gt;host.docker.internal&lt;/code&gt;, and you don't need to publish Ollama's port to the host.&lt;/p&gt;

&lt;p&gt;This is the tested file. The test run published &lt;code&gt;"3000:8080"&lt;/code&gt;, and the localhost binding shown here is the only change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ollama&lt;/span&gt;&lt;span class="pi"&gt;:&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;ollama/ollama:0.40.4&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;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;ollama:/root/.ollama&lt;/span&gt;
    &lt;span class="c1"&gt;# no "ports:" needed, only Open WebUI talks to it&lt;/span&gt;

  &lt;span class="na"&gt;open-webui&lt;/span&gt;&lt;span class="pi"&gt;:&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;ghcr.io/open-webui/open-webui:v0.12.0&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;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:3000:8080"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;OLLAMA_BASE_URL=http://ollama:11434&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}&lt;/span&gt;   &lt;span class="c1"&gt;# from .env, see above&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;open-webui:/app/backend/data&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;ollama&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;ollama&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;open-webui&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&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;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;ollama ollama pull llama3.2   &lt;span class="c"&gt;# or any model from Ollama's library that fits your RAM&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;open-webui curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://ollama:11434
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't need to change &lt;code&gt;OLLAMA_HOST&lt;/code&gt; here. The official &lt;code&gt;ollama/ollama&lt;/code&gt; image already sets &lt;code&gt;OLLAMA_HOST=0.0.0.0:11434&lt;/code&gt; (&lt;code&gt;docker image inspect ollama/ollama&lt;/code&gt; shows it), so Ollama listens on the container network.&lt;/p&gt;

&lt;p&gt;The test box couldn't download from Ollama's model registry, so a small model was loaded (SmolLM2-135M-Instruct) from a GGUF file with &lt;code&gt;ollama create&lt;/code&gt; instead of &lt;code&gt;ollama pull&lt;/code&gt;. Open WebUI listed it once pointed at &lt;code&gt;http://ollama:11434&lt;/code&gt;, and a chat completion through Open WebUI's API worked on CPU.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;http://ollama:11434&lt;/code&gt; set, the connection check in the admin settings passed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhdxpqsip15wzttbklgkr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhdxpqsip15wzttbklgkr.webp" alt="Open WebUI admin Connections settings with the Edit Connection dialog for http://ollama:11434 open and a green Server connection verified notification" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things break this setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two separate Compose projects. If Ollama and Open WebUI live in different folders and files, they're on different networks, and &lt;code&gt;ollama&lt;/code&gt; won't resolve. Put both services in one file, or attach both to a shared external network (&lt;code&gt;docker network create ai&lt;/code&gt;, then declare it as &lt;code&gt;external: true&lt;/code&gt; in both files).&lt;/li&gt;
&lt;li&gt;The wrong service name. The hostname is the service key under &lt;code&gt;services:&lt;/code&gt; (&lt;code&gt;ollama&lt;/code&gt; above), not the image name. If you set &lt;code&gt;container_name&lt;/code&gt;, that name resolves too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't publish Ollama's port unless you need it. Docker's firewall docs explain that traffic to published container ports is routed in the NAT table before UFW's rules apply. So with &lt;code&gt;-p 11434:11434&lt;/code&gt;, Ollama is reachable from the internet even if &lt;code&gt;ufw status&lt;/code&gt; says otherwise. If you need it on the host, publish it to loopback only: &lt;code&gt;127.0.0.1:11434:11434&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ollama on another machine
&lt;/h3&gt;

&lt;p&gt;Treat it like the host setup, but over the LAN:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On the Ollama machine, set &lt;code&gt;OLLAMA_HOST=0.0.0.0:11434&lt;/code&gt; and restart Ollama.&lt;/li&gt;
&lt;li&gt;In that machine's firewall, allow port 11434 from the Open WebUI machine only.&lt;/li&gt;
&lt;li&gt;Set the URL to &lt;code&gt;http://192.168.1.50:11434&lt;/code&gt; (your IP) in Admin Panel &amp;gt; Settings &amp;gt; Connections, or with &lt;code&gt;OLLAMA_BASE_URL&lt;/code&gt; on a fresh install.&lt;/li&gt;
&lt;li&gt;Test with &lt;code&gt;docker exec open-webui curl -s http://192.168.1.50:11434&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You don't need &lt;code&gt;OLLAMA_ORIGINS&lt;/code&gt; for this. Open WebUI's backend calls Ollama server to server, and Ollama's FAQ describes &lt;code&gt;OLLAMA_ORIGINS&lt;/code&gt; as the setting for extra web origins, such as browser extensions.&lt;/p&gt;

&lt;p&gt;If you put Ollama behind its own reverse proxy, Ollama's FAQ shows that it expects the &lt;code&gt;Host&lt;/code&gt; header rewritten (&lt;code&gt;proxy_set_header Host localhost:11434;&lt;/code&gt; in its nginx example).&lt;/p&gt;

&lt;h2&gt;
  
  
  Changed OLLAMA_BASE_URL and nothing happened?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why the old URL sticks
&lt;/h3&gt;

&lt;p&gt;This is the trap that makes people think their fix is wrong. &lt;code&gt;OLLAMA_BASE_URL&lt;/code&gt; is a ConfigVar (the docs used to call these PersistentConfig). On first start, Open WebUI saves the value in its database. The env reference says values saved in the database take precedence, so on later starts the environment variable is ignored for that setting.&lt;/p&gt;

&lt;p&gt;The test confirmed it. It started with the wrong URL, then changed the compose file to &lt;code&gt;http://ollama:11434&lt;/code&gt; and recreated the container. &lt;code&gt;docker exec ... printenv OLLAMA_BASE_URL&lt;/code&gt; showed the new value, but Open WebUI's Ollama config still said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ENABLE_OLLAMA_API"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"OLLAMA_BASE_URLS"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:11434"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Three ways to apply the new URL
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Change it in the UI. This is the recommended option. Click your avatar, then Admin Panel &amp;gt; Settings &amp;gt; Connections, and edit the Ollama URL there. UI changes are saved to the database and take effect.&lt;/li&gt;
&lt;li&gt;Force a reset from the environment once. Add &lt;code&gt;RESET_CONFIG_ON_START=true&lt;/code&gt;, run &lt;code&gt;docker compose up -d&lt;/code&gt;, check that it worked, then remove the variable. In testing this switched the stored URL to the env value on the next start. It resets the stored config to your environment values, not just this one setting.&lt;/li&gt;
&lt;li&gt;Ignore the database for config. &lt;code&gt;ENABLE_PERSISTENT_CONFIG=false&lt;/code&gt; makes Open WebUI read env vars on every start. The catch is that settings changed in the admin UI then don't survive a restart.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Deleting the &lt;code&gt;open-webui&lt;/code&gt; volume also works, but it deletes your users and chats too. Avoid it.&lt;/p&gt;

&lt;p&gt;One more thing came up in testing. Right after adding a model to Ollama, a chat request through Open WebUI returned &lt;code&gt;{"detail":"Model not found"}&lt;/code&gt; until the model list had been refreshed. If a new model isn't showing, reload the model list or the page before you blame the connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Models and connections
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Why does Open WebUI show no models?
&lt;/h4&gt;

&lt;p&gt;Open WebUI can't reach Ollama at the URL it has saved, and the model list comes back empty without an error in the UI. Check the container log for &lt;code&gt;Cannot connect to host&lt;/code&gt;, then run &lt;code&gt;curl&lt;/code&gt; from inside the container as shown above. With Ollama on the host, the usual cause is Ollama listening on &lt;code&gt;127.0.0.1&lt;/code&gt; only.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why is OLLAMA_BASE_URL ignored after a restart of the container?
&lt;/h4&gt;

&lt;p&gt;Open WebUI stored the first value in its database, and the stored value wins. Change the URL under Admin Panel &amp;gt; Settings &amp;gt; Connections, or use &lt;code&gt;RESET_CONFIG_ON_START=true&lt;/code&gt; for one start.&lt;/p&gt;

&lt;h4&gt;
  
  
  Can Open WebUI and Ollama run on different machines?
&lt;/h4&gt;

&lt;p&gt;Yes. Make Ollama listen on the network (&lt;code&gt;OLLAMA_HOST=0.0.0.0:11434&lt;/code&gt;), allow port 11434 from the Open WebUI machine in the firewall, and point Open WebUI at &lt;code&gt;http://&amp;lt;ollama-ip&amp;gt;:11434&lt;/code&gt;. If &lt;code&gt;curl&lt;/code&gt; to that address from the Open WebUI container doesn't answer &lt;code&gt;Ollama is running&lt;/code&gt;, fix that first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Access and proxies
&lt;/h3&gt;

&lt;h4&gt;
  
  
  How do you open Open WebUI from other devices on your network?
&lt;/h4&gt;

&lt;p&gt;Open WebUI listens on port 8080 inside the container, and &lt;code&gt;-p&lt;/code&gt; decides where it appears on the host. This guide binds it to &lt;code&gt;127.0.0.1:3000&lt;/code&gt;, which only the server itself can reach. For other devices, put a reverse proxy in front, or bind to the server's LAN IP (&lt;code&gt;-p 192.168.1.10:3000:8080&lt;/code&gt;). With &lt;code&gt;--network=host&lt;/code&gt; there's no mapping, so it's port 8080 on every interface. Either way, create the admin account before anyone else can reach the page.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why do you get "Open WebUI backend required" through an SSH tunnel or proxy?
&lt;/h4&gt;

&lt;p&gt;Open WebUI's connection guide says the page loaded but its request to &lt;code&gt;/api/config&lt;/code&gt; on the same address failed. It's usually a port mapping or tunnel that doesn't lead to container port 8080, or a stale cached page. Run &lt;code&gt;curl http://localhost:3000/api/config&lt;/code&gt; through the tunnel. JSON back means the backend is reachable. Then try a private window. Behind a proxy, also turn on WebSocket support, which Open WebUI requires from v0.5.0 on, per the same guide. In Nginx Proxy Manager that's the Websockets Support toggle. The &lt;a href="https://deploylark.com/posts/n8n-docker-webhook-url-connection-lost/" rel="noopener noreferrer"&gt;n8n reverse proxy guide&lt;/a&gt; shows the matching headers for plain nginx.&lt;/p&gt;




&lt;p&gt;Originally published on DeployLark: &lt;a href="https://deploylark.com/posts/open-webui-cannot-connect-ollama-docker/" rel="noopener noreferrer"&gt;Open WebUI Can't Connect to Ollama in Docker: The Fixes&lt;/a&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>docker</category>
      <category>ollama</category>
      <category>ai</category>
    </item>
    <item>
      <title>Install Coolify on a VPS and Deploy Your First App</title>
      <dc:creator>DeployLark</dc:creator>
      <pubDate>Fri, 16 Oct 2026 06:42:35 +0000</pubDate>
      <link>https://dev.to/deploylark/install-coolify-on-a-vps-and-deploy-your-first-app-4pf8</link>
      <guid>https://dev.to/deploylark/install-coolify-on-a-vps-and-deploy-your-first-app-4pf8</guid>
      <description>&lt;p&gt;Coolify is a self-hosted alternative to Heroku, Vercel and Netlify. You install it on your own server, point it at a Git repository, and it builds the app, runs it in Docker, routes a domain to it and gets a certificate. On October 16, 2026, the &lt;a href="https://github.com/coollabsio/coolify" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; had about 63,000 stars, and the &lt;code&gt;coollabsio/coolify&lt;/code&gt; image had about 3.3 million pulls on &lt;a href="https://hub.docker.com/r/coollabsio/coolify" rel="noopener noreferrer"&gt;Docker Hub&lt;/a&gt;. Version 4.0.0, the first stable v4 release, came out on April 27, 2026, and 4.4.6 followed on October 9.&lt;/p&gt;

&lt;p&gt;This guide installs Coolify 4.4.6 with the official script, closes the hole a fresh install leaves open, deploys a small Node.js app from a public GitHub repository and adds a domain with HTTPS. Every step ran on Ubuntu 24.04.5 LTS. It ran as a container with its own Docker Engine and systemd, standing in for a VPS, so the server IPs and the HTTPS test differ from a real cloud server, and the guide points out where.&lt;/p&gt;

&lt;p&gt;Short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a fresh Ubuntu LTS server with at least 2 cores, 2 GB of RAM and 10 GB free. Run &lt;code&gt;curl -fsSL https://cdn.coollabs.io/coolify/install.sh | sudo bash -s 4.4.6&lt;/code&gt; to install a specific version.&lt;/li&gt;
&lt;li&gt;The dashboard is at &lt;code&gt;http://your-server-ip:8000&lt;/code&gt;, and the first person to open it becomes root. Create your account right away, or bind port 8000 to &lt;code&gt;127.0.0.1&lt;/code&gt; with a compose override and use an SSH tunnel.&lt;/li&gt;
&lt;li&gt;In the dashboard, pick This machine, then add a Public Git Repository with the right base directory and port, and click Deploy. My first build took 37 seconds.&lt;/li&gt;
&lt;li&gt;Add an &lt;code&gt;https://&lt;/code&gt; domain, redeploy, and Traefik requests a Let's Encrypt certificate. That needs a DNS record pointing at the server and port 80 open.&lt;/li&gt;
&lt;li&gt;Coolify updates itself every night by default. Turn that off if you want to stay on the version you tested.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Coolify installs and what you need
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What runs on the server
&lt;/h3&gt;

&lt;p&gt;Coolify isn't one container. The installer sets up Docker, then runs Coolify's own containers next to the apps you deploy:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo3gu37589i2bw0tt9g39.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo3gu37589i2bw0tt9g39.webp" alt="Diagram of Coolify on one Ubuntu 24.04 server. Your browser reaches the coolify container, which runs Coolify 4.4.6 on ports 8000, 6001 and 6002, via an SSH tunnel or a domain. The coolify container uses coolify-db and coolify-redis, and connects over SSH to host.docker.internal to drive Docker Engine 29.9.1 on the host, which builds and runs your app container (built by Railpack, listening on 3000). Visitors reach coolify-proxy, Traefik 3.7.15 on ports 80 and 443, which routes to the app by domain. Certificates come from the Let's Encrypt HTTP challenge on port 80." width="799" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;coolify&lt;/code&gt; is the dashboard and API. It also serves real-time updates on port 6001 and the web terminal on port 6002.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;coolify-db&lt;/code&gt; and &lt;code&gt;coolify-redis&lt;/code&gt; are PostgreSQL and Redis for Coolify's own data. The 4.4.6 compose file uses &lt;code&gt;postgres:15-alpine&lt;/code&gt; and &lt;code&gt;redis:7-alpine&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;coolify-proxy&lt;/code&gt; is Traefik. It owns ports 80 and 443 and sends each domain to the right app.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;coolify-sentinel&lt;/code&gt; collects server metrics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Coolify manages the server it runs on the same way it manages a remote one, over SSH. The installer generates a key, adds it to root's &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;, and the dashboard then logs in as root at &lt;code&gt;host.docker.internal&lt;/code&gt;. That's why it needs SSH running and root login with a key allowed. Ubuntu's default (&lt;code&gt;PermitRootLogin prohibit-password&lt;/code&gt;) is fine.&lt;/p&gt;

&lt;p&gt;After the setup below, with one small app deployed, &lt;code&gt;docker stats&lt;/code&gt; on the test server showed about 500 MB for Coolify's own containers: 428 MB for &lt;code&gt;coolify&lt;/code&gt;, 45 MB for the database, 22 MB for Traefik, 7 MB for Redis and 3 MB for Sentinel. The Railpack build container stayed running after the build and used another 549 MB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server requirements and ports
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://coolify.io/docs/start-with-self-hosted" rel="noopener noreferrer"&gt;installation docs&lt;/a&gt; ask for 2 CPU cores, 2 GB of RAM and 10 GB of free disk, on amd64 or arm64. They add that Coolify can run on smaller servers, "but this is not recommended", and that builds on the same server as Coolify need watching because high resource use can make the server unresponsive. Ubuntu should be an LTS release. The docs name 20.04, 22.04 and 24.04 and suggest the manual install if the script fails on a non-LTS release. Ubuntu 26.04 isn't on that list yet, which is why the test used 24.04.&lt;/p&gt;

&lt;p&gt;Use a fresh server. The script installs Docker Engine itself, rewrites &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; (it keeps a backup of an existing one) and expects ports 80 and 443 to be free. If you already run &lt;a href="https://deploylark.com/posts/nginx-proxy-manager-docker-ssl/" rel="noopener noreferrer"&gt;Nginx Proxy Manager&lt;/a&gt; or another proxy on 80 and 443, give Coolify its own server.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://coolify.io/docs/core/infrastructure/servers/firewall" rel="noopener noreferrer"&gt;firewall docs&lt;/a&gt; list the ports:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Used for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;SSH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80 and 443&lt;/td&gt;
&lt;td&gt;Your apps through Traefik, and Let's Encrypt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8000&lt;/td&gt;
&lt;td&gt;The dashboard at &lt;code&gt;http://server-ip:8000&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6001 and 6002&lt;/td&gt;
&lt;td&gt;Real-time updates and the web terminal when you use the IP address&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once the dashboard has its own domain through the proxy, the docs say you can close 8000, 6001 and 6002. Use your hosting provider's firewall for this. On Linux, &lt;a href="https://docs.docker.com/engine/network/packet-filtering-firewalls/" rel="noopener noreferrer"&gt;Docker publishes ports through its own firewall rules&lt;/a&gt;, so ufw rules don't stop traffic to a container port.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install Coolify with the script
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Run the installer with a pinned version
&lt;/h3&gt;

&lt;p&gt;SSH in as root or a sudo user and run the script. The plain command from the docs installs the newest release. Adding a version number after &lt;code&gt;bash -s&lt;/code&gt; installs that one instead, which the script supports and which is what this guide does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://cdn.coollabs.io/coolify/install.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash &lt;span class="nt"&gt;-s&lt;/span&gt; 4.4.6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To read the script before it runs as root, download it with &lt;code&gt;curl -fsSL https://cdn.coollabs.io/coolify/install.sh -o install.sh&lt;/code&gt;, look through it and run &lt;code&gt;sudo bash install.sh 4.4.6&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On the test server it finished in 1 minute 22 seconds. The output, with the server's addresses replaced by an example IP and the private addresses left out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  1/9  Installing required packages  curl wget git jq openssl            23.1s done
  2/9  Checking OpenSSH server configuration                              0.0s done
  3/9  Installing Docker Engine  v29.9.1                                 22.4s done
  4/9  Configuring Docker daemon  log rotation, address pools            1.4s done
  5/9  Downloading required files from CDN                                0.2s done
  6/9  Setting up environment variables                                   0.0s done
  7/9  Checking SSH key for localhost access  generated new key           0.0s done
  8/9  Pulling images  coolify, postgres, redis, helper               18.6s done
  9/9  Starting Coolify  v4.4.6                                          16.0s done
  Coolify is ready!  Installed in 1m 22s
  Public IPv4   http://203.0.113.10:8000
  ! Back up /data/coolify/source/.env to a safe place outside this server (e.g. a password manager).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 4 set log rotation (10 MB files, 3 per container) and an address pool of &lt;code&gt;10.0.0.0/8&lt;/code&gt; for Docker networks in &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;. If your VPS provider uses addresses in that range, the docs have a custom network option for the installer.&lt;/p&gt;

&lt;p&gt;Do what the last line says. &lt;code&gt;/data/coolify/source/.env&lt;/code&gt; holds the generated secrets, including &lt;code&gt;APP_KEY&lt;/code&gt;, which encrypts values in Coolify's database. The docs say you need it to restore Coolify from a backup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lock down port 8000 before anyone else finds it
&lt;/h3&gt;

&lt;p&gt;The dashboard is now live on port 8000 on every interface, and it has no account yet. The installation docs warn: "Anyone who reaches the registration page first can become the instance admin and gain root access to your server." Do one of these right away.&lt;/p&gt;

&lt;p&gt;The quick fix is to open &lt;code&gt;http://your-server-ip:8000&lt;/code&gt; and create your account (next section). The safer fix is to stop publishing the dashboard to the internet at all. Coolify reads a compose override from &lt;code&gt;/data/coolify/source/docker-compose.custom.yml&lt;/code&gt; and keeps it across upgrades. The installation docs show this example for binding the dashboard to localhost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;coolify&lt;/span&gt;&lt;span class="pi"&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:8000:8080"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That didn't work as written on 4.4.6. Compose adds override ports to the existing list instead of replacing it, so the merged config had both &lt;code&gt;8000&lt;/code&gt; on all interfaces and &lt;code&gt;127.0.0.1:8000&lt;/code&gt;. Applying it failed and left the dashboard stopped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;failed to bind host port 127.0.0.1:8000/tcp: address already in use
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose's &lt;code&gt;!override&lt;/code&gt; tag replaces the whole list. The &lt;a href="https://coolify.io/docs/core/infrastructure/servers/firewall" rel="noopener noreferrer"&gt;firewall docs&lt;/a&gt; already use it in their version of this override. This one worked, and it also keeps the real-time and terminal ports off the internet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;coolify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!override&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:8000:8080"&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:6001:6001"&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:6002:6002"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it with the command from the docs:&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/coolify/source
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker compose &lt;span class="nt"&gt;--env-file&lt;/span&gt; /data/coolify/source/.env &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.yml &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.prod.yml &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.custom.yml &lt;span class="se"&gt;\&lt;/span&gt;
  up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--pull&lt;/span&gt; always &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt; &lt;span class="nt"&gt;--force-recreate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, &lt;code&gt;docker port coolify&lt;/code&gt; listed all three ports on &lt;code&gt;127.0.0.1&lt;/code&gt; only. From your own computer, open a tunnel and use the dashboard at &lt;code&gt;http://localhost:8000&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-L&lt;/span&gt; 8000:127.0.0.1:8000 &lt;span class="nt"&gt;-L&lt;/span&gt; 6001:127.0.0.1:6001 &lt;span class="nt"&gt;-L&lt;/span&gt; 6002:127.0.0.1:6002 root@your-server-ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logging in through that tunnel worked, and real-time updates worked, because the browser reaches 6001 and 6002 through the same tunnel. You can also skip the open registration page completely. The docs describe &lt;code&gt;ROOT_USERNAME&lt;/code&gt;, &lt;code&gt;ROOT_USER_EMAIL&lt;/code&gt; and &lt;code&gt;ROOT_USER_PASSWORD&lt;/code&gt; variables that create the root account during the install.&lt;/p&gt;

&lt;h2&gt;
  
  
  First login and connecting the server
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create the root account
&lt;/h3&gt;

&lt;p&gt;The first visit shows the sign-up form, with a clear warning:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F04zk05u88drk1b2sszzg.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F04zk05u88drk1b2sszzg.webp" alt="Coolify 4.4.6 sign-up page titled Create the root account for this instance, with a Full instance access warning that says This first account becomes the root user, fields for name, email, password and password again, a hint to use at least 8 characters with uppercase, lowercase, number and symbol, and a Create Account button." width="560" height="650"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The password needs at least 8 characters with uppercase, lowercase, a number and a symbol. Use a password manager, and turn on two-factor authentication in your profile once you're in. This account can run commands as root on every server you connect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect this machine
&lt;/h3&gt;

&lt;p&gt;After sign-up, an onboarding wizard asks where to deploy. Pick This machine. Coolify then connects to the server over SSH as root with the key the installer created and checks that Docker works. On the test server, that check passed in a few seconds and the wizard moved on to creating a project. This guide accepts the default, "My first project", with a &lt;code&gt;production&lt;/code&gt; environment.&lt;/p&gt;

&lt;p&gt;During that step, Coolify also started &lt;code&gt;coolify-proxy&lt;/code&gt;, the Traefik container that will receive web traffic. Run &lt;code&gt;sudo docker ps&lt;/code&gt; afterwards and you should see &lt;code&gt;coolify&lt;/code&gt;, &lt;code&gt;coolify-db&lt;/code&gt;, &lt;code&gt;coolify-redis&lt;/code&gt;, &lt;code&gt;coolify-proxy&lt;/code&gt; and &lt;code&gt;coolify-sentinel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the wizard says the server isn't reachable, it's an SSH problem between the Coolify container and the host. Check that sshd is running, that root may log in with a key, and that the public key shown in the wizard is in &lt;code&gt;/root/.ssh/authorized_keys&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy your first app from Git
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pick the repository and build settings
&lt;/h3&gt;

&lt;p&gt;Open your project, go to the &lt;code&gt;production&lt;/code&gt; environment and click New resource. Under Applications, the first option is Public Git Repository. It needs no credentials. For private repositories, Coolify offers a deploy key or a GitHub App. The resource picker describes the GitHub App option as the one with automatic webhooks and pull request previews.&lt;/p&gt;

&lt;p&gt;This guide uses Coolify's own example repository, &lt;code&gt;https://github.com/coollabsio/coolify-examples&lt;/code&gt;, which holds many small apps in subfolders. After Check Repository, Coolify read the default branch and showed the build settings:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F768ros1sz7fkfkivit1g.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F768ros1sz7fkfkivit1g.webp" alt="Coolify Public Git repository form with the repository URL https://github.com/coollabsio/coolify-examples checked, and the build configuration below it: branch main, build pack Railpack, output type Web application, port 3000 and base directory /javascript/runtimes/nodejs, with a Continue button." width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build pack: Railpack was the default on 4.4.6. It detects the language and builds an image without a Dockerfile. Pick Dockerfile or Docker Compose if your repository has one.&lt;/li&gt;
&lt;li&gt;Base directory: the folder with the app. Here it's &lt;code&gt;/javascript/runtimes/nodejs&lt;/code&gt;, a small Fastify server.&lt;/li&gt;
&lt;li&gt;Port: the port the app listens on inside the container. This app uses 3000. Get this wrong and the app builds fine but the domain returns errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click Continue. Coolify creates the application and opens its settings page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy and check the result
&lt;/h3&gt;

&lt;p&gt;Click Deploy at the top. The Deployment Logs page shows each step:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F13q3md12etdi01pf563u.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F13q3md12etdi01pf563u.webp" alt="Coolify Deployment Logs page showing one successful manual deployment of commit 4c57d59 that took 00m 37s on localhost, with log lines: Docker 29.9.1 with BuildKit and Buildx detected, starting deployment of coollabsio/coolify-examples from the main branch, preparing the helper image coolify-helper:1.0.17, image not found so building a new image, generating a Railpack build plan, building the image with Railpack, rolling update started, new container started, old containers removed, rolling update completed." width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My first deployment took 37 seconds. Coolify cloned the commit, generated a Railpack build plan, built the image, then did a rolling update: start the new container, then remove the old one. Click Show Debug Logs to see the full build output when a build fails.&lt;/p&gt;

&lt;p&gt;Coolify also gave the app a generated &lt;code&gt;sslip.io&lt;/code&gt; address. In the lab that address didn't resolve to the server from the browser, so the test went through Traefik with the matching &lt;code&gt;Host&lt;/code&gt; header and got the app's response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"hello":"from nodejs #2"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app's status read "Running (no healthcheck)". Add a health check under Healthcheck (this app has a &lt;code&gt;/health&lt;/code&gt; route) so Coolify can tell when a new version is broken. If you'd rather run a ready-made app than build one, the same New resource page has Docker Compose and Docker image options, so the compose files from the &lt;a href="https://deploylark.com/posts/uptime-kuma-docker-setup/" rel="noopener noreferrer"&gt;Uptime Kuma&lt;/a&gt;, &lt;a href="https://deploylark.com/posts/self-host-umami-docker-postgresql/" rel="noopener noreferrer"&gt;Umami&lt;/a&gt; or &lt;a href="https://deploylark.com/posts/paperless-ngx-docker-compose-ocr/" rel="noopener noreferrer"&gt;Paperless-ngx&lt;/a&gt; guides are a starting point there too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domains, HTTPS and updates
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Add a domain with automatic HTTPS
&lt;/h3&gt;

&lt;p&gt;First, create a DNS A record (and an AAAA record if the server has working IPv6) for your domain, pointing at the server. Then open the app's Domains page and click Add Domain. Pick &lt;code&gt;https&lt;/code&gt;, enter the host name and the app's port, and save. Coolify then showed the domain twice:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh36iwxr1ry0oppl9x4l5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh36iwxr1ry0oppl9x4l5.webp" alt="Coolify Domains page for the app with three domains: the generated sslip.io address over http, plus https://hello.coolify-lab.test and https://www.hello.coolify-lab.test, both with HTTP to HTTPS protocol redirect, internal port 3000 and DNS status Not checked. A Changes pending badge appears at the top next to the Deploy button." width="800" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Coolify added the &lt;code&gt;www.&lt;/code&gt; version of the domain on its own, and it shows Changes pending. The new domain only takes effect after you redeploy. After clicking Deploy again, Traefik redirected plain HTTP to HTTPS with a &lt;code&gt;307 Temporary Redirect&lt;/code&gt; and served the app on HTTPS.&lt;/p&gt;

&lt;p&gt;The certificate step is the part that couldn't be finished in a lab. Traefik asks Let's Encrypt for a certificate using the HTTP challenge on port 80. The test domain ended in &lt;code&gt;.test&lt;/code&gt;, which Let's Encrypt never issues for, so the proxy log showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unable to obtain ACME certificate for domains ... Cannot issue for "hello.coolify-lab.test": Domain name does not end with a valid public suffix (TLD)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meanwhile, Traefik served its fallback certificate, with &lt;code&gt;subject=CN=TRAEFIK DEFAULT CERT&lt;/code&gt;. That's the same certificate you'll see on a real domain whenever the certificate request fails. The &lt;a href="https://coolify.io/docs/troubleshoot/dns-and-domains/lets-encrypt-not-working" rel="noopener noreferrer"&gt;Let's Encrypt troubleshooting page&lt;/a&gt; lists the usual causes: port 80 closed, a third-party proxy such as Cloudflare in front of the domain, or an AAAA record pointing at a server without working IPv6. The docs point to Servers, your server, Proxy, Logs in the dashboard for the proxy log. &lt;code&gt;sudo docker logs coolify-proxy&lt;/code&gt; on the server shows the same thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep Coolify updated
&lt;/h3&gt;

&lt;p&gt;Coolify updates itself by default. On a fresh 4.4.6 install, automatic updates were already on, which matches the default in Coolify's &lt;a href="https://github.com/coollabsio/coolify/blob/v4.4.6/database/migrations/2023_03_20_112814_create_instance_settings_table.php" rel="noopener noreferrer"&gt;instance settings migration&lt;/a&gt;, with an update frequency of &lt;code&gt;0 0 * * *&lt;/code&gt;, daily at midnight in the instance's time zone. It also means a server you installed with &lt;code&gt;4.4.6&lt;/code&gt; won't stay on 4.4.6.&lt;/p&gt;

&lt;p&gt;To control updates yourself, turn off Automatic updates under Settings, then Updates, and update by hand after reading the &lt;a href="https://github.com/coollabsio/coolify/releases" rel="noopener noreferrer"&gt;release notes&lt;/a&gt;. The &lt;a href="https://coolify.io/docs/core/instance-management/update" rel="noopener noreferrer"&gt;update docs&lt;/a&gt; describe two ways: the Upgrade Now button in the dashboard, or the upgrade script with a version number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://cdn.coollabs.io/coolify/upgrade.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash &lt;span class="nt"&gt;-s&lt;/span&gt; 4.4.6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;4.4.6&lt;/code&gt; with the version you want. The docs warn not to rerun &lt;code&gt;install.sh&lt;/code&gt; on an existing instance, because it resets ownership and permissions under &lt;code&gt;/data/coolify&lt;/code&gt;. They also say to check for running deployments first, since an update can make them fail. Back up &lt;code&gt;/data/coolify/source/.env&lt;/code&gt; before any update.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Server and setup
&lt;/h3&gt;

&lt;h4&gt;
  
  
  How much RAM does Coolify need?
&lt;/h4&gt;

&lt;p&gt;The docs ask for 2 GB of RAM and 2 CPU cores. On the test server, Coolify's own five containers used about 500 MB, and the build container added about 550 MB while it was running. That fits in 2 GB with a small app, but larger builds on the same server need more memory or swap.&lt;/p&gt;

&lt;h4&gt;
  
  
  Can Coolify run on the same server as your apps?
&lt;/h4&gt;

&lt;p&gt;Yes. That's what the This machine option does, and it's how the test app ran. Coolify's own containers and your apps share the server's CPU and RAM, so a heavy build or a busy app slows the dashboard too. The docs show the same pattern for production, with a 4 GB example server running more than 40 small services next to Coolify, with the apps built elsewhere.&lt;/p&gt;

&lt;h4&gt;
  
  
  Does Coolify update itself automatically?
&lt;/h4&gt;

&lt;p&gt;Yes, by default. My fresh install had automatic updates on, scheduled daily at midnight. Turn them off under Settings, then Updates, if you want to choose when to upgrade, and use the dashboard or &lt;code&gt;upgrade.sh&lt;/code&gt; with a version number.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dashboard and HTTPS
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Why does Coolify say "Cannot connect to real-time service"?
&lt;/h4&gt;

&lt;p&gt;Your browser can't reach port 6001 on the server. This message showed on every page while port 6001 wasn't reachable, and it went away once the browser could connect. Open 6001 and 6002 in the firewall, forward them through your SSH tunnel, or give the dashboard a domain so the proxy carries that traffic over 443.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why does a Coolify app show TRAEFIK DEFAULT CERT?
&lt;/h4&gt;

&lt;p&gt;Traefik couldn't get a Let's Encrypt certificate for that domain, so it served its built-in fallback. Check the proxy log for the ACME error. In the test the reason was a &lt;code&gt;.test&lt;/code&gt; domain. On a real domain, check that DNS points at the server, that port 80 is open, and that no third-party proxy sits in front during the HTTP challenge. Redeploy the app after you change its domain.&lt;/p&gt;

&lt;h4&gt;
  
  
  How do you open the dashboard on a domain instead of port 8000?
&lt;/h4&gt;

&lt;p&gt;Set Instance Domain under Settings to a domain that points at the server. Coolify team member Cinzya &lt;a href="https://github.com/coollabsio/coolify/issues/5968#issuecomment-2968317949" rel="noopener noreferrer"&gt;explained&lt;/a&gt;: "If you set up the &lt;code&gt;Instance Domain&lt;/code&gt; under &lt;code&gt;Settings&lt;/code&gt; you don't need to add the port at the end of the URL anymore, as you would access coolify through the proxy." Once that works, the firewall docs say you can close ports 8000, 6001 and 6002.&lt;/p&gt;




&lt;p&gt;Originally published on DeployLark: &lt;a href="https://deploylark.com/posts/coolify-install-vps-first-app/" rel="noopener noreferrer"&gt;Install Coolify on a VPS and Deploy Your First App&lt;/a&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>docker</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
