<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Caddy on K-Life Hack | Systems Architecture &amp; DevOps</title><link>https://klifehack.com/en/tags/caddy/</link><description>Recent content in Caddy on K-Life Hack | Systems Architecture &amp; DevOps</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sat, 03 Oct 2026 10:14:29 +0900</lastBuildDate><atom:link href="https://klifehack.com/en/tags/caddy/index.xml" rel="self" type="application/rss+xml"/><item><title>Self-Hosting Dify with Docker and Caddy: Setup and Security Hardening Design</title><link>https://klifehack.com/en/p/dify-self-hosting-docker-caddy-hardening/</link><pubDate>Sat, 03 Oct 2026 10:14:29 +0900</pubDate><guid>https://klifehack.com/en/p/dify-self-hosting-docker-caddy-hardening/</guid><description>&lt;p&gt;The demand for self-hosting Dify, an LLM orchestration tool, is rapidly growing to bypass functional limitations of the public cloud version (such as constraints on the number of applications that can be created and knowledge pipelines). However, operating in a local environment introduces availability challenges such as machine sleep, dynamic IPs, and firewall traversal. Establishing a practical production environment can be achieved by deploying Dify using Docker on a 24/7 Linux VPS, along with automated HTTPS and security hardening via Caddy.&lt;/p&gt;&#10;&lt;p&gt;This note explains the infrastructure design and concrete implementation procedures based on Dify &lt;code&gt;v1.17.x&lt;/code&gt; and Caddy &lt;code&gt;v2.11.x&lt;/code&gt; to securely expose the application to the outside world via a reverse proxy while ensuring host machine security.&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="1-system-architecture-design"&gt;1. System Architecture Design&#10;&lt;/h2&gt;&lt;p&gt;The network and container deployment topology in this environment is as follows:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;[ External Client / Webhook Source ]&#10; │&#10; │ https://dify.&amp;lt;domain&amp;gt; (Port 443)&#10; ▼&#10;┌────────────────────────────────── VPS Host ──────────────────────────────────┐&#10;│ │&#10;│ Caddy Reverse Proxy (Auto ACME SSL Management, Ports 80 / 443) │&#10;│ │ │&#10;│ │ reverse_proxy (via local loopback) │&#10;│ ▼ │&#10;│ 127.0.0.1:8080 │&#10;│ │ │&#10;│ │ Docker Port Mapping │&#10;│ ▼ │&#10;│ dify-nginx (Container Port :80) │&#10;│ ├─ web / api / worker / worker_beat │&#10;│ ├─ db_postgres / redis / weaviate │&#10;│ └─ plugin_daemon (127.0.0.1:5003) │&#10;│ │&#10;└──────────────────────────────────────────────────────────────────────────────┘&#10;Exposed External Ports: 80/tcp, 443/tcp, 443/udp (Caddy), SSH (Custom Port)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As a paramount security design decision, the containers comprising Dify (Nginx, PostgreSQL, Redis, Plugin Daemon, etc.) do not bind to the host&amp;rsquo;s global network interface (&lt;code&gt;0.0.0.0&lt;/code&gt;) at all, but bind exclusively to the loopback address (&lt;code&gt;127.0.0.1&lt;/code&gt;). The Caddy reverse proxy receives all incoming external traffic, terminates SSL/TLS, and forwards it internally.&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="2-provisioning-host-infrastructure"&gt;2. Provisioning Host Infrastructure&#10;&lt;/h2&gt;&lt;h3 id="2-1-updating-system-packages-and-installing-dependencies"&gt;2-1. Updating System Packages and Installing Dependencies&#10;&lt;/h3&gt;&lt;p&gt;Update packages on the host OS (assuming Ubuntu 22.04 LTS / 24.04 LTS) and install the required utility tools.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt update &amp;amp;amp;&amp;amp;amp; sudo apt upgrade -y&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt install -y git curl jq acl&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2-2-installing-and-verifying-docker-engine"&gt;2-2. Installing and Verifying Docker Engine&#10;&lt;/h3&gt;&lt;p&gt;If Docker Engine is not installed, install it using the official script. Verify that the Docker Compose version is &lt;code&gt;v2.24.0&lt;/code&gt; or higher (required for supporting the &lt;code&gt;!override&lt;/code&gt; syntax described later).&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker --version 2&amp;amp;gt;/dev/null &lt;span style="color:#f92672"&gt;||&lt;/span&gt; curl -fsSL https://get.docker.com | sudo sh&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose version&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;systemctl is-enabled docker&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2-3-creating-a-dedicated-system-user-and-setting-permissions"&gt;2-3. Creating a Dedicated System User and Setting Permissions&#10;&lt;/h3&gt;&lt;p&gt;To ensure security, avoid running containers as the &lt;code&gt;root&lt;/code&gt; user and create a dedicated &lt;code&gt;dify&lt;/code&gt; user for operation.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo adduser dify&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo usermod -aG docker dify&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo usermod -aG sudo dify&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2-4-allocating-swap-space-4gb"&gt;2-4. Allocating Swap Space (4GB)&#10;&lt;/h3&gt;&lt;p&gt;Dify runs multiple microservices (over 10 containers) simultaneously in the backend, consuming 3–5 GB of memory even when idle. To prevent kernel OOM (Out of Memory) killer invocation due to insufficient memory, explicitly allocate 4 GB of swap space.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo fallocate -l 4G /swapfile&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo chmod &lt;span style="color:#ae81ff"&gt;600&lt;/span&gt; /swapfile&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo mkswap /swapfile&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo swapon /swapfile&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#39;/swapfile none swap sw 0 0&amp;#39;&lt;/span&gt; | sudo tee -a /etc/fstab&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo sysctl vm.swappiness&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#39;vm.swappiness=10&amp;#39;&lt;/span&gt; | sudo tee /etc/sysctl.d/99-swappiness.conf&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;free -h&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="3-deploying-dify-orchestration"&gt;3. Deploying Dify Orchestration&#10;&lt;/h2&gt;&lt;p&gt;The following tasks should be executed after switching to the newly created &lt;code&gt;dify&lt;/code&gt; user.&lt;/p&gt;&#10;&lt;h3 id="3-1-cloning-the-repository-and-configuring-environment-encryption"&gt;3-1. Cloning the Repository and Configuring Environment Encryption&#10;&lt;/h3&gt;&lt;p&gt;Dynamically retrieve the latest release version via the GitHub API and clone the repository.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd ~&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git clone --branch &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;curl -s https://api.github.com/repos/langgenius/dify/releases/latest | jq -r .tag_name&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; https://github.com/langgenius/dify.git&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd ~/dify/docker&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cp .env.example .env&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;chmod &lt;span style="color:#ae81ff"&gt;600&lt;/span&gt; .env&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Replace the initial passwords and encryption keys in the &lt;code&gt;.env&lt;/code&gt; file with high-entropy random strings. Running with default values (such as &lt;code&gt;difyai123456&lt;/code&gt;) is strictly prohibited as it significantly increases the risk of unauthorized external access.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Generate key for session token encryption&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sed -i &lt;span style="color:#e6db74"&gt;&amp;#34;s|^SECRET_KEY=.*|SECRET_KEY=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;openssl rand -base64 42&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;|&amp;#34;&lt;/span&gt; .env&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Generate passwords for PostgreSQL and Redis&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DBP&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;openssl rand -hex 24&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RP&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;openssl rand -hex 24&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sed -i &lt;span style="color:#e6db74"&gt;&amp;#34;s|^DB_PASSWORD=.*|DB_PASSWORD=&lt;/span&gt;$DBP&lt;span style="color:#e6db74"&gt;|&amp;#34;&lt;/span&gt; .env&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sed -i &lt;span style="color:#e6db74"&gt;&amp;#34;s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=&lt;/span&gt;$RP&lt;span style="color:#e6db74"&gt;|&amp;#34;&lt;/span&gt; .env&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Apply Redis password to Celery Broker URL&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sed -i &lt;span style="color:#e6db74"&gt;&amp;#34;s|^CELERY_BROKER_URL=.*|CELERY_BROKER_URL=redis://:&lt;/span&gt;$RP&lt;span style="color:#e6db74"&gt;@redis:6379/1|&amp;#34;&lt;/span&gt; .env&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="3-2-restricting-port-binding-to-loopback"&gt;3-2. Restricting Port Binding to Loopback&#10;&lt;/h3&gt;&lt;p&gt;By default in Docker, performing port mappings opens ports across all host interfaces (&lt;code&gt;0.0.0.0&lt;/code&gt;), which bypasses host-side firewalls like UFW. To prevent this, explicitly add a &lt;code&gt;127.0.0.1:&lt;/code&gt; prefix to the exposed port configurations in &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sed -i &lt;span style="color:#e6db74"&gt;&amp;#39;s|^NGINX_PORT=.*|NGINX_PORT=80|&amp;#39;&lt;/span&gt; .env&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sed -i &lt;span style="color:#e6db74"&gt;&amp;#39;s|^NGINX_SSL_PORT=.*|NGINX_SSL_PORT=443|&amp;#39;&lt;/span&gt; .env&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sed -i &lt;span style="color:#e6db74"&gt;&amp;#39;s|^EXPOSE_NGINX_PORT=.*|EXPOSE_NGINX_PORT=127.0.0.1:8080|&amp;#39;&lt;/span&gt; .env&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sed -i &lt;span style="color:#e6db74"&gt;&amp;#39;s|^EXPOSE_NGINX_SSL_PORT=.*|EXPOSE_NGINX_SSL_PORT=127.0.0.1:8443|&amp;#39;&lt;/span&gt; .env&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sed -i &lt;span style="color:#e6db74"&gt;&amp;#39;s|^EXPOSE_PLUGIN_DEBUGGING_PORT=.*|EXPOSE_PLUGIN_DEBUGGING_PORT=5003|&amp;#39;&lt;/span&gt; .env&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="3-3-plugin-daemon-binding-restriction-docker-compose-override"&gt;3-3. Plugin Daemon Binding Restriction (Docker Compose Override)&#10;&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;plugin_daemon&lt;/code&gt; service is configured by default to bind to &lt;code&gt;0.0.0.0:5003&lt;/code&gt;. To safely override this, create &lt;code&gt;docker-compose.override.yaml&lt;/code&gt; and restrict it to the loopback address.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ~/dify/docker/docker-compose.override.yaml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;services&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;plugin_daemon&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ports&lt;/span&gt;: !&lt;span style="color:#ae81ff"&gt;override&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#34;127.0.0.1:5003:5003&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="3-4-starting-containers-and-initial-health-check"&gt;3-4. Starting Containers and Initial Health Check&#10;&lt;/h3&gt;&lt;p&gt;Once configured, start the containers in the background.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose up -d&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After startup, verify via local loopback whether the API server responds normally.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sleep &lt;span style="color:#ae81ff"&gt;30&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -s http://127.0.0.1:8080/console/api/setup&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If operating correctly, it returns a JSON response of &lt;code&gt;{&amp;ldquo;step&amp;rdquo;:&amp;ldquo;not_started&amp;rdquo;,&amp;ldquo;setup_at&amp;rdquo;:null}&lt;/code&gt;.&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="4-setting-up-https-reverse-proxy-with-caddy"&gt;4. Setting Up HTTPS Reverse Proxy with Caddy&#10;&lt;/h2&gt;&lt;p&gt;Use Caddy to automatically acquire SSL/TLS certificates from Let&amp;rsquo;s Encrypt and terminate HTTPS communication.&lt;/p&gt;&#10;&lt;h3 id="4-1-installing-caddy"&gt;4-1. Installing Caddy&#10;&lt;/h3&gt;&lt;p&gt;Add the official repository and install Caddy.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -1sLf &lt;span style="color:#e6db74"&gt;&amp;#39;https://dl.cloudsmith.io/public/caddy/stable/gpg.key&amp;#39;&lt;/span&gt; | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -1sLf &lt;span style="color:#e6db74"&gt;&amp;#39;https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt&amp;#39;&lt;/span&gt; | sudo tee /etc/apt/sources.list.d/caddy-stable.list&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt update &amp;amp;amp;&amp;amp;amp; sudo apt install -y caddy&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="4-2-configuring-caddyfile"&gt;4-2. Configuring Caddyfile&#10;&lt;/h3&gt;&lt;p&gt;Edit &lt;code&gt;/etc/caddy/Caddyfile&lt;/code&gt; to route traffic from the domain to internal &lt;code&gt;127.0.0.1:8080&lt;/code&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-caddy" data-lang="caddy"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /etc/caddy/Caddyfile&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dify.example.com {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;reverse_proxy&lt;/span&gt; 127.0.0.1:&lt;span style="color:#ae81ff"&gt;8080&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note: Replace &lt;code&gt;dify.example.com&lt;/code&gt; with your domain configured in DNS to point to the VPS public IP.&lt;/p&gt;&#10;&lt;p&gt;Check the configuration file syntax, and if there are no issues, reload the Caddy service.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo caddy validate --config /etc/caddy/Caddyfile&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo systemctl reload caddy&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="5-troubleshooting"&gt;5. Troubleshooting&#10;&lt;/h2&gt;&lt;p&gt;The following section details representative friction points frequently encountered during the self-hosted environment setup and operational phases, along with their resolutions.&lt;/p&gt;&#10;&lt;h3 id="5-1-ufw-firewall-bypass-issue-by-docker"&gt;5-1. UFW (Firewall) Bypass Issue by Docker&#10;&lt;/h3&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;b&gt;Symptom&lt;/b&gt;: External direct access to &lt;code&gt;http://&amp;lt;VPS_IP&amp;gt;:8080&lt;/code&gt; remains possible despite configuring &lt;code&gt;sudo ufw deny 8080/tcp&lt;/code&gt; on the host.&lt;/li&gt;&#10;&lt;li&gt;&lt;b&gt;Cause&lt;/b&gt;: Docker directly manipulates host &lt;code&gt;iptables&lt;/code&gt; (Netfilter) rules for container routing, causing packets to reach containers with higher priority than UFW filtering rules.&lt;/li&gt;&#10;&lt;li&gt;&lt;b&gt;Resolution&lt;/b&gt;: Explicitly specify the IP address in &lt;code&gt;EXPOSE_NGINX_PORT&lt;/code&gt; within &lt;code&gt;.env&lt;/code&gt;, such as &lt;code&gt;127.0.0.1:8080&lt;/code&gt;. This ensures containers bind only to the loopback interface, physically blocking direct external access.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="5-2-database-authentication-error-after-initial-launch"&gt;5-2. Database Authentication Error After Initial Launch&#10;&lt;/h3&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;b&gt;Symptom&lt;/b&gt;: After changing &lt;code&gt;DB_PASSWORD&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; post-deployment, the &lt;code&gt;api&lt;/code&gt; container enters a crash loop outputting &lt;code&gt;FATAL: password authentication failed for user &amp;ldquo;postgres&amp;rdquo;&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;b&gt;Cause&lt;/b&gt;: The PostgreSQL container initializes the database using the password from &lt;code&gt;.env&lt;/code&gt; only on initial startup (when the volume is empty). Changing the value in &lt;code&gt;.env&lt;/code&gt; after startup does not update the password stored within the database inside the container, causing a mismatch.&lt;/li&gt;&#10;&lt;li&gt;&lt;b&gt;Resolution&lt;/b&gt;: To change the password, execute an &lt;code&gt;ALTER USER&lt;/code&gt; statement directly inside the PostgreSQL container to update the database-side password, or destroy the data and reinitialize (note: all data will be lost).&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="5-3-connection-refusal-for-asynchronous-tasks-celery"&gt;5-3. Connection Refusal for Asynchronous Tasks (Celery)&#10;&lt;/h3&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;b&gt;Symptom&lt;/b&gt;: Document upload or indexing tasks to Knowledge (RAG) remain stuck in the &amp;lsquo;in progress&amp;rsquo; state.&lt;/li&gt;&#10;&lt;li&gt;&lt;b&gt;Cause&lt;/b&gt;: When changing the Redis password (&lt;code&gt;REDIS_PASSWORD&lt;/code&gt;), the connection password contained in &lt;code&gt;CELERY_BROKER_URL&lt;/code&gt; was not synchronized, preventing asynchronous workers from fetching jobs from the queue.&lt;/li&gt;&#10;&lt;li&gt;&lt;b&gt;Resolution&lt;/b&gt;: Ensure that &lt;code&gt;CELERY_BROKER_URL&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; matches the format &lt;code&gt;redis://:&amp;lt;REDIS_PASSWORD&amp;gt;@redis:6379/1&lt;/code&gt; and exactly aligns with the current &lt;code&gt;REDIS_PASSWORD&lt;/code&gt;, then restart the containers.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="5-4-502-bad-gateway-caching-by-nginx-container"&gt;5-4. &amp;ldquo;502 Bad Gateway&amp;rdquo; Caching by Nginx Container&#10;&lt;/h3&gt;&lt;ul&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;b&gt;Symptom&lt;/b&gt;: During &lt;code&gt;api&lt;/code&gt; container restart or immediately after an update, &lt;code&gt;502 Bad Gateway&lt;/code&gt; continues to display in the browser despite Caddy functioning properly.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;b&gt;Cause&lt;/b&gt;: The Dify frontend Nginx container may be caching an old IP address of the upstream &lt;code&gt;api&lt;/code&gt; container, or name resolution failed before &lt;code&gt;api&lt;/code&gt; finished starting up, halting routing.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;b&gt;Resolution&lt;/b&gt;: Confirm that the &lt;code&gt;api&lt;/code&gt; container is fully operational, then restart only the Nginx container independently.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose restart nginx&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="6-operational-verification"&gt;6. Operational Verification&#10;&lt;/h2&gt;&lt;p&gt;After deployment is complete, execute the following verification commands to confirm that the system is operating securely as designed.&lt;/p&gt;&#10;&lt;h3 id="6-1-checking-container-operational-status"&gt;6-1. Checking Container Operational Status&#10;&lt;/h3&gt;&lt;p&gt;Verify that all containers are in the &lt;code&gt;Up&lt;/code&gt; (or &lt;code&gt;Up (healthy)&lt;/code&gt;) status.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ docker compose ps&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dify-api-1 langgenius/dify-api:0.15.3 &amp;#34;/bin/sh -c &amp;#39;./entry…&amp;#34; api 2 hours ago Up 2 hours (healthy) &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dify-db-1 postgres:15-alpine &amp;#34;docker-entrypoint.s…&amp;#34; db 2 hours ago Up 2 hours (healthy) 5432/tcp&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dify-nginx-1 nginx:1.25-alpine &amp;#34;/docker-entrypoint.…&amp;#34; nginx 2 hours ago Up 2 hours 127.0.0.1:8080-&amp;amp;gt;80/tcp, 127.0.0.1:8443-&amp;amp;gt;443/tcp&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dify-plugin_daemon langgenius/dify-plugin-daemon:0.15 &amp;#34;/entrypoint.sh&amp;#34; plugin_daemon 2 hours ago Up 2 hours (healthy) 127.0.0.1:5003-&amp;amp;gt;5003/tcp&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dify-redis-1 redis:7.2-alpine &amp;#34;docker-entrypoint.s…&amp;#34; redis 2 hours ago Up 2 hours (healthy) 6379/tcp&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dify-sandbox-1 langgenius/dify-sandbox:0.5.2 &amp;#34;/main&amp;#34; sandbox 2 hours ago Up 2 hours (healthy) &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dify-web-1 langgenius/dify-web:0.15.3 &amp;#34;/bin/sh -c &amp;#39;./entry…&amp;#34; web 2 hours ago Up 2 hours (healthy) &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dify-weaviate-1 semitechnologies/weaviate:1.19.0 &amp;#34;/bin/weaviate --hos…&amp;#34; weaviate 2 hours ago Up 2 hours (healthy) &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dify-worker-1 langgenius/dify-api:0.15.3 &amp;#34;/bin/sh -c &amp;#39;./entry…&amp;#34; worker 2 hours ago Up 2 hours (healthy)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="6-2-auditing-host-listening-ports"&gt;6-2. Auditing Host Listening Ports&#10;&lt;/h3&gt;&lt;p&gt;Verify that externally exposed ports are restricted as designed to SSH (e.g., &lt;code&gt;2022&lt;/code&gt;) and Caddy (&lt;code&gt;80&lt;/code&gt;, &lt;code&gt;443&lt;/code&gt;) only.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ sudo ss -tulnp | grep -vE &amp;#39;127\.0\.0\.|::1|%lo&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tcp LISTEN 0 4096 0.0.0.0:80 0.0.0.0:* users:((&amp;#34;caddy&amp;#34;,pid=1024,fd=6))&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tcp LISTEN 0 4096 0.0.0.0:443 0.0.0.0:* users:((&amp;#34;caddy&amp;#34;,pid=1024,fd=7))&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tcp LISTEN 0 128 0.0.0.0:2022 0.0.0.0:* users:((&amp;#34;sshd&amp;#34;,pid=821,fd=3))&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;udp UNCONN 0 0 0.0.0.0:443 0.0.0.0:* users:((&amp;#34;caddy&amp;#34;,pid=1024,fd=8))&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Verify that ports such as &lt;code&gt;8080&lt;/code&gt;, &lt;code&gt;5003&lt;/code&gt;, and &lt;code&gt;5432&lt;/code&gt; are not exposed to &lt;code&gt;0.0.0.0&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="6-3-end-to-end-https-connection-verification"&gt;6-3. End-to-End HTTPS Connection Verification&#10;&lt;/h3&gt;&lt;p&gt;Test from an external client machine whether HTTPS communication via Caddy terminates normally and reaches the Dify setup endpoint.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ curl -I https://dify.example.com/console/api/setup&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;HTTP/2 200 &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;alt-svc: h3=&amp;#34;:443&amp;#34;; ma=2592000&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;content-type: application/json; charset=utf-8&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;date: Sat, 03 Oct 2026 14:22:15 GMT&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;server: Caddy&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;server: nginx&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If &lt;code&gt;server: Caddy&lt;/code&gt; and &lt;code&gt;server: nginx&lt;/code&gt; appear sequentially in the response headers and a status code of &lt;code&gt;200&lt;/code&gt; is returned, the reverse proxy chain is functioning correctly.&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="7-lifecycle-dynamics-and-container-updates"&gt;7. Lifecycle Dynamics and Container Updates&#10;&lt;/h2&gt;&lt;p&gt;Temporary service downtime occurs when containers are recreated due to Dify version upgrades or configuration changes. When the connection to the backend (&lt;code&gt;127.0.0.1:8080&lt;/code&gt;) is disconnected, Caddy automatically returns &lt;code&gt;502 Bad Gateway&lt;/code&gt; to the client.&lt;/p&gt;&#10;&lt;p&gt;To partially achieve rolling updates (near zero-downtime switching), pre-pulling new images prior to stopping containers is an effective approach to minimize the time required for container recreation and startup.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd ~/dify/docker&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git pull origin main&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose pull&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose up -d --remove-orphans&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This eliminates downtime caused by image download times and compresses service interruption to a few seconds to tens of seconds required for container restarts.&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="operational-notes"&gt;Operational Notes&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;💡 &lt;b&gt;Backup Automation&lt;/b&gt;: The &lt;code&gt;dify-db-1&lt;/code&gt; (PostgreSQL) data volume and &lt;code&gt;.env&lt;/code&gt; file hold all system configurations and user data. Registering a &lt;code&gt;cron&lt;/code&gt; job to periodically run &lt;code&gt;pg_dump&lt;/code&gt; and offload backups to secure off-host storage is strongly recommended.&lt;/li&gt;&#10;&lt;li&gt;🛠️ &lt;b&gt;Log Rotation&lt;/b&gt;: In Docker default configurations, container standard output logs expand indefinitely, consuming disk space. Configure maximum size limits for the &lt;code&gt;json-file&lt;/code&gt; driver in &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; (e.g., &lt;code&gt;max-size: &amp;ldquo;10m&amp;rdquo;&lt;/code&gt;).&lt;/li&gt;&#10;&lt;li&gt;⚠️ &lt;b&gt;API Key Lifecycle&lt;/b&gt;: API keys for external LLMs (OpenAI, Anthropic, etc.) integrated within Dify are encrypted using &lt;code&gt;SECRET_KEY&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; and stored in the database. Changing this key breaks all existing external integrations; therefore, changing &lt;code&gt;SECRET_KEY&lt;/code&gt; after going into production is strictly prohibited.&lt;/domain&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item></channel></rss>