<?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: Ali Ilyas</title>
    <description>The latest articles on DEV Community by Ali Ilyas (@ali_ilyas).</description>
    <link>https://dev.to/ali_ilyas</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4064723%2Ff4e6bf6f-f60b-433a-ae47-238bcf783ed1.png</url>
      <title>DEV Community: Ali Ilyas</title>
      <link>https://dev.to/ali_ilyas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ali_ilyas"/>
    <language>en</language>
    <item>
      <title>n8n Docker Setup: Run n8n in a Container Without Losing Your Data (2026)</title>
      <dc:creator>Ali Ilyas</dc:creator>
      <pubDate>Fri, 21 Aug 2026 10:34:50 +0000</pubDate>
      <link>https://dev.to/ali_ilyas/n8n-docker-setup-run-n8n-in-a-container-without-losing-your-data-2026-48op</link>
      <guid>https://dev.to/ali_ilyas/n8n-docker-setup-run-n8n-in-a-container-without-losing-your-data-2026-48op</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://noderecipe.com/n8n-docker-setup" rel="noopener noreferrer"&gt;noderecipe.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fastest n8n Docker setup is a single command, and you'll have the editor open at &lt;code&gt;http://localhost:5678&lt;/code&gt; in about two minutes. The part that trips people up comes later — the container restarts, and every workflow is gone. This guide covers the command, then the things that decide whether your instance survives its first update: where n8n actually stores data, which environment variables matter, and how to pin and roll back versions.&lt;/p&gt;

&lt;p&gt;No prior Docker experience is assumed. Where a flag matters, I explain what it does rather than asking you to paste it on faith.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do you want Docker setup, or full self-hosting?
&lt;/h2&gt;

&lt;p&gt;These are two different jobs, and mixing them up wastes an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This guide&lt;/strong&gt; is about the container itself: how the n8n image runs, where it keeps state, how to update it safely, and what to do when Docker misbehaves. It applies whether you're on a laptop or a server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Putting n8n on the public internet&lt;/strong&gt; — a domain, HTTPS certificates, a reverse proxy, Postgres, backups, firewall rules — is a separate concern. That's covered step by step in the &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;complete guide to self-hosting n8n&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you just want n8n running locally to build and test workflows, stay here. If you want a URL your team can reach, read this first, then that.&lt;/p&gt;

&lt;p&gt;One practical note if you already know you're heading for a server: everything below runs unchanged on a &lt;strong&gt;$5/month VPS&lt;/strong&gt;, so there's no need to build locally and migrate later. &lt;a href="https://www.hetzner.com/cloud" rel="noopener noreferrer"&gt;Hetzner&lt;/a&gt; (2 GB for about €4.5) and &lt;a href="https://www.digitalocean.com/" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt; ($6 for 1 GB) both run this fine, and both hand new accounts enough signup credit to cover the first month or two. Spin one up now and follow along on it — the commands are identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Docker installed. On Linux, Docker Engine plus the Compose plugin. On Windows or macOS, Docker Desktop.&lt;/li&gt;
&lt;li&gt;About 1 GB of free RAM. n8n is comfortable in 2 GB once you have a few active workflows.&lt;/li&gt;
&lt;li&gt;A terminal. Every command below is copy-pasteable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verify Docker is working before going further:&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;If the second command errors, you have an older standalone &lt;code&gt;docker-compose&lt;/code&gt; binary. The commands still work — write them as &lt;code&gt;docker-compose&lt;/code&gt; instead of &lt;code&gt;docker compose&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start n8n in one command
&lt;/h2&gt;

&lt;p&gt;Create a volume for n8n's data, then start the 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 volume create n8n_data

docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; n8n &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 5678:5678 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;GENERIC_TIMEZONE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Europe/Berlin"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;TZ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Europe/Berlin"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; n8n_data:/home/node/.n8n &lt;span class="se"&gt;\&lt;/span&gt;
  docker.n8n.io/n8nio/n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:5678&lt;/code&gt; and create your owner account.&lt;/p&gt;

&lt;p&gt;What each part does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-p 5678:5678&lt;/code&gt; maps the container's port to your machine. Change the &lt;strong&gt;left&lt;/strong&gt; number if 5678 is taken: &lt;code&gt;-p 8080:5678&lt;/code&gt; serves n8n on &lt;code&gt;http://localhost:8080&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GENERIC_TIMEZONE&lt;/code&gt; sets the timezone for schedule-based nodes. Get this wrong and your Schedule Trigger fires at the wrong hour. &lt;code&gt;TZ&lt;/code&gt; sets the timezone for the operating system inside the container, which affects log timestamps. Set both, to the same value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true&lt;/code&gt; locks down the config file that holds your encryption key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-v n8n_data:/home/node/.n8n&lt;/code&gt; is the important one. See the next section.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two flags deserve a warning. &lt;code&gt;-it&lt;/code&gt; attaches the container to your terminal, so closing that terminal stops n8n. &lt;code&gt;--rm&lt;/code&gt; &lt;strong&gt;deletes the container when it stops.&lt;/strong&gt; That combination is right for trying n8n out and wrong for anything you care about. For a container that keeps running, drop both and add &lt;code&gt;-d&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; n8n &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 5678:5678 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;GENERIC_TIMEZONE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Europe/Berlin"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;TZ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Europe/Berlin"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; n8n_data:/home/node/.n8n &lt;span class="se"&gt;\&lt;/span&gt;
  docker.n8n.io/n8nio/n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--restart unless-stopped&lt;/code&gt; brings n8n back after a reboot or a crash.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;On n8n 1.x, add &lt;code&gt;-e N8N_RUNNERS_ENABLED=true&lt;/code&gt;.&lt;/strong&gt; Task runners are how n8n executes code nodes, and on the 1.x line you have to opt in. From version 2.0 the variable is deprecated and you no longer need to set it. If you're on 2.0 or later and copied an older tutorial, you can drop it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where n8n stores your data
&lt;/h2&gt;

&lt;p&gt;n8n keeps everything in &lt;strong&gt;&lt;code&gt;/home/node/.n8n&lt;/code&gt;&lt;/strong&gt; inside the container. That directory holds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the SQLite database with your workflows, credentials, and execution history&lt;/li&gt;
&lt;li&gt;the encryption key that your saved credentials are encrypted against&lt;/li&gt;
&lt;li&gt;instance settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Containers are disposable. Anything written inside one that isn't mapped to a volume disappears when the container is removed — and &lt;code&gt;docker run --rm&lt;/code&gt;, &lt;code&gt;docker rm&lt;/code&gt;, and most update procedures all remove containers. &lt;strong&gt;This is the single most common way people lose an n8n instance.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-v n8n_data:/home/node/.n8n&lt;/code&gt; flag maps a Docker-managed &lt;strong&gt;named volume&lt;/strong&gt; to that directory, so the data lives outside the container's lifecycle. Confirm it exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume inspect n8n_data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The encryption key detail is worth dwelling on. Your credentials are encrypted with a key stored in that directory. Lose the volume and you don't just lose workflows — every saved credential becomes undecryptable, even if you have a database backup. Restoring means re-entering every API key by hand.&lt;/p&gt;

&lt;p&gt;You can use a &lt;strong&gt;bind mount&lt;/strong&gt; instead, pointing at a folder you can see:&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="nt"&gt;-v&lt;/span&gt; ~/n8n-data:/home/node/.n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes backups as easy as copying a folder, but it introduces a permissions problem on Linux — see &lt;a href="https://noderecipe.com/n8n-docker-setup#common-docker-errors-and-fixes" rel="noopener noreferrer"&gt;common errors&lt;/a&gt; below. Named volumes are the lower-friction default; bind mounts are better when you want direct file access.&lt;/p&gt;

&lt;h2&gt;
  
  
  docker run vs. Docker Compose
&lt;/h2&gt;

&lt;p&gt;A long &lt;code&gt;docker run&lt;/code&gt; command is fine once. It stops being fine the moment you need to remember it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;docker run&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Docker Compose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trying n8n for ten minutes&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Overkill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keeping an instance around&lt;/td&gt;
&lt;td&gt;Painful — you must retype every flag&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adding Postgres or Redis&lt;/td&gt;
&lt;td&gt;Manual networking&lt;/td&gt;
&lt;td&gt;Handles it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Updating&lt;/td&gt;
&lt;td&gt;Stop, remove, retype the command&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pull&lt;/code&gt; then &lt;code&gt;up -d&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config lives in&lt;/td&gt;
&lt;td&gt;Your shell history&lt;/td&gt;
&lt;td&gt;A file you can version-control&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Anything you intend to keep belongs in Compose. The configuration becomes a file you can read, diff, and back up, instead of a command you half-remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Compose file for everyday local use
&lt;/h2&gt;

&lt;p&gt;Create a folder, add a &lt;code&gt;docker-compose.yml&lt;/code&gt;, and start it. This setup uses SQLite, which is the right choice for local development and light use:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it, and watch the logs:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;Ctrl+C&lt;/code&gt; to stop following the logs — the container keeps running. To stop n8n itself:&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;p&gt;&lt;code&gt;down&lt;/code&gt; removes the container but &lt;strong&gt;not&lt;/strong&gt; the named volume, so your workflows survive. Adding &lt;code&gt;-v&lt;/code&gt; to that command deletes the volume too, which is the destructive version. Don't type &lt;code&gt;docker compose down -v&lt;/code&gt; on an instance you care about.&lt;/p&gt;

&lt;p&gt;For a production setup with Postgres, HTTPS, and a reverse proxy, use the Compose file in the &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;self-hosting guide&lt;/a&gt; instead — SQLite is fine locally but not what you want under real load.&lt;/p&gt;

&lt;h2&gt;
  
  
  The environment variables that matter
&lt;/h2&gt;

&lt;p&gt;There are well over a hundred. These are the ones that change outcomes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Why you'd set it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GENERIC_TIMEZONE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Timezone for Schedule Trigger and other time-based nodes. Wrong value = jobs at the wrong hour.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TZ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Operating-system timezone in the container. Affects log timestamps. Match it to &lt;code&gt;GENERIC_TIMEZONE&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sets the credential encryption key explicitly instead of letting n8n generate one. Useful when you need reproducible deployments.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;N8N_SECURE_COOKIE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defaults to &lt;code&gt;true&lt;/code&gt;, which means the session cookie is only sent over HTTPS. Accessing n8n over plain &lt;code&gt;http://&lt;/code&gt; on anything other than localhost will fail to log in until you set this to &lt;code&gt;false&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restricts permissions on the settings file holding your encryption key.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;N8N_PORT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changes the port inside the container. Usually unnecessary — remap with &lt;code&gt;-p&lt;/code&gt; instead.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;N8N_RUNNERS_ENABLED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Required as &lt;code&gt;true&lt;/code&gt; on 1.x to enable task runners. Deprecated from 2.0 onward.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To use Postgres instead of SQLite, add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DB_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresdb
&lt;span class="nv"&gt;DB_POSTGRESDB_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres
&lt;span class="nv"&gt;DB_POSTGRESDB_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5432
&lt;span class="nv"&gt;DB_POSTGRESDB_DATABASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;n8n
&lt;span class="nv"&gt;DB_POSTGRESDB_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;n8n
&lt;span class="nv"&gt;DB_POSTGRESDB_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even on Postgres, keep the &lt;code&gt;/home/node/.n8n&lt;/code&gt; volume mounted. The database moves to Postgres, but the encryption key and instance settings still live in that directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating n8n, and rolling back
&lt;/h2&gt;

&lt;p&gt;The image tag you use decides how much control you have. &lt;code&gt;docker.n8n.io/n8nio/n8n&lt;/code&gt; with no tag means &lt;code&gt;latest&lt;/code&gt;, so every pull can move you forward by an unknown amount. For anything you depend on, pin a specific version:&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.n8n.io/n8nio/n8n:1.80.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back up before updating. With a named volume, the quickest snapshot is a tarball:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; n8n_data:/data &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:/backup alpine &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;tar &lt;/span&gt;czf /backup/n8n-backup.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /data &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then update:&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 pull
docker compose down
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rolling back is why pinning matters: change the tag in &lt;code&gt;docker-compose.yml&lt;/code&gt; back to the previous version and run &lt;code&gt;docker compose up -d&lt;/code&gt; again. On &lt;code&gt;latest&lt;/code&gt;, you have no easy way to name the version that was working.&lt;/p&gt;

