<?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: Arct Cloud</title>
    <description>The latest articles on DEV Community by Arct Cloud (arctcloud).</description>
    <link>https://dev.to/arctcloud</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%2Forganization%2Fprofile_image%2F15322%2Fcb8efacf-7a0d-41eb-872a-5174aad3151e.png</url>
      <title>DEV Community: Arct Cloud</title>
      <link>https://dev.to/arctcloud</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arctcloud"/>
    <language>en</language>
    <item>
      <title>How to Install Docker and Docker Compose on Ubuntu 24.04 and 26.04</title>
      <dc:creator>Tunç</dc:creator>
      <pubDate>Thu, 15 Oct 2026 13:36:59 +0000</pubDate>
      <link>https://dev.to/arctcloud/how-to-install-docker-and-docker-compose-on-ubuntu-2404-and-2604-co2</link>
      <guid>https://dev.to/arctcloud/how-to-install-docker-and-docker-compose-on-ubuntu-2404-and-2604-co2</guid>
      <description>&lt;p&gt;Originally published by Arct Cloud: &lt;a href="https://www.arct.cloud/tutorials/install-docker-ubuntu" rel="noopener noreferrer"&gt;How to Install Docker and Docker Compose on Ubuntu 24.04 and 26.04&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This article was prepared with AI assistance. Product references were removed and formatting adjusted for this DEV edition.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Install five packages from Docker's apt repository: &lt;code&gt;docker-ce&lt;/code&gt;, &lt;code&gt;docker-ce-cli&lt;/code&gt;, &lt;code&gt;containerd.io&lt;/code&gt;, &lt;code&gt;docker-buildx-plugin&lt;/code&gt; and &lt;code&gt;docker-compose-plugin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The same steps work on Ubuntu 24.04 LTS and 26.04 LTS and install Docker Engine 29.8.2 and Docker Compose 5.6.0 (October 2026).&lt;/li&gt;
&lt;li&gt;The repository key goes in &lt;code&gt;/etc/apt/keyrings/docker.asc&lt;/code&gt; and the source in &lt;code&gt;/etc/apt/sources.list.d/docker.sources&lt;/code&gt;, which replace the older &lt;code&gt;apt-key&lt;/code&gt; and &lt;code&gt;docker.list&lt;/code&gt; steps.&lt;/li&gt;
&lt;li&gt;Members of the &lt;code&gt;docker&lt;/code&gt; group get root-level access to the host, so add only your own sudo user.&lt;/li&gt;
&lt;li&gt;Docker-published ports bypass UFW. Bind containers to &lt;code&gt;127.0.0.1&lt;/code&gt; and put a reverse proxy on ports 80 and 443.&lt;/li&gt;
&lt;li&gt;Cap container logs in &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; at 3 files of 10 MB per container.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Applies to:&lt;/strong&gt; Ubuntu 24.04 LTS and 26.04 LTS · Docker Engine 29.8.2 · Docker Compose 5.6.0 · checked October 2026&lt;/p&gt;

&lt;p&gt;Docker Engine is the open-source container runtime that builds and runs containers on Linux. Docker Compose is the Docker CLI plugin that starts multi-container apps from one &lt;code&gt;compose.yaml&lt;/code&gt; file. This tutorial shows how to install Docker on Ubuntu 24.04 or 26.04 from Docker's official apt repository, then covers the &lt;code&gt;docker&lt;/code&gt; group, log rotation, the UFW port bypass and a first Compose stack. On a headless server, install Docker Engine. Docker Desktop is a graphical app for desktop computers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A VPS running Ubuntu 24.04 LTS or 26.04 LTS (64-bit) with SSH access.&lt;/li&gt;
&lt;li&gt;A non-root user with sudo privileges who logs in with an SSH key. To make a key, see &lt;a href="https://www.arct.cloud/tutorials/generate-ssh-key" rel="noopener noreferrer"&gt;How to Generate an SSH Key on Windows, macOS, and Linux&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;At least 1 GB of free disk: the Docker packages list about 365 MB of installed size in Docker's repository index. Docker's install docs set no RAM minimum, so size RAM for the containers you plan to run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you log in as root with an SSH key, create the sudo user once and copy your key to it. &lt;code&gt;adduser&lt;/code&gt; asks for a password, which &lt;code&gt;sudo&lt;/code&gt; uses later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adduser deploy
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;deploy
&lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 700 &lt;span class="nt"&gt;-o&lt;/span&gt; deploy &lt;span class="nt"&gt;-g&lt;/span&gt; deploy /home/deploy/.ssh
&lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 600 &lt;span class="nt"&gt;-o&lt;/span&gt; deploy &lt;span class="nt"&gt;-g&lt;/span&gt; deploy ~/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log out and connect again with &lt;code&gt;ssh deploy@YOUR_SERVER_IP&lt;/code&gt;. Every command from Step 1 on runs as &lt;code&gt;deploy&lt;/code&gt;. Replace values written in capitals, such as &lt;code&gt;YOUR_SERVER_IP&lt;/code&gt;, with your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Remove conflicting packages
&lt;/h2&gt;

&lt;p&gt;Ubuntu's own &lt;code&gt;docker.io&lt;/code&gt;, &lt;code&gt;docker-compose-v2&lt;/code&gt; and &lt;code&gt;containerd&lt;/code&gt; packages conflict with Docker's packages. Remove any that are installed:&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;apt remove &lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--get-selections&lt;/span&gt; docker.io docker-compose &lt;span class="se"&gt;\&lt;/span&gt;
  docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a fresh server, &lt;code&gt;dpkg&lt;/code&gt; finds no matching packages and apt removes nothing. Data in &lt;code&gt;/var/lib/docker&lt;/code&gt; stays on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add Docker's apt repository
&lt;/h2&gt;