&lt;p&gt;Check what you're actually running:&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 n8n | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Docker Desktop on Windows and macOS
&lt;/h2&gt;

&lt;p&gt;The commands are identical, with three differences worth knowing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Localhost works as expected.&lt;/strong&gt; &lt;code&gt;http://localhost:5678&lt;/code&gt; reaches the container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bind mounts are slower.&lt;/strong&gt; File operations across the Windows or macOS boundary carry overhead. Named volumes are meaningfully faster on these platforms — another reason to prefer them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On Windows, keep files inside WSL2.&lt;/strong&gt; If you use bind mounts, put the project folder in the WSL2 filesystem (&lt;code&gt;\\wsl$\...&lt;/code&gt;) rather than under &lt;code&gt;C:\Users\...&lt;/code&gt;. Crossing the filesystem boundary is slow and causes intermittent permission oddities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Docker errors and fixes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;All my workflows disappeared after a restart.&lt;/strong&gt;&lt;br&gt;
The container ran without a volume mapped to &lt;code&gt;/home/node/.n8n&lt;/code&gt;. Unless you had a backup, that data is gone — containers don't keep filesystem changes after removal. Add &lt;code&gt;-v n8n_data:/home/node/.n8n&lt;/code&gt; and rebuild. Verify with &lt;code&gt;docker volume inspect n8n_data&lt;/code&gt; before you invest hours in building workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;EACCES: permission denied&lt;/code&gt; writing to &lt;code&gt;/home/node/.n8n&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
This hits bind mounts on Linux. The n8n container runs as the &lt;code&gt;node&lt;/code&gt; user (UID 1000), and your host folder is owned by someone else. Fix ownership on the host:&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 chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 1000:1000 ~/n8n-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Named volumes don't have this problem, which is the main argument for them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;port is already allocated&lt;/code&gt; on 5678.&lt;/strong&gt;&lt;br&gt;
Something else holds the port — often a previous n8n container. Find it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;5678
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove the old container, or remap: &lt;code&gt;-p 8080:5678&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I can't log in — the page reloads back to the sign-in screen.&lt;/strong&gt;&lt;br&gt;
You're reaching n8n over plain &lt;code&gt;http://&lt;/code&gt; on a LAN IP or hostname. &lt;code&gt;N8N_SECURE_COOKIE&lt;/code&gt; defaults to &lt;code&gt;true&lt;/code&gt;, so the session cookie is only sent over HTTPS and never comes back. For local network use, set &lt;code&gt;N8N_SECURE_COOKIE=false&lt;/code&gt;. Don't do that on a public server — &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;set up HTTPS properly&lt;/a&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The container exits immediately.&lt;/strong&gt;&lt;br&gt;
Read the logs, which almost always name the cause:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Malformed environment variables and unreachable database hosts are the usual culprits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webhooks show a localhost URL that external services can't reach.&lt;/strong&gt;&lt;br&gt;
Expected: n8n advertises the address it thinks it has. For webhooks reachable from the internet you need a public URL and the &lt;code&gt;WEBHOOK_URL&lt;/code&gt; variable, which is part of &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;putting n8n on a real domain&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is the Docker version of n8n free?&lt;/strong&gt;&lt;br&gt;
Yes. The image is the Community Edition, which is free to self-host with unlimited workflow executions. Some features — SSO, projects, Git version control — are licensed separately, and the licence limits what you can do commercially. &lt;a href="https://noderecipe.com/is-n8n-free" rel="noopener noreferrer"&gt;Is n8n free?&lt;/a&gt; covers both lists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need Docker Compose, or is &lt;code&gt;docker run&lt;/code&gt; enough?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;docker run&lt;/code&gt; is enough to try it. Use Compose for anything you keep — it turns a command you have to remember into a file you can back up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run n8n in Docker on a Raspberry Pi?&lt;/strong&gt;&lt;br&gt;
Yes, on 64-bit ARM hardware with enough RAM. A Pi 4 with 2 GB handles light workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I move an instance to another machine?&lt;/strong&gt;&lt;br&gt;
Copy the &lt;code&gt;/home/node/.n8n&lt;/code&gt; volume contents, including the encryption key. Move the database without the key and your credentials will be unreadable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where does n8n store workflows in Docker?&lt;/strong&gt;&lt;br&gt;
In the SQLite database inside &lt;code&gt;/home/node/.n8n&lt;/code&gt;, unless you configured Postgres — in which case workflows live in Postgres, while the encryption key and settings remain in that directory.&lt;/p&gt;




&lt;p&gt;Official references worth bookmarking: n8n's &lt;a href="https://docs.n8n.io/deploy/host-n8n/install-options/install-with-docker/" rel="noopener noreferrer"&gt;Docker installation docs&lt;/a&gt; and Docker's own &lt;a href="https://docs.docker.com/engine/storage/volumes/" rel="noopener noreferrer"&gt;guide to volumes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ready to build?&lt;/strong&gt; The &lt;code&gt;docker-compose.yml&lt;/code&gt; files above are complete — copy them straight out of this page. For workflows to run on top of it, our &lt;a href="https://noderecipe.com/templates" rel="noopener noreferrer"&gt;templates page&lt;/a&gt; has eight importable starters, including an error handler and a heartbeat monitor. Taking this to a real server with a domain and HTTPS? That's the &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;complete self-hosting walkthrough&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
    </item>
    <item>
      <title>n8n Telegram Bot: Build One That Sends and Receives Messages (2026)</title>
      <dc:creator>Ali Ilyas</dc:creator>
      <pubDate>Sat, 15 Aug 2026 11:08:28 +0000</pubDate>
      <link>https://dev.to/ali_ilyas/n8n-telegram-bot-build-one-that-sends-and-receives-messages-2026-mha</link>
      <guid>https://dev.to/ali_ilyas/n8n-telegram-bot-build-one-that-sends-and-receives-messages-2026-mha</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://noderecipe.com/n8n-telegram-bot" rel="noopener noreferrer"&gt;noderecipe.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Building a Telegram bot in n8n takes about five minutes if all you want is outbound messages — notifications, alerts, a daily digest. Making the bot &lt;em&gt;listen&lt;/em&gt; and respond is a different job, and it's where most first attempts stall.&lt;/p&gt;

&lt;p&gt;The reason is a Telegram rule that has nothing to do with n8n: &lt;strong&gt;a bot can have exactly one webhook.&lt;/strong&gt; n8n uses one URL when you're testing a workflow and a different one once it's active, so the two modes silently overwrite each other. Your bot works in the editor, then stops when you activate it — or the reverse — and nothing in the error explains why.&lt;/p&gt;

&lt;p&gt;This guide covers the fast outbound path first, then the trigger, then that constraint and how to work around it properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Telegram account.&lt;/li&gt;
&lt;li&gt;A running n8n instance. &lt;strong&gt;Outbound messages work anywhere&lt;/strong&gt;, including a local instance on &lt;code&gt;localhost&lt;/code&gt;. &lt;strong&gt;Receiving messages requires a public HTTPS URL&lt;/strong&gt; — Telegram will not deliver updates to &lt;code&gt;localhost&lt;/code&gt; or to plain &lt;code&gt;http://&lt;/code&gt;. If you're running locally, &lt;a href="https://noderecipe.com/n8n-docker-setup" rel="noopener noreferrer"&gt;n8n in Docker&lt;/a&gt; is the quickest start; for the trigger you'll need &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;a real domain with HTTPS&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That split is worth internalizing before you begin. Half of this guide works on a laptop; the other half doesn't, and no amount of configuration changes that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the bot with BotFather
&lt;/h2&gt;

&lt;p&gt;BotFather is Telegram's official bot for creating bots. Open a chat with &lt;a href="https://telegram.me/BotFather" rel="noopener noreferrer"&gt;@BotFather&lt;/a&gt; and send:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;It asks for two things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name&lt;/strong&gt; — the display name people see. Changeable later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Username&lt;/strong&gt; — the permanent identifier. The rules: 5 to 32 characters, not case sensitive, Latin characters, numbers and underscores only, and it &lt;strong&gt;must end in &lt;code&gt;bot&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;tetris_bot&lt;/code&gt;, &lt;code&gt;my_alerts_bot&lt;/code&gt;). This one cannot be changed afterward, so pick something you won't regret.&lt;/p&gt;

&lt;p&gt;BotFather then gives you an &lt;strong&gt;access token&lt;/strong&gt; that looks roughly like &lt;code&gt;8123456789:AAF...&lt;/code&gt;. Treat it like a password — anyone holding it controls your bot completely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the credential in n8n
&lt;/h2&gt;

&lt;p&gt;In n8n, create a new &lt;strong&gt;Telegram API&lt;/strong&gt; credential and paste the token into the &lt;strong&gt;Access Token&lt;/strong&gt; field. Save.&lt;/p&gt;

&lt;p&gt;That's the entire credential setup. No OAuth flow, no consent screen, no Google Cloud project — a pleasant contrast to &lt;a href="https://noderecipe.com/n8n-google-sheets" rel="noopener noreferrer"&gt;connecting Google Sheets&lt;/a&gt;, where credentials are most of the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send your first message
&lt;/h2&gt;

&lt;p&gt;Add a &lt;strong&gt;Telegram&lt;/strong&gt; node, set Resource to &lt;strong&gt;Message&lt;/strong&gt; and Operation to &lt;strong&gt;Send Message&lt;/strong&gt;. Two fields matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chat ID&lt;/strong&gt; — where the message goes. For a public channel you can use &lt;code&gt;@channelusername&lt;/code&gt;. For anything else you need the numeric ID, which is the next section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text&lt;/strong&gt; — up to 4096 characters after entity parsing. Longer messages are rejected rather than truncated, so if you're piping in generated or scraped content, split it before it reaches this node.&lt;/p&gt;

&lt;p&gt;Before you run it: &lt;strong&gt;a bot cannot message you first.&lt;/strong&gt; Telegram requires the user to initiate contact. Open your bot's chat and send it &lt;code&gt;/start&lt;/code&gt;, or the send will fail no matter how correct your configuration is. For a group or channel, add the bot as a member — and as an &lt;strong&gt;administrator&lt;/strong&gt; if it needs to post to a channel.&lt;/p&gt;

&lt;p&gt;One setting you'll want immediately. By default n8n appends its own attribution line to outgoing messages. To remove it, open &lt;strong&gt;Additional Fields → Add Field&lt;/strong&gt; and turn off &lt;strong&gt;Append n8n Attribution&lt;/strong&gt;. Most people don't notice this until a client asks why the bot is advertising software.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding your chat ID
&lt;/h2&gt;

&lt;p&gt;The most common blocker on this integration, and there are three ways through it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the Telegram Trigger.&lt;/strong&gt; Add a Telegram Trigger node, execute it, and message your bot. The output contains the chat object, and &lt;code&gt;chat.id&lt;/code&gt; is what you want. This is the most reliable method because it gives you the exact value Telegram will use. It needs a publicly reachable instance, though — see the trigger section below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read it from Telegram Web.&lt;/strong&gt; Open the group in a browser. The chat ID is the run of digits after the letter &lt;code&gt;g&lt;/code&gt; in the URL. &lt;strong&gt;Prefix it with &lt;code&gt;-&lt;/code&gt; when you enter it in n8n&lt;/strong&gt; — group IDs are negative, and omitting the minus sign is a frequent, confusing failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invite @RawDataBot.&lt;/strong&gt; Add Telegram's &lt;code&gt;@RawDataBot&lt;/code&gt; to the group. It immediately posts a JSON dump; the &lt;code&gt;id&lt;/code&gt; inside the &lt;code&gt;chat&lt;/code&gt; object is your chat ID. Remove the bot afterward.&lt;/p&gt;

&lt;p&gt;Note that &lt;code&gt;@channelusername&lt;/code&gt; only works for public channels. Private groups have no username, so there the numeric ID is the only option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Formatting messages without breaking them
&lt;/h2&gt;

&lt;p&gt;Under &lt;strong&gt;Additional Fields&lt;/strong&gt;, &lt;strong&gt;Parse Mode&lt;/strong&gt; offers three choices: &lt;strong&gt;HTML&lt;/strong&gt; (the default), &lt;strong&gt;Markdown (Legacy)&lt;/strong&gt;, and &lt;strong&gt;MarkdownV2&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use HTML unless you have a specific reason not to.&lt;/strong&gt; MarkdownV2 looks like the modern option and is the biggest source of avoidable errors on this node, because it requires escaping a long list of reserved characters — including the period. A message ending in a normal sentence full stop can fail to parse. The resulting &lt;code&gt;Bad Request: can't parse entities&lt;/code&gt; error points at Telegram's parser, not at your text, so the cause is rarely obvious.&lt;/p&gt;

&lt;p&gt;HTML needs only &lt;code&gt;&amp;lt;b&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;i&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;code&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;a href=""&amp;gt;&lt;/code&gt;, and it doesn't blow up on punctuation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;Deploy finished&lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&lt;/span&gt;
Build &lt;span class="nt"&gt;&amp;lt;code&amp;gt;&lt;/span&gt;a1b2c3d&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&lt;/span&gt; is live.
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/logs"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;View logs&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two other fields worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disable Notification&lt;/strong&gt; — delivers silently. Right for high-frequency or low-priority alerts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disable WebPage Preview&lt;/strong&gt; — suppresses the link preview card, which otherwise dominates short messages containing a URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Receiving messages with the Telegram Trigger
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Telegram Trigger&lt;/strong&gt; is a separate node that starts a workflow when something happens in Telegram. It offers a long list of update types — &lt;strong&gt;Message&lt;/strong&gt;, &lt;strong&gt;Edited Message&lt;/strong&gt;, &lt;strong&gt;Channel Post&lt;/strong&gt;, &lt;strong&gt;Callback Query&lt;/strong&gt;, &lt;strong&gt;Inline Query&lt;/strong&gt;, &lt;strong&gt;Poll Answer&lt;/strong&gt;, &lt;strong&gt;Chat Member&lt;/strong&gt;, &lt;strong&gt;Chat Join Request&lt;/strong&gt;, and more, plus &lt;code&gt;*&lt;/code&gt; for all updates, which is the default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set this deliberately, and check it covers what you're actually sending.&lt;/strong&gt; Leaving it on &lt;code&gt;*&lt;/code&gt; means your workflow fires on events you never intended to handle, including your own bot's status changes. Narrowing it too far causes the opposite and more confusing problem: a trigger listening only for text messages silently ignores photos and documents. Nothing errors, because from the trigger's side nothing relevant happened. Worse, running the node manually often bypasses these filters, so it passes your test and then ignores real traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In groups, privacy mode limits what your bot can see.&lt;/strong&gt; Telegram enables it by default, which means a bot added to a group only receives messages that are commands or that mention it — not ordinary conversation. If your group bot reacts only sometimes, that's why. Turn it off through BotFather with &lt;code&gt;/setprivacy&lt;/code&gt;, then remove and re-add the bot to the group for the change to apply.&lt;/p&gt;

&lt;p&gt;Three options are worth setting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Download Images/Files&lt;/strong&gt; — fetches attachments and hands them to the next node as binary data rather than giving you a file reference you'd have to resolve yourself. &lt;strong&gt;Image Size&lt;/strong&gt; controls which resolution you get.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Restrict to Chat IDs&lt;/strong&gt; and &lt;strong&gt;Restrict to User IDs&lt;/strong&gt; — comma-separated allowlists. For a bot that isn't meant to be public, set at least one of these. A Telegram bot's username is discoverable, and without a restriction anyone who finds it can trigger your workflow.&lt;/p&gt;

&lt;p&gt;Unlike the &lt;a href="https://noderecipe.com/n8n-google-sheets" rel="noopener noreferrer"&gt;Google Sheets Trigger&lt;/a&gt;, which polls on a schedule, this one is a genuine webhook — Telegram pushes updates to n8n the moment they happen. That's why it's instant, and also why it has the constraint in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-webhook-per-bot problem
&lt;/h2&gt;

&lt;p&gt;This is the part that costs people an evening.&lt;/p&gt;

&lt;p&gt;n8n gives a workflow two webhook URLs: a &lt;strong&gt;test&lt;/strong&gt; URL used while you're clicking &lt;em&gt;Execute workflow&lt;/em&gt; in the editor, and a &lt;strong&gt;production&lt;/strong&gt; URL used when the workflow is active. Every other trigger handles this fine.&lt;/p&gt;

&lt;p&gt;Telegram doesn't, because &lt;strong&gt;Telegram only allows a single registered webhook per bot.&lt;/strong&gt; Whichever mode registered last wins. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You test in the editor. n8n registers the test URL. It works.&lt;/li&gt;
&lt;li&gt;You activate the workflow. n8n registers the production URL, overwriting the test one.&lt;/li&gt;
&lt;li&gt;You go back to the editor to debug. The test registration overwrites production — and your live bot goes quiet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing errors. The bot simply stops responding in the mode you're not currently using, which is exactly the symptom that sends people hunting through their workflow logic for a bug that isn't there.&lt;/p&gt;

&lt;p&gt;There are two clean fixes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create a second bot for development.&lt;/strong&gt; Run &lt;code&gt;/newbot&lt;/code&gt; again, make a separate n8n credential, and point your development copy of the workflow at it. This is the right answer for anything real — you get an isolated test environment and your live bot is never interrupted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Or deactivate the workflow while testing, then reactivate it.&lt;/strong&gt; Free, but it means your bot is down whenever you're working on it, and it's easy to forget the reactivation step.&lt;/p&gt;

&lt;p&gt;If you run more than one workflow off the same bot, the same rule applies: they compete for the single webhook slot. Route all updates into &lt;strong&gt;one&lt;/strong&gt; trigger and branch with a Switch node, rather than running several triggers on one token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skip the build.&lt;/strong&gt; The &lt;em&gt;Telegram Command Bot&lt;/em&gt; workflow is ready to import — &lt;a href="https://noderecipe.com/workflows/telegram-command-bot.json" rel="noopener noreferrer"&gt;grab the JSON&lt;/a&gt; and paste it onto the canvas, or browse &lt;a href="https://noderecipe.com/templates" rel="noopener noreferrer"&gt;all the templates&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This isn't unique to Telegram — &lt;a href="https://noderecipe.com/n8n-slack-notifications" rel="noopener noreferrer"&gt;Slack has the same one-request-URL-per-app rule&lt;/a&gt;, with the same symptom and the same fix. It's worth recognizing the pattern, because it shows up wherever a platform tracks a single callback address per integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Buttons, replies, and inline keyboards
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reply Markup&lt;/strong&gt; on the Send Message operation turns a message into something interactive. Four types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inline Keyboard&lt;/strong&gt; — buttons attached under the message itself. The usual choice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reply Keyboard&lt;/strong&gt; — replaces the user's keyboard with preset options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reply Keyboard Remove&lt;/strong&gt; — dismisses that keyboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Force Reply&lt;/strong&gt; — makes the client open a reply box automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Inline keyboard buttons emit a &lt;strong&gt;Callback Query&lt;/strong&gt; rather than a message, so your Telegram Trigger must include that update type or the taps go nowhere.&lt;/p&gt;

&lt;p&gt;One requirement people miss: after handling a callback you should run the &lt;strong&gt;Callback → Answer Query&lt;/strong&gt; operation. Until you do, Telegram shows a loading spinner on the button in the user's client. The action succeeds, but the interface looks broken.&lt;/p&gt;

&lt;p&gt;To reply in the same thread rather than as a new message, set &lt;strong&gt;Reply To Message ID&lt;/strong&gt; from the incoming message. In forum supergroups, &lt;strong&gt;Message Thread ID&lt;/strong&gt; keeps replies inside the right topic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sending photos and files
&lt;/h2&gt;

&lt;p&gt;Message operations cover &lt;strong&gt;Send Photo&lt;/strong&gt;, &lt;strong&gt;Send Document&lt;/strong&gt;, &lt;strong&gt;Send Video&lt;/strong&gt;, &lt;strong&gt;Send Audio&lt;/strong&gt;, &lt;strong&gt;Send Animation&lt;/strong&gt;, &lt;strong&gt;Send Sticker&lt;/strong&gt;, &lt;strong&gt;Send Media Group&lt;/strong&gt;, and &lt;strong&gt;Send Location&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each takes either binary data from a previous node — enable &lt;strong&gt;Binary File&lt;/strong&gt; — or a reference. For references you have two options: an HTTP URL that Telegram fetches itself, or a &lt;code&gt;file_id&lt;/code&gt; from a file Telegram already has. Prefer &lt;code&gt;file_id&lt;/code&gt; when resending something the bot has seen before; it's instant and skips the upload entirely.&lt;/p&gt;

&lt;p&gt;Going the other way, &lt;strong&gt;File → Get File&lt;/strong&gt; retrieves a file a user sent to your bot.&lt;/p&gt;

&lt;p&gt;Two limits worth planning around: Send Animation is documented for GIFs and soundless H.264/MPEG-4 up to 50 MB, and Telegram's Bot API is far more restrictive about uploads than the regular Telegram client. If you're moving large files, check the current Bot API limits rather than assuming client behavior applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common errors and fixes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Bad request: bad webhook: An HTTPS URL must be provided for webhook&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Telegram won't send updates to anything but a public HTTPS address. This appears when n8n sits behind a reverse proxy that hasn't been told its own public URL. Set the &lt;code&gt;WEBHOOK_URL&lt;/code&gt; environment variable to your public HTTPS address and terminate TLS in the proxy. That variable is covered in the &lt;a href="https://noderecipe.com/n8n-docker-setup" rel="noopener noreferrer"&gt;Docker setup guide&lt;/a&gt;, and the full proxy configuration in the &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;self-hosting walkthrough&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Error: Forbidden: bot is not a participant of the channel&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
The bot isn't in the channel. Add it through the channel's settings by searching its username — and make it an administrator, which channels require for posting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bot never responds, and there's no error at all.&lt;/strong&gt;&lt;br&gt;
Almost always the webhook collision above. Check whether the workflow is active, and whether you last executed it from the editor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The execution hangs, waiting for a trigger event.&lt;/strong&gt;&lt;br&gt;
On self-hosted instances this is typically a reverse proxy without websocket support. Enable websocket proxying in Nginx, Caddy, Apache, or Traefik.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Bad Request: can't parse entities&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Your Parse Mode doesn't match your text. Switch to HTML, or escape every reserved character MarkdownV2 requires.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Messages stop sending during a bulk run.&lt;/strong&gt;&lt;br&gt;
Telegram's API caps sending at roughly 30 messages per second. Batch with a Loop Over Items node and add a delay between batches rather than firing a node once per recipient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Messages arrive with an n8n advert appended.&lt;/strong&gt;&lt;br&gt;
Turn off &lt;strong&gt;Append n8n Attribution&lt;/strong&gt; under Additional Fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chat ID is rejected as invalid.&lt;/strong&gt;&lt;br&gt;
For groups, you're probably missing the leading &lt;code&gt;-&lt;/code&gt;. Group IDs are negative numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can my bot message someone who hasn't contacted it first?&lt;/strong&gt;&lt;br&gt;
No. Telegram requires the user to start the conversation, which is a deliberate anti-spam rule. For groups and channels, the bot has to be a member.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a server to build a Telegram bot in n8n?&lt;/strong&gt;&lt;br&gt;
Only for receiving. Sending works from a local instance. The trigger needs a publicly reachable HTTPS URL because Telegram pushes updates to you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can two workflows use the same bot?&lt;/strong&gt;&lt;br&gt;
Not reliably — they compete for the bot's single webhook registration. Use one trigger and branch internally, or create a second bot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is a Telegram bot free?&lt;/strong&gt;&lt;br&gt;
Yes. Telegram's Bot API has no cost. Your only expense is wherever n8n runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I log messages to a spreadsheet?&lt;/strong&gt;&lt;br&gt;
Chain a Google Sheets node onto the Telegram Trigger and append a row per message — the pattern is in the &lt;a href="https://noderecipe.com/n8n-google-sheets" rel="noopener noreferrer"&gt;Google Sheets integration guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the bot respond to slash commands like &lt;code&gt;/status&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
Yes. Commands arrive as ordinary message text, so branch on the text with a Switch node. Register the command list with BotFather via &lt;code&gt;/setcommands&lt;/code&gt; so it autocompletes for users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the bot handle voice notes?&lt;/strong&gt;&lt;br&gt;
Yes, and it's one of the better uses for a Telegram bot. Download the file with the Telegram node, then transcribe it — the &lt;a href="https://noderecipe.com/n8n-openai-integration" rel="noopener noreferrer"&gt;OpenAI integration guide&lt;/a&gt; has that build step by step.&lt;/p&gt;