&lt;p&gt;Docker signs its packages with its own key. Download the key into the apt keyrings directory:&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;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ca-certificates curl
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings
&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/ubuntu/gpg &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.asc
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the repository source. The command fills in your release codename and architecture:&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 tee&lt;/span&gt; /etc/apt/sources.list.d/docker.sources &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UBUNTU_CODENAME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
Components: stable
Architectures: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
Signed-By: /etc/apt/keyrings/docker.asc
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output looks similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: noble
Components: stable
Architectures: amd64
Signed-By: /etc/apt/keyrings/docker.asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Ubuntu 26.04 the &lt;code&gt;Suites&lt;/code&gt; line reads &lt;code&gt;resolute&lt;/code&gt;. Refresh the package index:&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;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker's docs do not recommend the &lt;code&gt;get.docker.com&lt;/code&gt; convenience script for production servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Install Docker on Ubuntu with the Compose plugin
&lt;/h2&gt;

&lt;p&gt;Install the five packages in one command, and type &lt;code&gt;Y&lt;/code&gt; when apt asks to continue:&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;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;apt also installs &lt;code&gt;docker-ce-rootless-extras&lt;/code&gt; and recommended tools such as &lt;code&gt;git&lt;/code&gt; and &lt;code&gt;pigz&lt;/code&gt; when they are missing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;What it installs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker-ce&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Docker Engine, the &lt;code&gt;dockerd&lt;/code&gt; daemon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker-ce-cli&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;docker&lt;/code&gt; command&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;containerd.io&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;containerd and runc, the runtime under Docker Engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker-buildx-plugin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker buildx&lt;/code&gt;, the BuildKit image builder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker-compose-plugin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On Ubuntu, the Docker service starts after the install and on every boot. Check both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl is-active docker
systemctl is-enabled docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;active
enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the installed versions:&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="nt"&gt;--version&lt;/span&gt;
docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output looks similar to this. Your versions may be newer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker version 29.8.2, build 7fc2dff
Docker Compose version v5.6.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Run the hello-world test
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;hello-world&lt;/code&gt; image pulls from Docker Hub, prints a message and exits:&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;docker run hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unable to find image 'hello-world:latest' locally
...
Hello from Docker!
This message shows that your installation appears to be working correctly.
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Run Docker without sudo
&lt;/h2&gt;

&lt;p&gt;The Docker daemon listens on &lt;code&gt;/var/run/docker.sock&lt;/code&gt;, which only root and the &lt;code&gt;docker&lt;/code&gt; group can open. The package creates the group with no members. Add your user:&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;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;docker&lt;/code&gt; group member can start a container that mounts any host directory, which is root-level access. Add only users you would also give sudo. Log out of SSH and back in so the group applies, then check:&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;id&lt;/span&gt; &lt;span class="nt"&gt;-nG&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The group list must include &lt;code&gt;docker&lt;/code&gt;. For a daemon that runs without root, Docker documents a separate rootless mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Configure the Docker daemon
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; sets defaults for new containers. This file caps logs and binds published ports to loopback unless a compose file names another address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/docker
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/docker/daemon.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &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;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "ip": "127.0.0.1",
  "default-network-opts": {
    "bridge": {
      "com.docker.network.bridge.host_binding_ipv4": "127.0.0.1"
    }
  }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;log-opts&lt;/code&gt;: the &lt;code&gt;json-file&lt;/code&gt; driver has no size limit by default. These values keep 3 files of 10 MB per container, written as quoted strings.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ip&lt;/code&gt;: ports published on the default bridge network without an address bind to &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;default-network-opts&lt;/code&gt;: the same default for user-defined networks, including the ones Compose creates.&lt;/li&gt;
&lt;li&gt;A container that must answer on the internet, such as a reverse proxy container, needs an explicit address in its port mapping: &lt;code&gt;"0.0.0.0:443:443"&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the file already existed, merge these keys into it. Validate it, then restart Docker:&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;dockerd &lt;span class="nt"&gt;--validate&lt;/span&gt; &lt;span class="nt"&gt;--config-file&lt;/span&gt; /etc/docker/daemon.json
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;configuration OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The settings apply to containers and networks created after the restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Enable UFW and keep container ports private
&lt;/h2&gt;

&lt;p&gt;The Uncomplicated Firewall (UFW) filters the INPUT chain. Docker routes published ports through NAT rules that run before that chain, so a port published on a public address stays reachable even when UFW denies it. The Step 6 defaults and an explicit &lt;code&gt;127.0.0.1&lt;/code&gt; in each compose file keep containers on loopback.&lt;/p&gt;

&lt;p&gt;Allow SSH before you enable the firewall, or UFW blocks new SSH logins:&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 OpenSSH
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable
sudo &lt;/span&gt;ufw status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type &lt;code&gt;y&lt;/code&gt; when UFW asks to confirm. The output looks similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To serve a container on the internet, run a reverse proxy on the host and allow &lt;code&gt;80/tcp&lt;/code&gt; and &lt;code&gt;443/tcp&lt;/code&gt;. The &lt;a href="https://www.arct.cloud/blog/install-nginx-postgresql-redis-ubuntu-24-04" rel="noopener noreferrer"&gt;Nginx, PostgreSQL, and Redis setup guide&lt;/a&gt; covers Nginx as a reverse proxy. A port you publish on purpose with &lt;code&gt;0.0.0.0:PORT:PORT&lt;/code&gt; skips UFW. To restrict it, add rules to the &lt;code&gt;DOCKER-USER&lt;/code&gt; iptables chain and match the host port with &lt;code&gt;-m conntrack --ctorigdstport PORT&lt;/code&gt;, as Docker's &lt;a href="https://docs.docker.com/engine/network/firewall-iptables/" rel="noopener noreferrer"&gt;iptables docs&lt;/a&gt; show.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Start a test Compose stack
&lt;/h2&gt;