&lt;p&gt;Official references worth bookmarking: n8n's &lt;a href="https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.telegram/" rel="noopener noreferrer"&gt;Telegram node documentation&lt;/a&gt; and Telegram's own &lt;a href="https://core.telegram.org/bots/api" rel="noopener noreferrer"&gt;Bot API reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want it prebuilt?&lt;/strong&gt; Importable Telegram workflows — a command-driven bot with the &lt;code&gt;@botname&lt;/code&gt; suffix already handled, and an error alerter — are on our &lt;a href="https://noderecipe.com/templates" rel="noopener noreferrer"&gt;templates page&lt;/a&gt;. Need the public HTTPS URL the trigger depends on? That's the &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;self-hosting guide&lt;/a&gt;, and &lt;a href="https://noderecipe.com/n8n-docker-setup" rel="noopener noreferrer"&gt;n8n in Docker&lt;/a&gt; if you're starting from scratch.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>telegram</category>
      <category>chatbot</category>
    </item>
    <item>
      <title>n8n Google Sheets Integration: Connect, Read, and Write Rows (2026)</title>
      <dc:creator>Ali Ilyas</dc:creator>
      <pubDate>Sun, 09 Aug 2026 10:48:16 +0000</pubDate>
      <link>https://dev.to/ali_ilyas/n8n-google-sheets-integration-connect-read-and-write-rows-2026-499k</link>
      <guid>https://dev.to/ali_ilyas/n8n-google-sheets-integration-connect-read-and-write-rows-2026-499k</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://noderecipe.com/n8n-google-sheets" rel="noopener noreferrer"&gt;noderecipe.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Google Sheets is the integration most people wire up first in n8n, and the node itself is easy. Almost all of the difficulty is in the ten minutes before it: getting Google to hand n8n a working credential. That step has more places to go wrong than the entire rest of the workflow, and most guides skip past it in two sentences.&lt;/p&gt;

&lt;p&gt;This one doesn't. We'll set up the credential properly, then build the three things you'll actually use — appending rows, reading rows with a filter, and updating a row that already exists — and finish with the gotchas that quietly cost people an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A running n8n instance. Either &lt;a href="https://n8n.io/cloud/" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; or a self-hosted one — the difference matters a lot for credentials, and we'll cover it below. If you don't have one yet, the quickest local option is &lt;a href="https://noderecipe.com/n8n-docker-setup" rel="noopener noreferrer"&gt;n8n in Docker&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A Google account.&lt;/li&gt;
&lt;li&gt;A spreadsheet with a header row. n8n reads your column names from the first row, so a sheet whose first row is data rather than headers will behave strangely from the start.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything below was checked against n8n 2.x. Where a detail changed between major versions, it's called out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting Google Sheets to n8n
&lt;/h2&gt;

&lt;p&gt;n8n supports two ways to authenticate with Google: &lt;strong&gt;OAuth2&lt;/strong&gt; and a &lt;strong&gt;Service Account&lt;/strong&gt;. Both work with Google Sheets. n8n recommends OAuth2 as the default, and for most people that's the right call.&lt;/p&gt;

&lt;p&gt;The important fork is where your n8n runs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On n8n Cloud&lt;/strong&gt;, Google Sheets is one of the nodes covered by managed OAuth2. You pick the credential, click through Google's consent screen, and you're done. No Google Cloud Console, no client ID. If you're on Cloud, skip to &lt;a href="https://noderecipe.com/n8n-google-sheets#the-operations-and-which-one-you-want" rel="noopener noreferrer"&gt;the operations&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-hosted&lt;/strong&gt;, managed OAuth2 isn't available. You have to create your own OAuth client in Google Cloud Console, or use a service account. This is the step people get stuck on, so it gets its own section.&lt;/p&gt;

&lt;p&gt;That gap is worth understanding rather than resenting. The managed credential works because n8n owns a verified OAuth app registered against its own domain. A self-hosted instance is a different application at a different address as far as Google is concerned, so Google needs you to register it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up custom OAuth2, step by step
&lt;/h2&gt;

&lt;p&gt;Budget ten minutes the first time. Once the Google Cloud project exists, adding more Google services to it takes about one minute each.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create a Google Cloud project
&lt;/h3&gt;

&lt;p&gt;In the &lt;a href="https://console.cloud.google.com/" rel="noopener noreferrer"&gt;Google Cloud Console&lt;/a&gt;, open the project dropdown in the top bar and choose &lt;strong&gt;New project&lt;/strong&gt;. Name it something you'll recognize — &lt;code&gt;n8n-automation&lt;/code&gt; is fine. Make sure it's selected in the top bar before continuing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Enable both APIs
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;APIs &amp;amp; Services → Library&lt;/strong&gt;. Enable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Google Sheets API&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Google Drive API&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enabling only the Sheets API is the single most common mistake here. n8n uses the Drive API to list your spreadsheets in the &lt;strong&gt;Document&lt;/strong&gt; dropdown, so without it the node authenticates fine and then shows you an empty list — which looks like a permissions bug and isn't one. Google Docs and Slides have the same requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Configure the OAuth consent screen
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;APIs &amp;amp; Services → OAuth consent screen&lt;/strong&gt; and select &lt;strong&gt;Get started&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enter an app name and user support email.&lt;/li&gt;
&lt;li&gt;Choose your audience: &lt;strong&gt;Internal&lt;/strong&gt; limits access to accounts in your Google Workspace organization. &lt;strong&gt;External&lt;/strong&gt; allows any Google account, and is what you'll pick if you're on a personal Gmail account.&lt;/li&gt;
&lt;li&gt;Add contact email addresses and accept the User Data Policy.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Branding → Authorized domains&lt;/strong&gt;, add &lt;code&gt;n8n.cloud&lt;/code&gt; if you're on Cloud, or your own domain if you're self-hosting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Read this part before you continue.&lt;/strong&gt; If you choose External, your app starts in &lt;strong&gt;Testing&lt;/strong&gt; mode. Two consequences follow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Only Google accounts you explicitly add as test users can complete the OAuth flow. Add yours under &lt;strong&gt;Audience&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consent and tokens expire after seven days.&lt;/strong&gt; Your workflow runs perfectly all week and then fails on day eight with an auth error, for no reason you changed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That second one is the highest-frustration item on this page, because the failure arrives long after the setup that caused it. If this is a workflow you intend to keep, publish the app rather than leaving it in Testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Create the OAuth client
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;APIs &amp;amp; Services → Credentials → + Create credentials → OAuth client ID&lt;/strong&gt;, and choose &lt;strong&gt;Web application&lt;/strong&gt; as the application type.&lt;/p&gt;

&lt;p&gt;Now switch to n8n. Create a new &lt;strong&gt;Google Sheets OAuth2 API&lt;/strong&gt; credential, and copy the &lt;strong&gt;OAuth Redirect URL&lt;/strong&gt; it displays. It follows this shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://your-n8n-domain.com/rest/oauth2-credential/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste that into &lt;strong&gt;Authorized redirect URIs&lt;/strong&gt; in Google Cloud Console and select &lt;strong&gt;Create&lt;/strong&gt;. Google shows you the &lt;strong&gt;Client ID&lt;/strong&gt; and &lt;strong&gt;Client Secret&lt;/strong&gt; in a modal — copy both now, because the secret isn't fully visible again later.&lt;/p&gt;

&lt;p&gt;If you're developing locally, this works without SSL or port forwarding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:5678/rest/oauth2-credential/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the URL exactly as n8n gives it to you, including the protocol and port number. A &lt;code&gt;redirect_uri_mismatch&lt;/code&gt; error almost always means one of those three differs — &lt;code&gt;http&lt;/code&gt; against &lt;code&gt;https&lt;/code&gt;, a missing &lt;code&gt;:5678&lt;/code&gt;, or a trailing slash. Google matches this string literally.&lt;/p&gt;

&lt;p&gt;One consequence for self-hosters: a public n8n instance needs a stable HTTPS address before this credential is worth creating, because the redirect URI is registered against a fixed URL. If your instance is still on a bare IP or a tunnel URL that changes, sort out &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;a real domain and certificate&lt;/a&gt; first — otherwise you'll be re-registering the redirect URI every time the address moves.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Finish in n8n
&lt;/h3&gt;

&lt;p&gt;Paste the Client ID and Client Secret into the n8n credential, click &lt;strong&gt;Sign in with Google&lt;/strong&gt;, complete the consent flow, and save. If your app is External and in Testing, you'll see an "unverified app" warning — expected, and you can proceed through it for an app you created yourself.&lt;/p&gt;

&lt;p&gt;The scopes n8n requests for Sheets are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.googleapis.com/auth/spreadsheets
https://www.googleapis.com/auth/drive.file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;drive.file&lt;/code&gt; is deliberately narrow: it grants access only to files opened or created through the app, not your whole Drive.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use a service account instead
&lt;/h2&gt;

&lt;p&gt;A service account is a robot Google account with its own email address. Instead of a consent flow, you share the spreadsheet with that address the same way you'd share it with a colleague.&lt;/p&gt;

&lt;p&gt;Use one when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The workflow belongs to a team or a server rather than to you personally. Service account access doesn't disappear when someone leaves.&lt;/li&gt;
&lt;li&gt;You want to avoid the seven-day token expiry entirely.&lt;/li&gt;
&lt;li&gt;The instance runs unattended and nobody will be around to re-authorize it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay with OAuth2 when the sheets are your own and you want the fastest setup. One caveat worth knowing: Google supports service accounts with Gmail only through domain-wide delegation, which Google discourages and which behaves inconsistently — so n8n recommends OAuth2 for Gmail specifically. For Sheets, both are fully supported.&lt;/p&gt;

&lt;p&gt;The thing people forget with service accounts: &lt;strong&gt;share the sheet with the service account's email address.&lt;/strong&gt; Nothing works until you do, and the resulting permission error doesn't say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  The operations, and which one you want
&lt;/h2&gt;

&lt;p&gt;The Google Sheets node has two resources. &lt;strong&gt;Document&lt;/strong&gt; operates on the spreadsheet file (Create, Delete). &lt;strong&gt;Sheet Within Document&lt;/strong&gt; operates on a tab inside it, which is where nearly all real work happens:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Use it when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Append Row&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adding new records. Never touches existing rows.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Append or Update Row&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You don't know whether the record exists yet — the upsert.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Update Row&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The row definitely exists and you're changing it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Get Row(s)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reading data out, optionally filtered.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Clear&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wiping a sheet's contents before a fresh load.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Delete Rows or Columns&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Removing rows or columns by position.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Create&lt;/strong&gt; / &lt;strong&gt;Delete&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Adding or removing a tab.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're unsure between the first three: use &lt;strong&gt;Append Row&lt;/strong&gt; for logs and event streams where every entry is new, and &lt;strong&gt;Append or Update Row&lt;/strong&gt; for anything keyed on an identifier — a customer email, an order ID — where running the workflow twice shouldn't produce two rows.&lt;/p&gt;

&lt;p&gt;Every operation shares the same two selectors. &lt;strong&gt;Document&lt;/strong&gt; and &lt;strong&gt;Sheet&lt;/strong&gt; can each be picked &lt;strong&gt;From list&lt;/strong&gt;, &lt;strong&gt;By URL&lt;/strong&gt;, or &lt;strong&gt;By ID&lt;/strong&gt; (Sheet also offers &lt;strong&gt;By Name&lt;/strong&gt;). Use &lt;em&gt;From list&lt;/em&gt; while building, since it confirms your credential can actually see the file. Switch to &lt;em&gt;By ID&lt;/em&gt; or an expression when the sheet is chosen dynamically at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build 1: append incoming data to a sheet
&lt;/h2&gt;

&lt;p&gt;The most common workflow in n8n: something happens, write a row about it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a trigger. A &lt;strong&gt;Webhook&lt;/strong&gt; node works for form submissions; a &lt;strong&gt;Schedule Trigger&lt;/strong&gt; works for periodic jobs.&lt;/li&gt;
&lt;li&gt;Add a &lt;strong&gt;Google Sheets&lt;/strong&gt; node. Set Resource to &lt;strong&gt;Sheet Within Document&lt;/strong&gt; and Operation to &lt;strong&gt;Append Row&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select your Document and Sheet.&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Mapping Column Mode&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last setting is the one to understand. It has three options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Map Automatically&lt;/strong&gt; — n8n matches incoming field names to column headers. Clean and fast when your data's keys already match your headers exactly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map Each Column Manually&lt;/strong&gt; — n8n lists your sheet's columns and you drag or type an expression into each. More clicks, far more predictable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing&lt;/strong&gt; — pass the raw data through untouched, for cases where you're shaping it upstream.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with &lt;strong&gt;Map Each Column Manually&lt;/strong&gt; while you're learning. Automatic mapping is silent when it fails: a field whose name doesn't match a header simply doesn't get written, and you get a row with blank cells rather than an error.&lt;/p&gt;

&lt;p&gt;Three options are worth setting under &lt;strong&gt;Options&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cell Format&lt;/strong&gt; — &lt;em&gt;Let Google Sheets format&lt;/em&gt; (the default) means Google interprets your values the way it would if you typed them, so &lt;code&gt;1/2&lt;/code&gt; may become a date and a leading &lt;code&gt;+&lt;/code&gt; may become a formula. &lt;em&gt;Let n8n format&lt;/em&gt; writes values as-is. If you're writing product codes, phone numbers, or anything with leading zeros, choose &lt;em&gt;Let n8n format&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Location on Sheet&lt;/strong&gt; — sets which row holds your headers and which row your data starts on. Change these if your sheet has a title block above the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handling extra fields in input&lt;/strong&gt; — what happens when your data contains a key with no matching column: &lt;em&gt;Insert in New Column(s)&lt;/em&gt;, &lt;em&gt;Ignore Them&lt;/em&gt;, or &lt;em&gt;Error&lt;/em&gt;. For a workflow you want to trust, &lt;em&gt;Error&lt;/em&gt; is the honest choice — it surfaces schema drift instead of hiding it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build 2: read rows and filter them
&lt;/h2&gt;

&lt;p&gt;Set Operation to &lt;strong&gt;Get Row(s)&lt;/strong&gt;. With no filters, it returns every row as an n8n item.&lt;/p&gt;

&lt;p&gt;Add a &lt;strong&gt;Filter&lt;/strong&gt; to narrow it: pick a &lt;strong&gt;Column&lt;/strong&gt;, give it a &lt;strong&gt;Value&lt;/strong&gt;, and only matching rows come back. Under &lt;strong&gt;Options&lt;/strong&gt;, &lt;strong&gt;When Filter Has Multiple Matches&lt;/strong&gt; decides whether you get the first match or all of them — set it deliberately, because the default behavior surprises people building "look up this customer" workflows.&lt;/p&gt;

&lt;p&gt;Also under &lt;strong&gt;Options&lt;/strong&gt;, &lt;strong&gt;Output Formatting&lt;/strong&gt; controls what you actually receive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Values unformatted&lt;/strong&gt; — the raw underlying value. A currency cell comes through as &lt;code&gt;1234.5&lt;/code&gt;, not &lt;code&gt;$1,234.50&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Values formatted&lt;/strong&gt; — the display string, as shown in the sheet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Formulas&lt;/strong&gt; — the formula text rather than its result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choose &lt;em&gt;unformatted&lt;/em&gt; when the number feeds a calculation, and &lt;em&gt;formatted&lt;/em&gt; when it feeds a message someone will read. Getting this backwards produces the classic bug where a total is off because &lt;code&gt;$1,234.50&lt;/code&gt; was parsed as a string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build 3: update a row instead of duplicating it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Append or Update Row&lt;/strong&gt; is the operation that makes a workflow safe to re-run.&lt;/p&gt;

&lt;p&gt;Set Mapping Column Mode to &lt;strong&gt;Map Each Column Manually&lt;/strong&gt;, and a &lt;strong&gt;Column to Match On&lt;/strong&gt; appears. That column is the identity key. n8n looks for a row whose value in that column matches your incoming data — if it finds one, it updates that row; if not, it appends a new one.&lt;/p&gt;

&lt;p&gt;Pick something genuinely unique. Email addresses and order IDs work. Names don't.&lt;/p&gt;

&lt;p&gt;Two limits to plan around:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can only match on one column.&lt;/strong&gt; There's no composite key. If your identity is genuinely two fields, build a combined key column in the sheet — &lt;code&gt;email|date&lt;/code&gt;, say — and match on that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "Use Append" option is a performance trade.&lt;/strong&gt; Enabling it uses Google's append endpoint, which means fewer API calls and better throughput on large runs. The documented cost: if your existing sheet data has gaps or breaks between rows and columns, n8n can place new data in the wrong location. Leave it off on a messy sheet; turn it on for a clean, densely-populated one where you're writing a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Triggering a workflow when a row is added
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Google Sheets Trigger&lt;/strong&gt; is a separate node from the one above, and it starts a workflow when the sheet changes. Three events are available:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Row added&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Row updated&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Row added or updated&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a polling trigger, not a push. Google doesn't notify n8n; n8n checks the sheet on the interval you configure. Two things follow. Detection is not instant — it's bounded by your polling interval. And a row added and then deleted between two polls is never seen at all.&lt;/p&gt;

&lt;p&gt;For anything needing true real-time behavior, have the upstream system call an n8n Webhook node directly rather than routing it through a spreadsheet. That needs a publicly reachable instance, which is the &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;self-hosting setup&lt;/a&gt;. The &lt;a href="https://noderecipe.com/n8n-telegram-bot" rel="noopener noreferrer"&gt;Telegram Trigger&lt;/a&gt; is a good example of the push model for comparison — updates arrive the instant they happen, at the cost of requiring a public HTTPS address.&lt;/p&gt;

&lt;p&gt;Two documented issues are worth knowing before they bite:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dates arrive as numbers like &lt;code&gt;45678.5&lt;/code&gt;.&lt;/strong&gt; That's Google's serial format — the whole number is days since December 30, 1899, and the decimal is the fraction of a 24-hour day. Fix it in the node: &lt;strong&gt;Add option → DateTime Render → Formatted String&lt;/strong&gt;. This applies when &lt;strong&gt;Trigger On&lt;/strong&gt; is set to &lt;em&gt;Row Added&lt;/em&gt;. The resulting format follows your spreadsheet's locale, which you can change under &lt;strong&gt;File → Settings → Locale&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The execution appears stuck waiting for a trigger event.&lt;/strong&gt; On self-hosted instances this is usually a reverse proxy without websocket support. Enable websocket proxying in Nginx, Caddy, Apache, or Traefik — whichever sits in front of n8n. The &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;self-hosting guide&lt;/a&gt; covers a configuration that has this set correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas that cost people hours
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Document dropdown is empty even though the credential connected.&lt;/strong&gt;&lt;br&gt;
The Google Drive API isn't enabled on your Google Cloud project. Sheets API alone authenticates but can't list files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;redirect_uri_mismatch&lt;/code&gt; when signing in.&lt;/strong&gt;&lt;br&gt;
The redirect URI in Google Cloud Console doesn't match n8n's byte for byte. Check protocol, port, and trailing slash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It worked for a week, then stopped.&lt;/strong&gt;&lt;br&gt;
Your OAuth app is External and still in Testing mode, where consent and tokens expire after seven days. Publish the app, or move to a service account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Column names were updated after the node was set up."&lt;/strong&gt;&lt;br&gt;
You changed headers in the sheet after configuring the node. n8n cached the old ones. Re-select the &lt;strong&gt;Mapping Column Mode&lt;/strong&gt; option to refresh the column list, then fix any parameters that referenced the old names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An array field writes as &lt;code&gt;[object Object]&lt;/code&gt; or errors.&lt;/strong&gt;&lt;br&gt;
Google Sheets cells hold single values, so an array has to be flattened into key/value pairs first. Use a &lt;strong&gt;Split Out&lt;/strong&gt; node to turn each array element into its own item and its own row, or a &lt;strong&gt;Code&lt;/strong&gt; node to join them into a string if one cell is what you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One row goes in, but you expected many.&lt;/strong&gt;&lt;br&gt;
n8n runs the node once per input item. If your data arrived as a single item containing an array of records, you get one row. Split it first — same &lt;strong&gt;Split Out&lt;/strong&gt; node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leading zeros vanish and codes turn into dates.&lt;/strong&gt;&lt;br&gt;
Google is interpreting your values. Set &lt;strong&gt;Cell Format&lt;/strong&gt; to &lt;em&gt;Let n8n format&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A permission error with a service account.&lt;/strong&gt;&lt;br&gt;
You didn't share the spreadsheet with the service account's email address. Open the sheet, hit Share, paste that address, give it Editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need a paid Google Workspace account?&lt;/strong&gt;&lt;br&gt;
No. A free personal Google account works. Workspace only matters if you want the &lt;strong&gt;Internal&lt;/strong&gt; audience option, which avoids the Testing-mode expiry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there a limit on how much data I can write?&lt;/strong&gt;&lt;br&gt;
Google's Sheets API enforces per-minute quotas per project and per user, so very high-frequency writes will hit rate limits. Batch your writes rather than calling the node once per record, and be aware that a spreadsheet is a poor substitute for a database above a few tens of thousands of rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can n8n read a sheet shared with me by someone else?&lt;/strong&gt;&lt;br&gt;
Yes, provided your Google account has access. With OAuth2 the &lt;code&gt;drive.file&lt;/code&gt; scope covers files you open through the app. With a service account, the owner has to share the sheet with the service account address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does my number come through as a string?&lt;/strong&gt;&lt;br&gt;
Output Formatting is set to &lt;em&gt;Values formatted&lt;/em&gt;. Switch to &lt;em&gt;Values unformatted&lt;/em&gt; for the raw value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use one credential for Sheets, Drive, and Gmail?&lt;/strong&gt;&lt;br&gt;
You can reuse one Google Cloud project for all of them, but n8n uses a separate credential type per service. Enable each API in the same project and create each credential against the same OAuth client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this work the same on n8n Cloud and self-hosted?&lt;/strong&gt;&lt;br&gt;
The node is identical. Only the credential setup differs — Cloud gives you managed OAuth2, self-hosted requires the custom client above.&lt;/p&gt;




&lt;p&gt;Official references worth bookmarking: n8n's &lt;a href="https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.googlesheets/" rel="noopener noreferrer"&gt;Google Sheets node documentation&lt;/a&gt; and Google's &lt;a href="https://developers.google.com/workspace/sheets/api/scopes" rel="noopener noreferrer"&gt;Sheets API authorization scopes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want this as a working workflow?&lt;/strong&gt; Importable workflows built on Sheets — validated webhook capture, a lookup-before-write guard, and a scheduled heartbeat monitor — are on our &lt;a href="https://noderecipe.com/templates" rel="noopener noreferrer"&gt;templates page&lt;/a&gt;. Running n8n yourself and still setting things up? Start with &lt;a href="https://noderecipe.com/n8n-docker-setup" rel="noopener noreferrer"&gt;n8n in Docker&lt;/a&gt;, then &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;put it on a real domain with HTTPS&lt;/a&gt; so your OAuth redirect URI stops moving.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>googlesheets</category>
      <category>nocode</category>
    </item>
    <item>
      <title>n8n Workflow Not Triggering? Work Through It in This Order (2026)</title>
      <dc:creator>Ali Ilyas</dc:creator>
      <pubDate>Fri, 07 Aug 2026 10:04:49 +0000</pubDate>
      <link>https://dev.to/ali_ilyas/n8n-workflow-not-triggering-work-through-it-in-this-order-2026-1f7b</link>
      <guid>https://dev.to/ali_ilyas/n8n-workflow-not-triggering-work-through-it-in-this-order-2026-1f7b</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://noderecipe.com/n8n-workflow-not-triggering" rel="noopener noreferrer"&gt;noderecipe.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A workflow that won't trigger gives you almost nothing to work with. No error, no failed execution, no red node — just silence. So the instinct is to start rebuilding the trigger, which is usually the one thing that isn't broken.&lt;/p&gt;

&lt;p&gt;Work through the checks below in order instead. They're arranged by how often they turn out to be the cause, and the first two account for the clear majority of cases.&lt;/p&gt;

&lt;p&gt;One thing to know before you start, because it invalidates a lot of older advice: &lt;strong&gt;n8n renamed the "Active" toggle to a "Publish" button.&lt;/strong&gt; If you're following a guide that tells you to flip a switch labelled &lt;em&gt;Active&lt;/em&gt; and you can't find it, the guide predates the change. Same concept, different control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start here: is the workflow published?
&lt;/h2&gt;