&lt;p&gt;Compose names the project, network and containers after the directory:&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; ~/hello-compose
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/hello-compose
nano compose.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste this file, then save with Ctrl+O and exit with Ctrl+X:&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;web&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;nginx:1.30.5-alpine&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:8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Current Compose releases ignore the old &lt;code&gt;version:&lt;/code&gt; key, so the file has none. &lt;code&gt;restart: unless-stopped&lt;/code&gt; starts the container again after a reboot. Start the stack:&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;Compose pulls the image, creates the &lt;code&gt;hello-compose_default&lt;/code&gt; network and starts the &lt;code&gt;hello-compose-web-1&lt;/code&gt; container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Verify the stack and the firewall
&lt;/h2&gt;

&lt;p&gt;Check that the service runs, answers on loopback, binds to &lt;code&gt;127.0.0.1&lt;/code&gt; and uses the log limits:&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 ps &lt;span class="nt"&gt;--services&lt;/span&gt; &lt;span class="nt"&gt;--status&lt;/span&gt; running
curl &lt;span class="nt"&gt;-sI&lt;/span&gt; http://127.0.0.1:8080 | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1
docker port hello-compose-web-1
docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{json .HostConfig.LogConfig}}'&lt;/span&gt; hello-compose-web-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;web
HTTP/1.1 200 OK
80/tcp -&amp;gt; 127.0.0.1:8080
{"Type":"json-file","Config":{"max-file":"3","max-size":"10m"}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From your own computer, test the public address:&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;--max-time&lt;/span&gt; 5 http://YOUR_SERVER_IP:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;YOUR_SERVER_IP&lt;/code&gt; with the server's IP address, such as &lt;code&gt;203.0.113.10&lt;/code&gt;. The request times out after 5 seconds; a page means the port is public. Remove the test stack:&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 down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Update Docker
&lt;/h2&gt;

&lt;p&gt;Docker's repository is an apt source, so a system upgrade also updates Docker Engine and the plugins:&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;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Containers with &lt;code&gt;restart: unless-stopped&lt;/code&gt; start again when the Docker service restarts. To stay on one release, list the versions in the repository, install one as Docker's install docs show, then hold both packages so &lt;code&gt;apt upgrade&lt;/code&gt; skips them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt list &lt;span class="nt"&gt;--all-versions&lt;/span&gt; docker-ce
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker-ce&lt;span class="o"&gt;=&lt;/span&gt;YOUR_VERSION docker-ce-cli&lt;span class="o"&gt;=&lt;/span&gt;YOUR_VERSION
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark hold docker-ce docker-ce-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;YOUR_VERSION&lt;/code&gt; with a full version string from the list, such as &lt;code&gt;5:29.8.2-1~ubuntu.24.04~noble&lt;/code&gt;. Run &lt;code&gt;sudo apt-mark unhold docker-ce docker-ce-cli&lt;/code&gt; when you want to upgrade again.&lt;/p&gt;

&lt;p&gt;The Ubuntu release upgrader turns off third-party sources such as Docker's, and &lt;code&gt;Suites&lt;/code&gt; keeps the old codename. After you &lt;a href="https://www.arct.cloud/tutorials/upgrade-ubuntu-26-04" rel="noopener noreferrer"&gt;upgrade Ubuntu 24.04 to 26.04&lt;/a&gt;, rerun the Step 2 source command and &lt;code&gt;sudo apt update&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshoot common Docker errors
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&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;&lt;code&gt;permission denied while trying to connect to the docker API at unix:///var/run/docker.sock&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your user is not in the &lt;code&gt;docker&lt;/code&gt; group, or the SSH session started before you joined it&lt;/td&gt;
&lt;td&gt;Run Step 5, then log out and back in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The Docker service is stopped or failed to start&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sudo systemctl start docker&lt;/code&gt;, then &lt;code&gt;sudo journalctl -u docker -n 50&lt;/code&gt;. Run the Step 6 validate command to check &lt;code&gt;daemon.json&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Conflicting values set for option Signed-By&lt;/code&gt; from &lt;code&gt;apt update&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;An older &lt;code&gt;docker.list&lt;/code&gt; from another guide points at a different key file&lt;/td&gt;
&lt;td&gt;Delete &lt;code&gt;/etc/apt/sources.list.d/docker.list&lt;/code&gt; and keep &lt;code&gt;docker.sources&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker-compose: command not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Compose v1 used &lt;code&gt;docker-compose&lt;/code&gt;; the supported plugin runs as &lt;code&gt;docker compose&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;docker compose&lt;/code&gt;, with a space&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warning that the attribute &lt;code&gt;version&lt;/code&gt; is obsolete&lt;/td&gt;
&lt;td&gt;A compose file written for Compose v1&lt;/td&gt;
&lt;td&gt;Delete the &lt;code&gt;version:&lt;/code&gt; line&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;h3&gt;
  
  
  Does Docker work on Ubuntu 24.04 and 26.04?
&lt;/h3&gt;

&lt;p&gt;Yes. Docker's install docs list Ubuntu 26.04, 24.04 and 22.04 LTS as supported releases. The same repository steps install Docker Engine 29.8.2 and Compose 5.6.0 on both 24.04 and 26.04 as of October 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Docker still relevant in 2026?
&lt;/h3&gt;

&lt;p&gt;Yes. Docker Engine 29.8.2 shipped on September 30, 2026, and Compose 5.6.0 on October 2, 2026. Self-hosted tools such as n8n and Open WebUI ship official Docker images.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I install docker.io or docker-ce on Ubuntu?
&lt;/h3&gt;

&lt;p&gt;Install docker-ce from Docker's repository for current releases. Ubuntu's docker.io package was at 29.1.3 on 24.04 and 26.04 in October 2026, while Docker's repository had 29.8.2. The two conflict, so remove docker.io first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Docker Compose installed with Docker?
&lt;/h3&gt;

&lt;p&gt;Yes, when you install the docker-compose-plugin package from Docker's repository, as in Step 3. Compose then runs as docker compose, with a space. The older docker-compose v1 command is no longer supported.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I fix docker permission denied on Ubuntu?
&lt;/h3&gt;

&lt;p&gt;Add your user to the docker group with sudo usermod -aG docker $USER, then log out and back in. Only root and docker group members can open /var/run/docker.sock. Group members get root-level access to the host.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does UFW block Docker ports?
&lt;/h3&gt;

&lt;p&gt;No. Docker routes published ports through its own NAT rules before UFW filters traffic, so a published port stays reachable when UFW denies it. Publish containers on 127.0.0.1 and use a reverse proxy on ports 80 and 443.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need Docker Desktop on an Ubuntu server?
&lt;/h3&gt;

&lt;p&gt;No. Docker Desktop is a graphical app that runs Docker Engine inside a VM. It needs KVM, a desktop environment such as GNOME, KDE or MATE, and at least 4 GB of RAM. A headless server needs Docker Engine and the Compose plugin only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.arct.cloud/tutorials/self-host-n8n" rel="noopener noreferrer"&gt;How to Self-Host n8n with Docker Compose on Ubuntu 24.04&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.arct.cloud/tutorials/install-ollama-ubuntu" rel="noopener noreferrer"&gt;How to Install Ollama on Ubuntu 24.04 with Open WebUI (CPU Only)&lt;/a&gt;, which runs Open WebUI with Compose&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.arct.cloud/tutorials/nginx-proxy-manager" rel="noopener noreferrer"&gt;How to Install Nginx Proxy Manager with Docker on Ubuntu 24.04&lt;/a&gt;, to serve containers on your own domain with HTTPS&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.docker.com/engine/install/ubuntu/" rel="noopener noreferrer"&gt;Install Docker Engine on Ubuntu&lt;/a&gt; and &lt;a href="https://docs.docker.com/engine/network/packet-filtering-firewalls/" rel="noopener noreferrer"&gt;Packet filtering and firewalls&lt;/a&gt; in Docker's docs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Licensed under &lt;a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" rel="noopener noreferrer"&gt;CC BY-NC-SA 4.0&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>ubuntu</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Self-Host n8n with Docker Compose on Ubuntu 24.04</title>
      <dc:creator>Tunç</dc:creator>
      <pubDate>Thu, 15 Oct 2026 13:36:35 +0000</pubDate>
      <link>https://dev.to/arctcloud/how-to-self-host-n8n-with-docker-compose-on-ubuntu-2404-c4i</link>
      <guid>https://dev.to/arctcloud/how-to-self-host-n8n-with-docker-compose-on-ubuntu-2404-c4i</guid>
      <description>&lt;p&gt;Originally published by Arct Cloud: &lt;a href="https://www.arct.cloud/tutorials/self-host-n8n" rel="noopener noreferrer"&gt;How to Self-Host n8n with Docker Compose on Ubuntu 24.04&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This article was prepared with AI assistance. Product references were removed and formatting adjusted for this DEV edition.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;This n8n Docker Compose stack runs n8n 2.41.6, its &lt;code&gt;n8nio/runners&lt;/code&gt; task runner and PostgreSQL 18, with Caddy in front for HTTPS on ports 80 and 443.&lt;/li&gt;
&lt;li&gt;Publish n8n on &lt;code&gt;127.0.0.1:5678&lt;/code&gt; only. Docker-published ports bypass UFW, so a public 5678 stays open even when UFW denies it.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt;, &lt;code&gt;N8N_WEBHOOK_URL&lt;/code&gt; and &lt;code&gt;N8N_PROXY_HOPS=1&lt;/code&gt;. &lt;code&gt;N8N_WEBHOOK_URL&lt;/code&gt; replaced &lt;code&gt;WEBHOOK_URL&lt;/code&gt; in n8n 2.35.0, and n8n 1.0 removed the &lt;code&gt;N8N_BASIC_AUTH&lt;/code&gt; variables.&lt;/li&gt;
&lt;li&gt;n8n's sizing example lists 320 MB to 2 GB of memory for n8n itself. The optional n8n Assistant sandbox needs 4 GB RAM and 2 vCPU.&lt;/li&gt;
&lt;li&gt;Update by changing one version line in &lt;code&gt;.env&lt;/code&gt;. n8n 3.0 supports Docker-based installs only.&lt;/li&gt;
&lt;li&gt;The Community Edition is source-available under the Sustainable Use License, which allows internal business, personal and non-commercial use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Applies to:&lt;/strong&gt; Ubuntu 24.04 LTS · n8n 2.41.6 · PostgreSQL 18 · Caddy 2.11.7 · Docker Compose 5.6.0 · checked October 2026&lt;/p&gt;

&lt;p&gt;n8n is a fair-code workflow automation platform that connects apps, APIs and AI models in visual workflows started by webhooks, schedules or other triggers. This tutorial self-hosts n8n 2.41.6 on Ubuntu 24.04 with Docker Compose, PostgreSQL 18, an external task runner and Caddy for HTTPS, then covers updates and backups. A VPS gives your webhooks a public HTTPS address that stays reachable when your own computer is off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites and n8n VPS requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A VPS running Ubuntu 24.04 LTS or 26.04 LTS with at least 2 GB RAM, which this tutorial uses as its minimum for the three containers, or 2 vCPU and 4 GB RAM if you plan to add n8n Assistant. Docker's and Caddy's apt repositories support both releases.&lt;/li&gt;
&lt;li&gt;A non-root user with sudo privileges and SSH key login. See &lt;a href="https://www.arct.cloud/tutorials/generate-ssh-key" rel="noopener noreferrer"&gt;How to Generate an SSH Key on Windows, macOS, and Linux&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Docker Engine and the Docker Compose plugin from Docker's apt repository, with your user in the &lt;code&gt;docker&lt;/code&gt; group. Follow &lt;a href="https://www.arct.cloud/tutorials/install-docker-ubuntu" rel="noopener noreferrer"&gt;How to Install Docker and Docker Compose on Ubuntu 24.04 and 26.04&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A domain with a DNS A record for a subdomain such as &lt;code&gt;n8n.example.com&lt;/code&gt; that points to your server's IP address, for example &lt;code&gt;203.0.113.10&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Commands run as the sudo user. Replace values written in capitals, such as &lt;code&gt;YOUR_DOMAIN&lt;/code&gt;, with your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this n8n Docker Compose stack works
&lt;/h2&gt;

&lt;p&gt;Four components run on the server. Only Caddy accepts connections from the internet:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Runs as&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Reachable from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Caddy&lt;/td&gt;
&lt;td&gt;systemd service on the host&lt;/td&gt;
&lt;td&gt;80, 443&lt;/td&gt;
&lt;td&gt;Internet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n8n&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;n8nio/n8n:2.41.6&lt;/code&gt; container&lt;/td&gt;
&lt;td&gt;5678&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;127.0.0.1&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task runner&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;n8nio/runners:2.41.6&lt;/code&gt; container&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Compose network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;postgres:18&lt;/code&gt; container&lt;/td&gt;
&lt;td&gt;5432&lt;/td&gt;
&lt;td&gt;Compose network&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The task runner executes Code node tasks in its own container and connects to n8n's task broker on port 5679. n8n's docs recommend this external mode for production, because internal mode runs user code next to n8n and its stored credentials. The three images are about 620 MB compressed, and n8n deletes execution data older than 14 days by default.&lt;/p&gt;

&lt;p&gt;n8n's one-line installer at &lt;code&gt;get.n8n.io&lt;/code&gt; uses SQLite and serves plain HTTP on port 5678. This tutorial writes the Compose file by hand for a public server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Open the firewall for SSH and HTTPS
&lt;/h2&gt;

&lt;p&gt;Caddy needs ports 80 and 443 to get a TLS certificate, often called an SSL certificate, and to serve n8n. Allow SSH before you enable UFW, or the current session drops:&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 OpenSSH
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable
sudo &lt;/span&gt;ufw status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type &lt;code&gt;y&lt;/code&gt; if UFW asks to confirm. The output looks similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
443/tcp                    ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
80/tcp (v6)                ALLOW       Anywhere (v6)
443/tcp (v6)               ALLOW       Anywhere (v6)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not open 5678. Step 4 publishes n8n on the loopback address, and Caddy reaches it there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create the project directory and secrets
&lt;/h2&gt;

&lt;p&gt;Compose reads variables from a &lt;code&gt;.env&lt;/code&gt; file in the project directory. Create the directory:&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; ~/n8n
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;YOUR_DOMAIN&lt;/code&gt; with your subdomain, such as &lt;code&gt;n8n.example.com&lt;/code&gt;, and &lt;code&gt;YOUR_TIMEZONE&lt;/code&gt; with a time zone name, such as &lt;code&gt;Europe/Berlin&lt;/code&gt;. Then run the block. It writes the file and generates four random secrets, and &lt;code&gt;umask 077&lt;/code&gt; inside the parentheses makes &lt;code&gt;.env&lt;/code&gt; readable only by your user:&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="o"&gt;(&lt;/span&gt;
&lt;span class="nb"&gt;umask &lt;/span&gt;077
&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="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
N8N_VERSION=2.41.6
N8N_DOMAIN=YOUR_DOMAIN
GENERIC_TIMEZONE=YOUR_TIMEZONE
POSTGRES_USER=postgres
POSTGRES_PASSWORD=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 24&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
POSTGRES_DB=n8n
POSTGRES_NON_ROOT_USER=n8n
POSTGRES_NON_ROOT_PASSWORD=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 24&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
N8N_ENCRYPTION_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="sh"&gt;
N8N_RUNNERS_AUTH_TOKEN=&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="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; encrypts every credential n8n stores. Without it, a restored database cannot decrypt your credentials, so save a copy in your password manager.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Add the database init script
&lt;/h2&gt;

&lt;p&gt;n8n connects as a PostgreSQL user without superuser rights. This script, from n8n's &lt;code&gt;withPostgres&lt;/code&gt; example, creates that user. Open a new file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano init-data.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste the script, then save with Ctrl+O and exit with Ctrl+X:&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="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_NON_ROOT_USER&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_NON_ROOT_PASSWORD&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;psql &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;--username&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--dbname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_DB&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOSQL&lt;/span&gt;&lt;span class="sh"&gt;
    CREATE USER &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_NON_ROOT_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt; WITH PASSWORD '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_NON_ROOT_PASSWORD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;';
    GRANT ALL PRIVILEGES ON DATABASE &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt; TO &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_NON_ROOT_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;;
    GRANT CREATE ON SCHEMA public TO &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_NON_ROOT_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;;
&lt;/span&gt;&lt;span class="no"&gt;EOSQL
&lt;/span&gt;&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SETUP INFO: No Environment variables given!"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PostgreSQL image runs scripts in &lt;code&gt;/docker-entrypoint-initdb.d&lt;/code&gt; once, when the database volume is empty. A later change to the passwords in &lt;code&gt;.env&lt;/code&gt; does not update the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Write the n8n Docker Compose file
&lt;/h2&gt;

&lt;p&gt;The Compose file defines the three containers, their volumes and the n8n settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano compose.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste this file, then save and exit:&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;postgres&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;postgres:18&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_NON_ROOT_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_NON_ROOT_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_NON_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_NON_ROOT_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;PGDATA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/postgresql/data&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;db_storage:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh:ro&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-h&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_DB}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&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;n8nio/n8n:${N8N_VERSION}&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="na"&gt;DB_TYPE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresdb&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432"&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_NON_ROOT_USER}&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_NON_ROOT_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;N8N_ENCRYPTION_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${N8N_ENCRYPTION_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;N8N_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${N8N_DOMAIN}&lt;/span&gt;
      &lt;span class="na"&gt;N8N_PROTOCOL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
      &lt;span class="na"&gt;N8N_WEBHOOK_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://${N8N_DOMAIN}/&lt;/span&gt;
      &lt;span class="na"&gt;N8N_PROXY_HOPS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
      &lt;span class="na"&gt;N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
      &lt;span class="na"&gt;GENERIC_TIMEZONE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GENERIC_TIMEZONE}&lt;/span&gt;
      &lt;span class="na"&gt;TZ&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GENERIC_TIMEZONE}&lt;/span&gt;
      &lt;span class="na"&gt;N8N_RUNNERS_MODE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external&lt;/span&gt;
      &lt;span class="na"&gt;N8N_RUNNERS_AUTH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${N8N_RUNNERS_AUTH_TOKEN}&lt;/span&gt;
      &lt;span class="na"&gt;N8N_RUNNERS_BROKER_LISTEN_ADDRESS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0&lt;/span&gt;
    &lt;span class="na"&gt;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_storage:/home/node/.n8n&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;

  &lt;span class="na"&gt;n8n-runner&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;n8nio/runners:${N8N_VERSION}&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;N8N_RUNNERS_AUTH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${N8N_RUNNERS_AUTH_TOKEN}&lt;/span&gt;
      &lt;span class="na"&gt;N8N_RUNNERS_TASK_BROKER_URI&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://n8n:5679&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;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;db_storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n_storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the main settings do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;127.0.0.1:5678:5678&lt;/code&gt; publishes n8n on loopback only. PostgreSQL and the task broker publish no host ports.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;N8N_WEBHOOK_URL&lt;/code&gt; and &lt;code&gt;N8N_HOST&lt;/code&gt; set the public address. Without them, webhook URLs show &lt;code&gt;http://localhost:5678/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;N8N_PROXY_HOPS: "1"&lt;/code&gt; trusts the &lt;code&gt;X-Forwarded-*&lt;/code&gt; headers from one reverse proxy.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PGDATA&lt;/code&gt; keeps PostgreSQL 18 data inside the volume, since version 18 changed its default data path.&lt;/li&gt;