&lt;p&gt;The single most common cause, and the easiest to overlook once you've been staring at the same canvas for an hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manual executions and production executions are different things.&lt;/strong&gt; Clicking &lt;strong&gt;Execute workflow&lt;/strong&gt; runs the workflow ad-hoc from the canvas. It does not make the trigger live. A workflow only responds to real events once you attach a non-manual trigger and &lt;strong&gt;publish&lt;/strong&gt; it.&lt;/p&gt;

&lt;p&gt;So: open the workflow and confirm it's published. Then confirm you published &lt;em&gt;the version you're looking at&lt;/em&gt; — editing a published workflow doesn't push the changes live on its own. Save, then publish again.&lt;/p&gt;

&lt;p&gt;This also catches a subtler variant. If your workflow has a &lt;strong&gt;Manual Trigger&lt;/strong&gt; node still attached from when you were building it, and no other trigger, there is nothing for n8n to activate. Manual triggers never fire on their own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After a bulk import or migration, check publish state explicitly.&lt;/strong&gt; Imported workflows can land effectively inactive while still looking normal in the workflow list. If a batch of workflows all went quiet at once and they were recently imported or migrated, check them one at a time rather than trusting the list view. &lt;a href="https://noderecipe.com/n8n-workflow-templates" rel="noopener noreferrer"&gt;Importing templates&lt;/a&gt; covers the other things that arrive half-configured — placeholders, node version mismatches, and instructions written against an older n8n.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Publish control is greyed out or missing
&lt;/h2&gt;

&lt;p&gt;A distinct problem worth separating out: you can't publish at all, so nothing about triggers applies yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can't click Publish.&lt;/strong&gt; Two usual causes. First, &lt;strong&gt;unsaved changes&lt;/strong&gt; — n8n won't publish a workflow that isn't saved, and every node edit puts it back into an unsaved state, so a tweak after your last save is enough. Hit save, and the control wakes up. Second, a &lt;strong&gt;validation problem in the trigger node&lt;/strong&gt; — an empty required field like a webhook path, or an unset HTTP method, blocks publishing even when the node looks fine at a glance. Open the trigger and look for a warning marker.&lt;/p&gt;

&lt;p&gt;Hover the control before guessing. n8n generally shows a tooltip naming the reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's no Publish control at all.&lt;/strong&gt; An empty canvas has nothing to publish. You need at least a trigger node and something connected to it. This confuses people following older video tutorials, where an &lt;em&gt;Active&lt;/em&gt; toggle was visible from the start on a blank workflow — that toggle no longer exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Are you using the test URL or the production URL?
&lt;/h2&gt;

&lt;p&gt;Every webhook-style trigger in n8n has two URLs, and they behave completely differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The test URL&lt;/strong&gt; only listens after you select &lt;strong&gt;Listen for test event&lt;/strong&gt;, and it stays open for &lt;strong&gt;120 seconds&lt;/strong&gt;. Its advantage is that incoming data appears in the editor, which makes it the right tool for debugging. Its disadvantage is that it goes dead two minutes later, which produces the classic "it worked when I tested it and stopped an hour later."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The production URL&lt;/strong&gt; is live whenever the workflow is published, and stays live until you unpublish it. Data flowing through it is &lt;strong&gt;not&lt;/strong&gt; shown in the editor — that's expected, not a fault. Check the executions list instead.&lt;/p&gt;

&lt;p&gt;If you registered the test URL with an external service, that service is now calling a URL that stopped listening. Swap it for the production URL and republish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the executions list before anything else
&lt;/h2&gt;

&lt;p&gt;This is the step that saves the most time, and it's the one people skip.&lt;/p&gt;

&lt;p&gt;Open the workflow's executions. What you see splits the problem in half:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No execution at all&lt;/strong&gt; — the trigger never fired. The cause is in this article: publish state, wrong URL, timezone, or a platform-side registration problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An execution that failed&lt;/strong&gt; — the trigger fired correctly and something downstream broke. That's a different problem entirely, and the execution will name the failing node. Stop looking at the trigger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An execution that succeeded but did nothing useful&lt;/strong&gt; — the trigger fired with empty or unexpected data. Jump to &lt;a href="https://noderecipe.com/n8n-workflow-not-triggering#it-triggers-but-nothing-happens" rel="noopener noreferrer"&gt;It triggers, but nothing happens&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Getting this wrong is how people spend an evening rebuilding a trigger that was working the whole time.&lt;/p&gt;

&lt;h2&gt;
  
  
  It works when I run it manually
&lt;/h2&gt;

&lt;p&gt;This deserves its own answer, because it's the most misleading symptom in the whole list. The workflow runs perfectly from the canvas, so the logic is obviously fine — and people conclude the trigger is broken when it usually isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manual execution and live execution don't apply the same filters.&lt;/strong&gt; Running a node by hand often bypasses the trigger's event filtering just to test the connection. Once published, the trigger enforces its filter strictly. So a Telegram Trigger set to receive only text messages will pass a manual test and then ignore every photo and document you send it — nothing errors, because from the trigger's point of view nothing relevant happened.&lt;/p&gt;

&lt;p&gt;The fix is to check what your trigger is actually subscribed to, not whether it responds when poked. In practice that means the allowed update or event types on app triggers, and the HTTP method on webhooks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credentials also behave differently.&lt;/strong&gt; A manual run happens while you're sitting there with a fresh session. A production run at 3am uses a stored token that may have expired since. If manual works and scheduled doesn't, suspect the token before the trigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhook triggers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The URL returns 404.&lt;/strong&gt; Almost always the workflow isn't published, or you're calling the test URL outside its 120-second window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two workflows share a path.&lt;/strong&gt; n8n allows only one webhook per path and method combination. A second published workflow using the same pair won't register, and publishing fails. Note it's the &lt;strong&gt;pair&lt;/strong&gt; that has to be unique — a &lt;code&gt;GET&lt;/code&gt; and a &lt;code&gt;POST&lt;/code&gt; on the same path coexist happily, which is why one method can work while the other refuses. Unpublish the conflicting workflow, or change the path.&lt;/p&gt;

&lt;p&gt;If both workflows are yours, webhooks are the wrong tool for the job. Use the &lt;strong&gt;When Executed by Another Workflow&lt;/strong&gt; trigger with an &lt;strong&gt;Execute Sub-workflow&lt;/strong&gt; node instead: no URLs, no path conflicts, and data passes directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sender uses a method your node doesn't accept.&lt;/strong&gt; A Webhook node listens for a single HTTP method by default. If the service sends &lt;code&gt;POST&lt;/code&gt; and your node expects &lt;code&gt;GET&lt;/code&gt;, nothing arrives and nothing errors. Either match the method or enable &lt;strong&gt;Allow Multiple HTTP Methods&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your IP allowlist blocks legitimate traffic.&lt;/strong&gt; Behind a reverse proxy, n8n sees the proxy's IP rather than the caller's. Set &lt;code&gt;N8N_PROXY_HOPS&lt;/code&gt; to the number of proxies in front of n8n so the real client IP is resolved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The webhook URL points at localhost.&lt;/strong&gt; External services can't reach your machine. n8n advertises whatever address it believes it has, so behind a proxy you need &lt;code&gt;WEBHOOK_URL&lt;/code&gt; set to your public HTTPS address — the variable is covered in the &lt;a href="https://noderecipe.com/n8n-docker-setup" rel="noopener noreferrer"&gt;Docker setup guide&lt;/a&gt;, and the proxy configuration in the &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;self-hosting walkthrough&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It responds, but the caller times out.&lt;/strong&gt; Long-running workflows can exceed the caller's patience. Split it: one webhook that responds immediately and starts the work, and a second the caller polls for status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schedule triggers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The timezone is wrong.&lt;/strong&gt; The most common cause by a wide margin, and it's easy to misread as "didn't fire" when it actually fired at 3am. Set &lt;code&gt;GENERIC_TIMEZONE&lt;/code&gt; on self-hosted instances, or the timezone in your n8n Cloud settings. You can also override it per workflow: &lt;strong&gt;three-dot menu → Settings → Timezone&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your cron expression is invalid.&lt;/strong&gt; When &lt;strong&gt;Trigger Interval&lt;/strong&gt; is &lt;strong&gt;Custom (Cron)&lt;/strong&gt;, a malformed expression means n8n never schedules anything. Validate it — dropping the seconds column — at crontab.guru.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The schedule restarts from when you publish.&lt;/strong&gt; This one surprises people: after changing an interval, the schedule recalculates from the moment of publication, not from the original start time. If you republish an hourly workflow at 2:40, it runs at 3:40, not 3:00. To apply a changed interval at all, unpublish and publish a new version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variables aren't picking up new values.&lt;/strong&gt; Variables in a Schedule Trigger are evaluated at publish time. Changing a variable's value does nothing until you publish a new version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The instance was down when the run was due.&lt;/strong&gt; By default n8n schedules in memory, and &lt;strong&gt;a run whose time passes during downtime is skipped rather than caught up.&lt;/strong&gt; A nightly job on a server that reboots at 3am simply doesn't happen that night, with no record that it should have.&lt;/p&gt;

&lt;p&gt;If that matters, self-hosted instances can opt into the durable scheduler, which records runs in a database-backed queue in advance so a missed run fires when the instance recovers. It's &lt;strong&gt;off by default&lt;/strong&gt; and needs both &lt;code&gt;N8N_SCHEDULER_ENABLED=true&lt;/code&gt; and &lt;code&gt;N8N_USE_WORKFLOW_PUBLICATION_SERVICE=true&lt;/code&gt;. Worth enabling for anything where a skipped run has consequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  App triggers: Telegram, Slack, Google Sheets
&lt;/h2&gt;

&lt;p&gt;Service-specific triggers add their own failure modes on top of everything above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Telegram and Slack: the single-callback rule.&lt;/strong&gt; Telegram permits one webhook per bot; Slack permits one request URL per app. n8n uses a test URL in the editor and a production URL when published, so the two overwrite each other. Symptom: the workflow works in one mode and goes silent in the other, with no error. Fix: a separate bot or app for development. Full detail in the &lt;a href="https://noderecipe.com/n8n-telegram-bot" rel="noopener noreferrer"&gt;Telegram guide&lt;/a&gt; and the &lt;a href="https://noderecipe.com/n8n-slack-notifications" rel="noopener noreferrer"&gt;Slack guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trigger is subscribed to the wrong event.&lt;/strong&gt; The most common version of this: a &lt;a href="https://noderecipe.com/n8n-telegram-bot" rel="noopener noreferrer"&gt;Telegram Trigger&lt;/a&gt; left on its default update types receives text messages but not photos or documents. You send a file, nothing happens, and no error appears — the trigger correctly ignored an event it wasn't listening for. Add the update types you actually need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Telegram bot in a group can't see most messages.&lt;/strong&gt; Telegram's bot privacy mode is on by default, which limits a bot in a group to messages that are commands or that mention it directly. If your group bot only reacts sometimes, this is why. Change it through BotFather with &lt;code&gt;/setprivacy&lt;/code&gt;, then remove and re-add the bot for it to take effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polling triggers aren't instant.&lt;/strong&gt; The &lt;a href="https://noderecipe.com/n8n-google-sheets" rel="noopener noreferrer"&gt;Google Sheets Trigger&lt;/a&gt; checks on an interval rather than being pushed to. Detection is bounded by that interval, and a change made and reverted between two polls is never seen. That's the design, not a bug — if you need immediacy you need a push-based trigger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credentials expired.&lt;/strong&gt; A trigger that stops after working for days is usually auth, not logic. Two specific traps worth checking: a Google OAuth app left in &lt;strong&gt;Testing&lt;/strong&gt; mode expires consent after seven days, and a Slack app with &lt;strong&gt;token rotation&lt;/strong&gt; enabled expires tokens every 12 hours. Both are covered in their respective guides.&lt;/p&gt;

&lt;p&gt;The nasty part is how an expired credential presents on a polling trigger. It often doesn't error — the poll returns an &lt;strong&gt;empty result&lt;/strong&gt; instead. So the workflow keeps "running fine" forever while doing nothing, and no failure alert ever fires.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bot lost channel access.&lt;/strong&gt; If a bot was removed from a channel, or a Google service account lost access to a sheet, events stop arriving with no error on the n8n side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosted-only causes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The host went to sleep.&lt;/strong&gt; Free and low-tier hosting plans suspend idle containers, and a suspended instance runs no schedules. This is a common cause of an every-minute trigger that fires erratically or not at all. If your instance sleeps, either move to an always-on plan or accept that time-based triggers won't be reliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;n8n isn't running.&lt;/strong&gt; Obvious, and worth ruling out in ten seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
docker logs n8n &lt;span class="nt"&gt;--tail&lt;/span&gt; 50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A container that exited or is restart-looping explains everything above it. See &lt;a href="https://noderecipe.com/n8n-docker-setup" rel="noopener noreferrer"&gt;common Docker errors&lt;/a&gt; if it won't stay up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your public URL changed.&lt;/strong&gt; Tunnel URLs from ngrok and similar tools rotate. Every webhook you registered with an external service now points somewhere that no longer exists. This is the strongest practical argument for &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;a stable domain with HTTPS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The reverse proxy is dropping requests.&lt;/strong&gt; Check the proxy's access log, not just n8n's. If the request never reaches n8n, n8n can't tell you about it. Trigger nodes that appear to hang while listening usually mean websocket support isn't enabled in Nginx, Caddy, Apache, or Traefik.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTPS isn't actually terminating.&lt;/strong&gt; Telegram and Slack both refuse to deliver to plain &lt;code&gt;http://&lt;/code&gt;. A valid certificate isn't optional for those triggers.&lt;/p&gt;

&lt;h2&gt;
  
  
  It triggers, but nothing happens
&lt;/h2&gt;

&lt;p&gt;The execution list shows successful runs, but nothing downstream changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero items reached the next node.&lt;/strong&gt; A node that receives no items doesn't run, and this cascades silently through the rest of the workflow. Look at the item count on each connection — the first &lt;code&gt;0&lt;/code&gt; is your answer. A Filter or IF node that matched nothing is the usual culprit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A filter is stricter than you think.&lt;/strong&gt; String comparisons are exact. &lt;code&gt;"Active"&lt;/code&gt; doesn't match &lt;code&gt;"active"&lt;/code&gt;, and a trailing space breaks equality invisibly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trigger fired on the wrong event.&lt;/strong&gt; A trigger set to all events fires on things you never meant to handle — including your own bot's activity. Narrow the event type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You're editing one workflow and running another.&lt;/strong&gt; Duplicated workflows with near-identical names are more common than they sound. Confirm the execution belongs to the workflow you're editing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the next failure visible
&lt;/h2&gt;

&lt;p&gt;Silent failure is the real problem here. A wrong trigger is only the symptom, and you found it by noticing something didn't happen — which means you found it late.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An Error Trigger is not enough, and this is the part people get wrong.&lt;/strong&gt; An error workflow fires when an execution &lt;em&gt;fails&lt;/em&gt;. Every failure mode in this article produces &lt;strong&gt;no execution at all&lt;/strong&gt;, so there is nothing to fail and nothing to report. The alert you set up specifically to catch problems is structurally blind to the most common one.&lt;/p&gt;

&lt;p&gt;Three principles make monitoring actually work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alert on absence of success, not presence of failure.&lt;/strong&gt; Have each critical workflow write a timestamp somewhere cheap — a database row, a key-value store, a spreadsheet — and run a separate watcher that alerts when a timestamp goes stale: &lt;em&gt;"this should run hourly, last success was three hours ago."&lt;/em&gt; That single check catches deactivated triggers, expired tokens, and the empty-poll case above, none of which ever raise an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write that timestamp conditionally on real output.&lt;/strong&gt; If you log it unconditionally at the end of the run, a green execution that produced nothing still marks itself healthy — and you've built a monitor that confirms your silent failure is fine. Assert on the actual result: rows returned, message sent, record created.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The watcher must not share fate with what it watches.&lt;/strong&gt; This is the one that catches people out. A heartbeat running inside the same n8n instance dies with that instance and goes quiet at exactly the moment you needed it to shout. The same applies to any shared dependency — using the same credential, the same API provider, or the same database in both the workflow and its error handler means one outage blinds both at once. Put the watcher somewhere else: a cron job on a different machine, or an external dead-man's-switch service that pages you when the ping stops.&lt;/p&gt;

&lt;p&gt;With that in place, an error workflow is still worth attaching for loud failures. Set one in &lt;strong&gt;workflow settings&lt;/strong&gt; using an &lt;strong&gt;Error Trigger&lt;/strong&gt; node, and make the alert name the workflow, the node, and the input — a bare "workflow failed" with no context just trains you to ignore alerts. Route it to &lt;a href="https://noderecipe.com/n8n-slack-notifications" rel="noopener noreferrer"&gt;Slack&lt;/a&gt; or &lt;a href="https://noderecipe.com/n8n-telegram-bot" rel="noopener noreferrer"&gt;Telegram&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why does my workflow work when I click Execute but not on its own?&lt;/strong&gt;&lt;br&gt;
Manual execution and production execution are separate. Publish the workflow, and make sure it has a trigger other than the Manual Trigger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did my webhook work for two minutes and then stop?&lt;/strong&gt;&lt;br&gt;
That was the test URL, which listens for 120 seconds. Use the production URL for anything external.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My scheduled workflow skipped a night. Where did it go?&lt;/strong&gt;&lt;br&gt;
The instance was probably down when the run was due. The default in-memory scheduler skips missed runs rather than catching them up. The durable scheduler changes that, but it's off by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can two workflows use the same webhook path?&lt;/strong&gt;&lt;br&gt;
No — one webhook per path and method. Change the path or unpublish the other workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I have to republish after every edit?&lt;/strong&gt;&lt;br&gt;
Yes, for the change to affect production runs. And for Schedule Triggers, publishing restarts the interval from that moment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I see what a production webhook received?&lt;/strong&gt;&lt;br&gt;
Not in the editor — that's test-URL behavior. Open the execution in the executions list and inspect the trigger node's output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where did the Active toggle go?&lt;/strong&gt;&lt;br&gt;
It was replaced. Publishing a workflow is what makes it live now. Tutorials showing a toggle on an empty canvas predate the change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why can't I click Publish?&lt;/strong&gt;&lt;br&gt;
Usually unsaved changes, or a required field missing in the trigger node. Hover the control for the specific reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My workflow runs but processes the same item twice. Is that a trigger problem?&lt;/strong&gt;&lt;br&gt;
It's a retry. If your workflow is slow to respond, the sender may retry and deliver the same payload again — and nothing errors, because both runs succeed. Write an idempotency key before the irreversible step, not after it, and check it on entry.&lt;/p&gt;




&lt;p&gt;Official references worth bookmarking: n8n's &lt;a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/common-issues/" rel="noopener noreferrer"&gt;Webhook node common issues&lt;/a&gt; and &lt;a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.scheduletrigger/common-issues/" rel="noopener noreferrer"&gt;Schedule Trigger common issues&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Still stuck?&lt;/strong&gt; Working starter workflows — including an error-handler and a heartbeat monitor — are on our &lt;a href="https://noderecipe.com/templates" rel="noopener noreferrer"&gt;templates page&lt;/a&gt;. If the root cause turned out to be your hosting setup, the &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;self-hosting guide&lt;/a&gt; and &lt;a href="https://noderecipe.com/n8n-docker-setup" rel="noopener noreferrer"&gt;Docker setup guide&lt;/a&gt; cover the URL and proxy configuration properly.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>devops</category>
      <category>debugging</category>
    </item>
    <item>
      <title>How to Self-Host n8n: A Complete Beginner's Guide (2026)</title>
      <dc:creator>Ali Ilyas</dc:creator>
      <pubDate>Wed, 05 Aug 2026 18:19:46 +0000</pubDate>
      <link>https://dev.to/ali_ilyas/how-to-self-host-n8n-a-complete-beginners-guide-2026-412f</link>
      <guid>https://dev.to/ali_ilyas/how-to-self-host-n8n-a-complete-beginners-guide-2026-412f</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://noderecipe.com/self-host-n8n" rel="noopener noreferrer"&gt;noderecipe.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Self-hosting n8n gives you &lt;strong&gt;unlimited workflow executions&lt;/strong&gt;, full ownership of your data, and no per-task fees — all for the cost of a small server (about &lt;strong&gt;$5/month&lt;/strong&gt;). This guide takes you from an empty VPS to a secured, HTTPS-enabled n8n instance running 24/7, using Docker and Docker Compose. You'll also get the environment variables that actually matter, a backup routine, and fixes for the errors that trip up most beginners.&lt;/p&gt;

&lt;p&gt;No prior Docker experience is assumed. If you can copy a command into a terminal, you can finish this in about 30 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosted vs. n8n Cloud: which do you need?
&lt;/h2&gt;

&lt;p&gt;Before you spin up a server, be honest about which path fits you. n8n offers a paid cloud plan and a free, source-available self-hosted option (the Community Edition). Here's the trade-off:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Self-hosted (Community)&lt;/th&gt;
&lt;th&gt;n8n Cloud&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Price&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$5/mo server, unlimited executions&lt;/td&gt;
&lt;td&gt;From ~$24/mo, metered executions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data location&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your server, your control&lt;/td&gt;
&lt;td&gt;n8n's managed infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You handle updates &amp;amp; backups&lt;/td&gt;
&lt;td&gt;Fully managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~30 min (this guide)&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom nodes / npm packages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full control&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Developers, high-volume, privacy needs&lt;/td&gt;
&lt;td&gt;Non-technical users, teams who want zero ops&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Choose self-hosting if&lt;/strong&gt; you run a lot of executions, want your data on infrastructure you control, or need custom community nodes. &lt;strong&gt;Choose &lt;a href="https://n8n.io/" rel="noopener noreferrer"&gt;Cloud&lt;/a&gt; if&lt;/strong&gt; you never want to think about servers — the honest version is that if your automations make you money and your time is worth more than $20/month, managed hosting is the cheaper option however it looks on the invoice. The rest of this guide covers self-hosting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;VPS&lt;/strong&gt; (virtual private server) — roughly $5/month. See the comparison below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH access&lt;/strong&gt; to that server (a terminal on Mac/Linux, or a client like PuTTY/Windows Terminal on Windows).&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;domain or subdomain&lt;/strong&gt; you can point at the server (e.g. &lt;code&gt;n8n.yourdomain.com&lt;/code&gt;). This is required for HTTPS and for webhooks to work reliably.&lt;/li&gt;
&lt;li&gt;About &lt;strong&gt;30 minutes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do &lt;em&gt;not&lt;/em&gt; need to know Docker beforehand — every command is provided.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to host n8n (VPS comparison)
&lt;/h2&gt;