&lt;li&gt;Both images read &lt;code&gt;N8N_VERSION&lt;/code&gt;, because the runner and n8n versions must match.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Start n8n with Docker Compose
&lt;/h2&gt;

&lt;p&gt;Pull the images and start the stack in the background:&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;PostgreSQL starts first. n8n waits for its health check, creates its tables, and then starts. Check the n8n log after a minute:&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 logs &lt;span class="nt"&gt;--no-log-prefix&lt;/span&gt; n8n | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A1&lt;/span&gt; &lt;span class="s2"&gt;"accessible via"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Editor is now accessible via:
https://n8n.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the command prints nothing, n8n is still starting. Run it again after 30 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Set up HTTPS with a reverse proxy
&lt;/h2&gt;

&lt;p&gt;The reverse proxy terminates TLS on port 443 and forwards requests to &lt;code&gt;127.0.0.1:5678&lt;/code&gt;. The n8n editor receives live updates over WebSockets by default, so the proxy must pass them through. Use one of the two options.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: Caddy
&lt;/h3&gt;

&lt;p&gt;Caddy obtains and renews TLS certificates automatically. Ubuntu 24.04 and 26.04 ship Caddy 2.6.2, so install the current release from Caddy's apt repository:&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;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; debian-keyring debian-archive-keyring apt-transport-https curl
curl &lt;span class="nt"&gt;-1sLf&lt;/span&gt; &lt;span class="s1"&gt;'https://dl.cloudsmith.io/public/caddy/stable/gpg.key'&lt;/span&gt; | &lt;span class="nb"&gt;sudo &lt;/span&gt;gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl &lt;span class="nt"&gt;-1sLf&lt;/span&gt; &lt;span class="s1"&gt;'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/caddy-stable.list
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;o+r /etc/apt/sources.list.d/caddy-stable.list
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;caddy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package starts Caddy as a systemd service named &lt;code&gt;caddy&lt;/code&gt;. Replace the default site configuration:&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;nano /etc/caddy/Caddyfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete the existing lines and paste this, with your domain in place of &lt;code&gt;YOUR_DOMAIN&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;YOUR_DOMAIN {
    reverse_proxy 127.0.0.1:5678 {
        flush_interval -1
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caddy proxies WebSockets and sets the &lt;code&gt;X-Forwarded-For&lt;/code&gt;, &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; and &lt;code&gt;X-Forwarded-Host&lt;/code&gt; headers by default. &lt;code&gt;flush_interval -1&lt;/code&gt;, from n8n's own Caddyfile, sends responses to the browser without buffering. Validate the file and reload Caddy:&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;caddy validate &lt;span class="nt"&gt;--config&lt;/span&gt; /etc/caddy/Caddyfile
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload caddy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The validate output ends with &lt;code&gt;Valid configuration&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: Nginx
&lt;/h3&gt;

&lt;p&gt;If the server already runs Nginx with a Certbot certificate, as in the &lt;a href="https://www.arct.cloud/blog/install-nginx-postgresql-redis-ubuntu-24-04" rel="noopener noreferrer"&gt;Nginx, PostgreSQL, and Redis guide&lt;/a&gt;, use this &lt;code&gt;location&lt;/code&gt; block in the server block for &lt;code&gt;YOUR_DOMAIN&lt;/code&gt;. Ubuntu's Nginx packages, 1.24 on 24.04 and 1.28 on 26.04, need &lt;code&gt;proxy_http_version 1.1&lt;/code&gt; for the WebSocket upgrade. &lt;code&gt;client_max_body_size 16m&lt;/code&gt; raises Nginx's 1 MB default request body limit to match n8n's default 16 MiB payload limit:&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;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://127.0.0.1:5678&lt;/span&gt;&lt;span class="p"&gt;;&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="s"&gt;"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-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-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-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_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="kn"&gt;client_max_body_size&lt;/span&gt; &lt;span class="mi"&gt;16m&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;Test the configuration with &lt;code&gt;sudo nginx -t&lt;/code&gt;, then apply it with &lt;code&gt;sudo systemctl reload nginx&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Create the n8n owner account
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;https://YOUR_DOMAIN&lt;/code&gt; in your browser. n8n shows the owner setup form, and the first person to complete it becomes the instance owner. Until you complete this form, anyone who opens the address can claim the instance, so do this step immediately after Step 6. Enter your email address, name and a strong password.&lt;/p&gt;

&lt;p&gt;n8n removed basic auth in version 1.0, and no supported setting turns off the login screen. Without SMTP settings, you send invite links by hand and users cannot reset their passwords.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Verify n8n is running
&lt;/h2&gt;

&lt;p&gt;List the running services:&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 ps &lt;span class="nt"&gt;--services&lt;/span&gt; &lt;span class="nt"&gt;--status&lt;/span&gt; running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output lists &lt;code&gt;n8n&lt;/code&gt;, &lt;code&gt;n8n-runner&lt;/code&gt; and &lt;code&gt;postgres&lt;/code&gt;. Check that n8n is connected to the database and ready:&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:5678/healthz/readiness
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"status":"ok"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From your own computer, confirm that port 5678 is closed to the internet:&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;--max-time&lt;/span&gt; 5 http://YOUR_SERVER_IP:5678
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;YOUR_SERVER_IP&lt;/code&gt; with the server's address, such as &lt;code&gt;203.0.113.10&lt;/code&gt;. The request fails or times out; a page means the port is public. Check memory use per container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stats &lt;span class="nt"&gt;--no-stream&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s2"&gt;"table {{.Name}}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;{{.MemUsage}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Memory use rises with large JSON or binary data, Code nodes, workflows that run at the same time, and manual executions, because n8n copies their data for the editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update n8n
&lt;/h2&gt;

&lt;p&gt;n8n releases a new minor version most weeks, and its docs recommend updating at least once a month. Back up first, because n8n migrates the database when a new version starts.&lt;/p&gt;

&lt;p&gt;Find the current stable number on &lt;a href="https://github.com/n8n-io/n8n/releases" rel="noopener noreferrer"&gt;n8n's GitHub releases page&lt;/a&gt;, where beta builds are marked as pre-releases. Set it in &lt;code&gt;.env&lt;/code&gt;, recreate the containers and remove the old images:&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; ~/n8n
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/^N8N_VERSION=.*/N8N_VERSION=YOUR_NEW_VERSION/'&lt;/span&gt; .env
docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker image &lt;span class="nb"&gt;rm &lt;/span&gt;n8nio/n8n:YOUR_OLD_VERSION n8nio/runners:YOUR_OLD_VERSION
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;YOUR_NEW_VERSION&lt;/code&gt; with the stable release number and &lt;code&gt;YOUR_OLD_VERSION&lt;/code&gt; with the version you replaced, which &lt;code&gt;grep N8N_VERSION .env&lt;/code&gt; shows before the change. The runner image reads the same line, and &lt;code&gt;docker image rm&lt;/code&gt; deletes the two old images.&lt;/p&gt;

&lt;p&gt;Read the &lt;a href="https://docs.n8n.io/changelog/v30-breaking-changes.md" rel="noopener noreferrer"&gt;v3.0 breaking changes&lt;/a&gt; before you move from 2.x to 3.0. This setup already uses Docker and an external task runner. In 3.0, the default Code node task timeout drops from 300 to 60 seconds.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;postgres:18&lt;/code&gt; pulls PostgreSQL minor releases. A new major version needs a dump and restore; changing the image tag alone fails with &lt;code&gt;database files are incompatible with server&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back up n8n
&lt;/h2&gt;

&lt;p&gt;A complete n8n backup has two parts: the PostgreSQL database and the &lt;code&gt;.n8n&lt;/code&gt; folder in the &lt;code&gt;n8n_storage&lt;/code&gt; volume. Keep the &lt;code&gt;.env&lt;/code&gt; file with them, because it holds the encryption key and the database passwords:&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; ~/n8n-backups
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/n8n
&lt;span class="nv"&gt;STAMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F-%H%M&lt;span class="si"&gt;)&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; postgres pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-d&lt;/span&gt; n8n &lt;span class="nt"&gt;-Fc&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/n8n-backups/n8n-db-&lt;span class="nv"&gt;$STAMP&lt;/span&gt;.dump
docker compose &lt;span class="nb"&gt;cp &lt;/span&gt;n8n:/home/node/.n8n ~/n8n-backups/n8n-files-&lt;span class="nv"&gt;$STAMP&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env compose.yaml init-data.sh ~/n8n-backups/
&lt;span class="nb"&gt;chmod&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; go-rwx ~/n8n-backups
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pg_dump -Fc&lt;/code&gt; writes a compressed dump while n8n keeps running, and &lt;code&gt;docker compose cp&lt;/code&gt; copies the folder out of the container. &lt;code&gt;STAMP&lt;/code&gt; holds the date and time, so two runs on the same day write separate files.&lt;/p&gt;

&lt;p&gt;To run the backup on a schedule, save the commands in a script such as &lt;code&gt;~/n8n/backup.sh&lt;/code&gt; with &lt;code&gt;#!/bin/bash&lt;/code&gt; as the first line, make it executable with &lt;code&gt;chmod +x ~/n8n/backup.sh&lt;/code&gt;, and call the script from cron or a systemd timer. Do not paste the commands into a crontab line, because cron treats &lt;code&gt;%&lt;/code&gt; as a line break. Copy &lt;code&gt;~/n8n-backups&lt;/code&gt; to another machine, for example with &lt;code&gt;rsync&lt;/code&gt; over SSH.&lt;/p&gt;

&lt;p&gt;To restore the database on the same server, stop n8n and the runner, load the dump, and start them again:&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 stop n8n n8n-runner
docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; postgres pg_restore &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-d&lt;/span&gt; n8n &lt;span class="nt"&gt;--clean&lt;/span&gt; &lt;span class="nt"&gt;--if-exists&lt;/span&gt; &amp;lt; ~/n8n-backups/n8n-db-YOUR_TIMESTAMP.dump
docker compose start n8n n8n-runner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;YOUR_TIMESTAMP&lt;/code&gt; with the date and time in the backup file name. Never run &lt;code&gt;docker compose down -v&lt;/code&gt; in this project: &lt;code&gt;-v&lt;/code&gt; deletes both volumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshoot common n8n errors
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&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;Caddy returns 502 and logs &lt;code&gt;dial tcp 127.0.0.1:5678: connect: connection refused&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;n8n is stopped or still starting&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker compose ps&lt;/code&gt;, then &lt;code&gt;docker compose logs n8n&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Database is not ready!&lt;/code&gt; in the browser, or &lt;code&gt;There was an error initializing DB&lt;/code&gt; in the n8n log&lt;/td&gt;
&lt;td&gt;PostgreSQL is down or rejects the n8n user&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker compose logs postgres&lt;/code&gt;, and compare the passwords in &lt;code&gt;.env&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;password authentication failed for user "n8n"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The database volume is older than the passwords in &lt;code&gt;.env&lt;/code&gt;, so &lt;code&gt;init-data.sh&lt;/code&gt; did not set them&lt;/td&gt;
&lt;td&gt;On a new install with no workflows: &lt;code&gt;docker compose down&lt;/code&gt;, &lt;code&gt;docker volume rm n8n_db_storage&lt;/code&gt;, then &lt;code&gt;docker compose up -d&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Mismatching encryption keys. The encryption key in the settings file /home/node/.n8n/config does not match the N8N_ENCRYPTION_KEY env var.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; changed after the first start&lt;/td&gt;
&lt;td&gt;Put the original key back in &lt;code&gt;.env&lt;/code&gt;, then &lt;code&gt;docker compose up -d&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser certificate error on &lt;code&gt;YOUR_DOMAIN&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The DNS A record points elsewhere, or port 80 is closed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;getent hosts YOUR_DOMAIN&lt;/code&gt;, &lt;code&gt;sudo ufw status&lt;/code&gt;, &lt;code&gt;sudo journalctl -u caddy -n 50&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;h3&gt;
  
  
  How much RAM does n8n need?
&lt;/h3&gt;

&lt;p&gt;n8n's sizing example lists 320 MB to 2 GB of memory for n8n, and an idle n8n Cloud instance uses about 100 MB. PostgreSQL and the task runner add to that, and large files or Code nodes raise it. The n8n Assistant sandbox needs at least 4 GB RAM and 2 vCPU.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is n8n free to use?
&lt;/h3&gt;

&lt;p&gt;Yes, the self-hosted Community Edition is free to use under the Sustainable Use License, a fair-code license n8n created in 2022. It allows internal business, personal and non-commercial use, and n8n describes the code as source-available.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I update an n8n Docker Compose install?
&lt;/h3&gt;

&lt;p&gt;Set N8N_VERSION in the .env file to the new release, then run docker compose pull and docker compose up -d in the project folder. The task runner image uses the same tag. Back up the database first, because n8n migrates it on start.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use PostgreSQL or SQLite for n8n?
&lt;/h3&gt;

&lt;p&gt;Use PostgreSQL on a server that runs workflows all day. n8n's Docker Compose guide calls SQLite fine for trying things out and recommends Postgres for production. Existing SQLite data does not move to Postgres on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do my n8n webhook URLs show localhost:5678?
&lt;/h3&gt;

&lt;p&gt;n8n builds webhook URLs from N8N_PROTOCOL, N8N_HOST and N8N_PORT unless N8N_WEBHOOK_URL is set. Behind a reverse proxy, set N8N_WEBHOOK_URL to your HTTPS address and N8N_PROXY_HOPS to 1. N8N_WEBHOOK_URL replaced WEBHOOK_URL in n8n 2.35.0.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I run n8n locally with Docker Desktop?
&lt;/h3&gt;

&lt;p&gt;Yes. The n8n image runs under Docker Desktop and serves the editor at &lt;a href="http://localhost:5678" rel="noopener noreferrer"&gt;http://localhost:5678&lt;/a&gt;. Outside services cannot reach webhooks on a laptop without a tunnel, so workflows that receive webhooks run on a server with a public HTTPS address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.arct.cloud/tutorials/n8n-ai-agent" rel="noopener noreferrer"&gt;How to Build an n8n AI Agent with an API Model or Ollama&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.arct.cloud/tutorials/nginx-proxy-manager" rel="noopener noreferrer"&gt;How to Install Nginx Proxy Manager with Docker on Ubuntu 24.04&lt;/a&gt;, to run n8n and other apps behind one reverse proxy&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.n8n.io/deploy/host-n8n/configure-n8n/set-up-task-runners.md" rel="noopener noreferrer"&gt;Set up task runners&lt;/a&gt; and &lt;a href="https://docs.n8n.io/deploy/host-n8n/keep-n8n-running/backup-and-restore.md" rel="noopener noreferrer"&gt;Back up and restore&lt;/a&gt; in n8n's docs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Licensed under &lt;a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" rel="noopener noreferrer"&gt;CC BY-NC-SA 4.0&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>n8n</category>
      <category>docker</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