&lt;p&gt;Any Linux server with 1 GB of RAM will run a small n8n instance, but 2 GB is more comfortable once you have a few active workflows. These are the popular, budget-friendly options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Entry price&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://www.hetzner.com/cloud" rel="noopener noreferrer"&gt;Hetzner Cloud&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~€4.5/mo&lt;/td&gt;
&lt;td&gt;2–4 GB&lt;/td&gt;
&lt;td&gt;Best price-to-performance (EU/US regions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://www.digitalocean.com/" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$6/mo&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;Beginner-friendly UI, great docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://railway.com/" rel="noopener noreferrer"&gt;Railway&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Usage-based&lt;/td&gt;
&lt;td&gt;Scales&lt;/td&gt;
&lt;td&gt;Fastest setup, no server management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Home server / Raspberry Pi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;td&gt;Tinkering, LAN-only automations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most people starting out, &lt;strong&gt;&lt;a href="https://www.hetzner.com/cloud" rel="noopener noreferrer"&gt;Hetzner's&lt;/a&gt; CX22&lt;/strong&gt; (2 GB RAM) or a &lt;strong&gt;&lt;a href="https://www.digitalocean.com/" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt; basic droplet&lt;/strong&gt; is the sweet spot. Pick Ubuntu 24.04 LTS as the operating system when you create the server.&lt;/p&gt;

&lt;p&gt;Two notes on picking between them, since this is the one decision that costs money. &lt;strong&gt;Hetzner is roughly half the price for double the RAM&lt;/strong&gt; — a CX22 gives you 2 GB for about €4.5, where DigitalOcean's $6 droplet gives you 1 GB. If you are comfortable in a terminal, Hetzner is the better machine for the money. &lt;strong&gt;DigitalOcean is worth the premium if you are new to servers&lt;/strong&gt;: the control panel is friendlier, and their documentation is genuinely the best in the business for the exact moment when something breaks at 11pm. Both give new accounts a signup credit that covers your first month or two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick start: run n8n in one command
&lt;/h2&gt;

&lt;p&gt;Want to &lt;em&gt;see&lt;/em&gt; n8n before committing to the full setup? SSH into your server and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume create n8n_data

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped &lt;span class="nt"&gt;--name&lt;/span&gt; n8n &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 5678:5678 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; n8n_data:/home/node/.n8n &lt;span class="se"&gt;\&lt;/span&gt;
  docker.n8n.io/n8nio/n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://your-server-ip:5678&lt;/code&gt; in your browser and create your owner account. That's a working n8n — but it has &lt;strong&gt;no HTTPS, no database, and no domain&lt;/strong&gt;, so webhooks and OAuth logins will misbehave. Treat this as a test drive, then move to the production setup below.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't have Docker yet?&lt;/strong&gt; Install it in one line: &lt;code&gt;curl -fsSL https://get.docker.com | sh&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Production setup with Docker Compose
&lt;/h2&gt;

&lt;p&gt;Docker Compose lets you define n8n, a PostgreSQL database, and a reverse proxy in a single file so they start together and survive reboots. This is the setup you actually want to keep.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install Docker and Compose
&lt;/h3&gt;

&lt;p&gt;On a fresh Ubuntu server, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Compose plugin ships with modern Docker. Verify both:&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;h3&gt;
  
  
  Step 2: Point your domain at the server
&lt;/h3&gt;

&lt;p&gt;In your DNS provider (Cloudflare, Namecheap, etc.), create an &lt;strong&gt;A record&lt;/strong&gt; for &lt;code&gt;n8n.yourdomain.com&lt;/code&gt; pointing to your server's public IP. DNS can take a few minutes to propagate — you can continue while it does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Create the project folder
&lt;/h3&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 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Create the &lt;code&gt;.env&lt;/code&gt; file
&lt;/h3&gt;

&lt;p&gt;This holds your secrets and settings. Create &lt;code&gt;~/n8n/.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# --- Your domain ---&lt;/span&gt;
&lt;span class="nv"&gt;DOMAIN_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;n8n.yourdomain.com

&lt;span class="c"&gt;# --- Database (Postgres) ---&lt;/span&gt;
&lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;n8n
&lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;change-this-to-a-long-random-string
&lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;n8n

&lt;span class="c"&gt;# --- n8n security ---&lt;/span&gt;
&lt;span class="c"&gt;# Generate with: openssl rand -hex 32&lt;/span&gt;
&lt;span class="nv"&gt;N8N_ENCRYPTION_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;paste-a-32-byte-random-hex-string-here
&lt;span class="nv"&gt;GENERIC_TIMEZONE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;America/New_York
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate strong values before saving:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32   &lt;span class="c"&gt;# use for N8N_ENCRYPTION_KEY&lt;/span&gt;
openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16   &lt;span class="c"&gt;# use for POSTGRES_PASSWORD&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keep &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; safe.&lt;/strong&gt; It encrypts all your saved credentials. If you lose it, every stored credential becomes unreadable and you'll have to re-enter them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 5: Create &lt;code&gt;docker-compose.yml&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;~/n8n/docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;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:16&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="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=${POSTGRES_DB}&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;postgres_data:/var/lib/postgresql/data&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;-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;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;n8n&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.n8n.io/n8nio/n8n:latest&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="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;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_TYPE=postgresdb&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_HOST=postgres&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_DATABASE=${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_USER=${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_HOST=${DOMAIN_NAME}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PROTOCOL=https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PORT=5678&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEBHOOK_URL=https://${DOMAIN_NAME}/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GENERIC_TIMEZONE=${GENERIC_TIMEZONE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_RUNNERS_ENABLED=true&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;n8n_data:/home/node/.n8n&lt;/span&gt;
    &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;5678&lt;/span&gt;

  &lt;span class="na"&gt;caddy&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;caddy:2&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;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&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;./Caddyfile:/etc/caddy/Caddyfile&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_data:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_config:/config&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;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that n8n uses &lt;code&gt;expose&lt;/code&gt; (internal only) rather than publishing port 5678 to the public internet — Caddy is the only service that faces the outside world.&lt;/p&gt;

&lt;h2&gt;
  
  
  The environment variables that matter
&lt;/h2&gt;

&lt;p&gt;Most self-hosting problems come from missing or wrong environment variables. These are the ones you should always set on a production instance:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Encrypts stored credentials. Set it explicitly and back it up.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;N8N_HOST&lt;/code&gt; / &lt;code&gt;WEBHOOK_URL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Must match your real domain, or webhooks and OAuth callbacks break.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;N8N_PROTOCOL=https&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tells n8n it's served over HTTPS (behind the proxy).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DB_TYPE=postgresdb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Uses Postgres instead of the default SQLite — required for reliability at scale.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GENERIC_TIMEZONE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Makes Schedule/Cron nodes fire at the times you expect.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;N8N_RUNNERS_ENABLED=true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enables task runners, the recommended way to execute Code nodes. Required on n8n 1.x; &lt;strong&gt;deprecated from version 2.0&lt;/strong&gt;, where it's on by default and can be omitted.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One more worth knowing about if you import workflows you didn't write: &lt;code&gt;N8N_BLOCK_ENV_ACCESS_IN_NODE&lt;/code&gt; defaults to &lt;code&gt;false&lt;/code&gt;, which means Code nodes and expressions &lt;strong&gt;can read your environment variables&lt;/strong&gt; — including the encryption key and database password above. Setting it to &lt;code&gt;true&lt;/code&gt; closes that off. &lt;a href="https://noderecipe.com/n8n-workflow-templates" rel="noopener noreferrer"&gt;Importing templates safely&lt;/a&gt; covers why that matters and what else to check before running someone else's workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding HTTPS with Caddy
&lt;/h2&gt;

&lt;p&gt;Caddy provisions and renews a free Let's Encrypt certificate automatically — no manual certbot steps. Create &lt;code&gt;~/n8n/Caddyfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;n8n.yourdomain.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;reverse_proxy&lt;/span&gt; &lt;span class="nf"&gt;n8n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5678&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;n8n.yourdomain.com&lt;/code&gt; with your real subdomain (it must match &lt;code&gt;DOMAIN_NAME&lt;/code&gt;). That's the entire config — Caddy handles the TLS certificate the first time someone hits the domain.&lt;/p&gt;

&lt;p&gt;Now start everything:&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
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give it a minute (the certificate is issued on first request), then open &lt;code&gt;https://n8n.yourdomain.com&lt;/code&gt;. Create your owner account and you're live — with a valid HTTPS padlock.&lt;/p&gt;

&lt;p&gt;Check that all three containers are healthy:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Keeping n8n running, updated, and backed up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Auto-restart:&lt;/strong&gt; &lt;code&gt;restart: unless-stopped&lt;/code&gt; in the Compose file means Docker brings your containers back after a crash or server reboot. Nothing else needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Updating n8n:&lt;/strong&gt; pull the newest image and recreate the container. Your data lives in named volumes, so it survives the upgrade:&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
docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Backups — do not skip this.&lt;/strong&gt; Your workflows and credentials live in Postgres and the n8n volume. Back up the database regularly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; postgres &lt;span class="se"&gt;\&lt;/span&gt;
  pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; n8n n8n &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/n8n-backup-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy that &lt;code&gt;.sql&lt;/code&gt; file off the server (to your machine, S3, or a backup service) and keep your &lt;code&gt;.env&lt;/code&gt; alongside it — remember, without &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; the backup's credentials can't be decrypted. Automate it with a daily &lt;code&gt;cron&lt;/code&gt; job for peace of mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Securing your instance
&lt;/h2&gt;

&lt;p&gt;A public n8n instance is a target. Lock it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Firewall:&lt;/strong&gt; allow only ports 22 (SSH), 80, and 443. On Ubuntu: &lt;code&gt;ufw allow 22 &amp;amp;&amp;amp; ufw allow 80 &amp;amp;&amp;amp; ufw allow 443 &amp;amp;&amp;amp; ufw enable&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never expose port 5678 directly.&lt;/strong&gt; Let Caddy terminate TLS; keep n8n internal (as configured above).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use strong, unique secrets&lt;/strong&gt; for the database password and encryption key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the server updated:&lt;/strong&gt; &lt;code&gt;apt update &amp;amp;&amp;amp; apt upgrade -y&lt;/code&gt; periodically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restrict SSH&lt;/strong&gt; to key-based auth and disable root password login.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common errors and fixes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"This site can't provide a secure connection" / no HTTPS.&lt;/strong&gt;&lt;br&gt;
DNS isn't pointing at your server yet, or port 80/443 is blocked. Confirm the A record resolves (&lt;code&gt;dig n8n.yourdomain.com&lt;/code&gt;) and that your firewall/cloud security group allows 80 and 443. Caddy needs port 80 reachable to issue the certificate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webhooks return the wrong URL or don't fire.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;WEBHOOK_URL&lt;/code&gt; and &lt;code&gt;N8N_HOST&lt;/code&gt; must match your public HTTPS domain exactly. If they still show &lt;code&gt;localhost&lt;/code&gt; or an IP, you edited the values but didn't recreate the container — run &lt;code&gt;docker compose up -d&lt;/code&gt; again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Command 'code' is not allowed" or Code node fails.&lt;/strong&gt;&lt;br&gt;
On n8n 1.x, enable task runners with &lt;code&gt;N8N_RUNNERS_ENABLED=true&lt;/code&gt; (already in the Compose file above) and restart. On 2.0 and later, task runners are enabled by default and that variable is deprecated — if the error persists there, check the container logs instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credentials show as "unable to decrypt."&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; changed between runs. It must stay identical to the one used when the credentials were saved. Restore the original key from your backup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Containers keep restarting.&lt;/strong&gt;&lt;br&gt;
Check logs to see which one: &lt;code&gt;docker compose logs n8n --tail=50&lt;/code&gt; or &lt;code&gt;docker compose logs postgres --tail=50&lt;/code&gt;. The most common cause is a Postgres password mismatch after editing &lt;code&gt;.env&lt;/code&gt; without recreating the database volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schedule/Cron node fires at the wrong time.&lt;/strong&gt;&lt;br&gt;
Set &lt;code&gt;GENERIC_TIMEZONE&lt;/code&gt; to your IANA timezone (e.g. &lt;code&gt;Europe/London&lt;/code&gt;) and restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is self-hosting n8n free?&lt;/strong&gt;&lt;br&gt;
The n8n Community Edition is free to self-host, permanently and with no execution limit. Your only cost is the server (~$5/month) and your domain. One caveat worth knowing before you build a business on it: n8n is source-available, not open source, and its &lt;a href="https://docs.n8n.io/privacy-and-security/sustainable-use-license/" rel="noopener noreferrer"&gt;Sustainable Use License&lt;/a&gt; restricts hosting n8n for paying users. &lt;a href="https://noderecipe.com/is-n8n-free" rel="noopener noreferrer"&gt;What's free and what isn't&lt;/a&gt; breaks that down, including what it means for client work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a domain?&lt;/strong&gt;&lt;br&gt;
For a real setup, yes. Webhooks, OAuth logins, and HTTPS all depend on a proper domain. You can test on a raw IP, but production needs a domain. Google's OAuth flow makes this concrete: the redirect URI is registered against one fixed address, so &lt;a href="https://noderecipe.com/n8n-google-sheets" rel="noopener noreferrer"&gt;connecting a service like Google Sheets&lt;/a&gt; means re-registering it every time your instance moves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQLite or Postgres?&lt;/strong&gt;&lt;br&gt;
n8n defaults to SQLite, which is fine for testing. For anything you rely on, use Postgres (as in this guide) — it handles concurrent executions far more reliably.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much RAM do I need?&lt;/strong&gt;&lt;br&gt;
1 GB runs a light instance; 2 GB is comfortable for regular use with several active workflows. Heavy AI or data workflows benefit from more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I move from n8n Cloud to self-hosted?&lt;/strong&gt;&lt;br&gt;
Export your workflows as JSON from Cloud and import them into your self-hosted instance, then re-enter credentials. Your workflows are portable JSON either way.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Ready to build?&lt;/strong&gt; The &lt;code&gt;docker-compose.yml&lt;/code&gt; and Caddyfile above are ready to copy as they stand. Once the instance is up, our &lt;a href="https://noderecipe.com/templates" rel="noopener noreferrer"&gt;templates page&lt;/a&gt; has eight importable workflows to run on it — error handling, monitoring, and AI starters. New to containers, or want the details on volumes, image tags, and safe updates? Start with the &lt;a href="https://noderecipe.com/n8n-docker-setup" rel="noopener noreferrer"&gt;n8n Docker setup guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>selfhosted</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
