<?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: Kwik Server</title>
    <description>The latest articles on DEV Community by Kwik Server (@kwik_server_42069a6ad5b18).</description>
    <link>https://dev.to/kwik_server_42069a6ad5b18</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%2F4168368%2F1de426b6-79c0-4b7f-9a83-7a636f3a2386.png</url>
      <title>DEV Community: Kwik Server</title>
      <link>https://dev.to/kwik_server_42069a6ad5b18</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kwik_server_42069a6ad5b18"/>
    <language>en</language>
    <item>
      <title>How to Self-Host n8n with Docker Compose and Nginx (2026)</title>
      <dc:creator>Kwik Server</dc:creator>
      <pubDate>Wed, 07 Oct 2026 13:53:14 +0000</pubDate>
      <link>https://dev.to/kwik_server_42069a6ad5b18/how-to-self-host-n8n-with-docker-compose-and-nginx-2026-2014</link>
      <guid>https://dev.to/kwik_server_42069a6ad5b18/how-to-self-host-n8n-with-docker-compose-and-nginx-2026-2014</guid>
      <description>&lt;p&gt;To self-host n8n with Docker Compose and Nginx, run the n8n container bound to localhost on port 5678, put an Nginx server block with WebSocket headers in front of it, and add a Let’s Encrypt certificate. Then set &lt;code&gt;WEBHOOK_URL&lt;/code&gt; and &lt;code&gt;N8N_PROXY_HOPS=1&lt;/code&gt; so webhooks and the editor work behind the proxy.&lt;/p&gt;

&lt;p&gt;n8n’s own Compose guide uses Traefik, and its other server guides use Caddy. This guide is for people who already run Nginx on the server, or want to, and need the proxy settings that those guides leave out. It also covers running n8n next to another tool on the same server, backups and the errors a proxy setup tends to cause.&lt;/p&gt;

&lt;p&gt;Tested on Ubuntu 24.04.5 LTS with n8n 2.42.4 in October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do you need to self-host n8n?
&lt;/h2&gt;

&lt;p&gt;A Linux VPS with root SSH access, a subdomain pointed at it, and about half an hour.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A VPS running Ubuntu.&lt;/strong&gt; The commands below are written for Ubuntu 24.04 and work the same way on Debian.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A subdomain.&lt;/strong&gt; Create an A record such as &lt;code&gt;n8n.yourdomain.com&lt;/code&gt; that points to the server’s IP address. Do this first so DNS has updated by the time you request the certificate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some comfort with a terminal.&lt;/strong&gt; n8n’s docs recommend self-hosting for people who are used to managing servers, and point everyone else to n8n Cloud. That is fair advice. You are responsible for updates, backups and security.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guide installs n8n by itself. It does not include the separate sandbox stack that n8n’s newer Compose guide adds for its AI Assistant, which needs more memory and a privileged container.&lt;/p&gt;

&lt;h2&gt;
  
  
  What size VPS does n8n need?
&lt;/h2&gt;

&lt;p&gt;A small one. n8n’s Hetzner guide says a small CPX11 instance is enough for most usage, and suggests 4 GB RAM and 2 vCPU if you add the n8n Assistant. What grows memory use in practice is the size of the data your workflows handle and how many run at once.&lt;/p&gt;

&lt;p&gt;On our test server, which has 4 GB of RAM, n8n used about 353 MB of memory at idle, or 9% of the total. SerpBear was running on the same server at the time, and the two containers together used about 476 MB, roughly 12%.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you set up n8n with Docker Compose?
&lt;/h2&gt;

&lt;p&gt;Install Docker, create a project folder with a &lt;code&gt;.env&lt;/code&gt; file and a &lt;code&gt;compose.yaml&lt;/code&gt; file, and start the container. The Compose file below is n8n’s &lt;a href="https://docs.n8n.io/deploy/host-n8n/install-options/use-a-cloud-provider/use-docker-compose" rel="noopener noreferrer"&gt;official one&lt;/a&gt; with the Traefik service and labels removed and one variable added.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com | sh
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;Skip this if Docker is already on the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: 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; /opt/n8n/local-files &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; /opt/n8n
&lt;span class="nb"&gt;chown &lt;/span&gt;1000:1000 local-files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;local-files&lt;/code&gt; folder is shared with the container at &lt;code&gt;/files&lt;/code&gt;, for workflows that read or write files on disk. The &lt;code&gt;chown&lt;/code&gt; matters when you work as root: n8n runs inside the container as user ID 1000 and cannot write to a folder that root owns.&lt;/p&gt;

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



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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DOMAIN_NAME=yourdomain.com
SUBDOMAIN=n8n
GENERIC_TIMEZONE=Europe/Berlin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;yourdomain.com&lt;/code&gt; with your real domain before you save. We left the placeholder in on our own test run. n8n still starts with it, but it builds its webhook addresses from these two values, so every webhook URL would point at a domain you do not own.&lt;/p&gt;

&lt;p&gt;Set the timezone to your own as well. n8n uses it for schedule triggers, and the default is New York if you leave it out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Create the compose.yaml file
&lt;/h3&gt;



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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8nio/n8n&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;always&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:5678:5678"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME}&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;N8N_PROTOCOL=https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_ENV=production&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEBHOOK_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PROXY_HOPS=1&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;TZ=${GENERIC_TIMEZONE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_RESTRICT_FILE_ACCESS_TO=/files&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="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./local-files:/files&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;The port is bound to &lt;code&gt;127.0.0.1&lt;/code&gt;, as in the official file. Ports that Docker publishes skip UFW rules, so binding to localhost is what keeps port 5678 off the public internet. Only Nginx on the same server can reach it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Start n8n
&lt;/h3&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 ps
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press Ctrl+C to leave the logs. Do not open n8n in a browser yet. Set up Nginx and SSL first.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you put n8n behind Nginx with SSL?
&lt;/h2&gt;

&lt;p&gt;Create an Nginx server block that proxies to port 5678 with WebSocket support, then let Certbot add the certificate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx &lt;span class="nt"&gt;-y&lt;/span&gt;
nano /etc/nginx/sites-available/n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;n8n.yourdomain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;client_max_body_size&lt;/span&gt; &lt;span class="mi"&gt;16m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:5678&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;"upgrade"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_buffering&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt; &lt;span class="s"&gt;3600s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change &lt;code&gt;server_name&lt;/code&gt; to your own subdomain before you go on. If it does not match the address you open in the browser, Nginx serves its default “Welcome to nginx” page instead of n8n. We hit exactly that on our test run.&lt;/p&gt;

&lt;p&gt;Enable the site and add the certificate:&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;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
systemctl reload nginx
apt &lt;span class="nb"&gt;install &lt;/span&gt;certbot python3-certbot-nginx &lt;span class="nt"&gt;-y&lt;/span&gt;
certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; n8n.yourdomain.com &lt;span class="nt"&gt;--redirect&lt;/span&gt;
certbot renew &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the firewall is not on yet, allow SSH before you enable it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ufw allow ssh
ufw allow &lt;span class="s1"&gt;'Nginx Full'&lt;/span&gt;
ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now open &lt;code&gt;https://n8n.yourdomain.com&lt;/code&gt; and create the owner account straight away. The first person to reach a new n8n instance gets to create that account, so do not leave it sitting unclaimed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which settings stop webhooks and the editor breaking behind a proxy?
&lt;/h2&gt;

&lt;p&gt;Four of them. n8n’s docs on &lt;a href="https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/configuration-examples/configure-webhook-urls-with-reverse-proxy" rel="noopener noreferrer"&gt;webhook URLs behind a reverse proxy&lt;/a&gt; cover the first three, and the fourth is on the Nginx side.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;What goes wrong without it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WEBHOOK_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;compose.yaml&lt;/td&gt;
&lt;td&gt;n8n builds webhook URLs from its internal host and port, so they point at port 5678 and outside services cannot reach them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;N8N_PROXY_HOPS=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;compose.yaml&lt;/td&gt;
&lt;td&gt;n8n does not trust the forwarded headers, so it sees every request as coming from the proxy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;X-Forwarded-For&lt;/code&gt;, &lt;code&gt;X-Forwarded-Host&lt;/code&gt;, &lt;code&gt;X-Forwarded-Proto&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Nginx&lt;/td&gt;
&lt;td&gt;n8n cannot tell the visitor’s address or that the original request was HTTPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Upgrade&lt;/code&gt; and &lt;code&gt;Connection&lt;/code&gt; headers&lt;/td&gt;
&lt;td&gt;Nginx&lt;/td&gt;
&lt;td&gt;The editor cannot hold its live connection and shows “Connection lost”&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two Nginx lines in our config are choices you may want to change. &lt;code&gt;client_max_body_size 16m&lt;/code&gt; matches n8n’s default payload limit of 16 MiB, set by &lt;code&gt;N8N_PAYLOAD_SIZE_MAX&lt;/code&gt;. Nginx’s own default of 1 MB would reject larger webhook bodies and file uploads first. &lt;code&gt;proxy_read_timeout 3600s&lt;/code&gt; keeps long-running requests and the editor connection from being cut after Nginx’s default 60 seconds.&lt;/p&gt;

&lt;p&gt;After changing anything in &lt;code&gt;compose.yaml&lt;/code&gt; or &lt;code&gt;.env&lt;/code&gt;, apply it with &lt;code&gt;docker compose up -d&lt;/code&gt;. A plain restart does not pick up new environment values.&lt;/p&gt;

&lt;p&gt;To check the whole chain, add a Webhook node, click Listen for test event, and open its Test URL from another device. On our test server the request showed up in the editor straight away with &lt;code&gt;x-forwarded-proto: https&lt;/code&gt;, the right &lt;code&gt;x-forwarded-host&lt;/code&gt; and the caller’s real IP address in &lt;code&gt;x-forwarded-for&lt;/code&gt;. If the editor receives the event, the WebSocket connection is working too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can n8n and SerpBear run on the same server?
&lt;/h2&gt;

&lt;p&gt;Yes. Each runs in its own container on its own localhost port, and Nginx routes by hostname. Our test server runs both.&lt;/p&gt;

&lt;p&gt;If you followed our guide to &lt;a href="https://kwikserver.com/tutorials/install-serpbear-vps/" rel="noopener noreferrer"&gt;install SerpBear on a VPS&lt;/a&gt;, you already have Nginx, Certbot and the firewall in place. Skip those install lines above, add the &lt;code&gt;n8n&lt;/code&gt; server block as a second file in &lt;code&gt;sites-available&lt;/code&gt;, and run Certbot for the new subdomain. SerpBear stays on port 3000 and n8n on 5678, each in its own folder under &lt;code&gt;/opt&lt;/code&gt; with its own Compose file, so updating one does not touch the other.&lt;/p&gt;

&lt;p&gt;The pairing is useful for more than saving a server. SerpBear has an API, so an n8n workflow can pull keyword positions on a schedule and send rank changes to Slack, email or a spreadsheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you back up and update n8n?
&lt;/h2&gt;

&lt;p&gt;Back up the &lt;code&gt;n8n_data&lt;/code&gt; volume, and update with three Compose commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Back up the data volume
&lt;/h3&gt;

&lt;p&gt;n8n’s docs describe the &lt;code&gt;n8n_data&lt;/code&gt; volume as where it saves its SQLite database file and encryption key. The key is what decrypts your saved credentials. If you lose it, the workflows can be rebuilt but every stored credential has to be entered again, so back up the whole volume, not just an export of workflows.&lt;/p&gt;

&lt;p&gt;Compose prefixes the volume with the folder name, so it is called &lt;code&gt;n8n_n8n_data&lt;/code&gt; if you used &lt;code&gt;/opt/n8n&lt;/code&gt;. Confirm with &lt;code&gt;docker volume ls&lt;/code&gt;. Save this as &lt;code&gt;/root/n8n-backup.sh&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;#!/bin/sh&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /root/backups
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/n8n
docker compose stop
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; n8n_n8n_data:/data &lt;span class="nt"&gt;-v&lt;/span&gt; /root/backups:/backup alpine &lt;span class="nb"&gt;tar &lt;/span&gt;czf /backup/n8n-&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;.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /data &lt;span class="nb"&gt;.&lt;/span&gt;
docker compose start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /root/n8n-backup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schedule it with &lt;code&gt;crontab -e&lt;/code&gt;, for example weekly at 03:30 on Sunday:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;30 3 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; 0 /root/n8n-backup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;n8n is stopped for a few seconds while the archive is made, so pick a time when no workflow is due. Copy the archives off the server with &lt;code&gt;scp&lt;/code&gt; or &lt;code&gt;rsync&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update n8n
&lt;/h3&gt;

&lt;p&gt;n8n’s docs say a new minor version is released most weeks. These are the update steps from the docs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/n8n
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;Take a backup first. If you would rather choose when you upgrade, pin a version in &lt;code&gt;compose.yaml&lt;/code&gt;, for example &lt;code&gt;image: n8nio/n8n:2.42.4&lt;/code&gt;, the version we tested. Change the tag when you are ready to move.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the common problems with n8n behind Nginx?
&lt;/h2&gt;

&lt;p&gt;Nearly all of them trace back to a placeholder, a missing header or a missing variable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The “Welcome to nginx” page instead of n8n.&lt;/strong&gt; The &lt;code&gt;server_name&lt;/code&gt; line still has the placeholder or does not match your subdomain. Fix it, then run &lt;code&gt;nginx -t&lt;/code&gt; and &lt;code&gt;systemctl reload nginx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Connection lost” in the editor.&lt;/strong&gt; The &lt;code&gt;Upgrade&lt;/code&gt; and &lt;code&gt;Connection&lt;/code&gt; headers or &lt;code&gt;proxy_http_version 1.1&lt;/code&gt; are missing from the Nginx block. Check that Certbot’s edits left them in place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook URLs show the wrong domain, port 5678 or http.&lt;/strong&gt; The &lt;code&gt;.env&lt;/code&gt; values are wrong, &lt;code&gt;WEBHOOK_URL&lt;/code&gt; is not set, or the container was not recreated. Fix the values and run &lt;code&gt;docker compose up -d&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A secure cookie warning on the login page.&lt;/strong&gt; You opened n8n over plain HTTP or by IP address. Use the HTTPS subdomain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;413 Request Entity Too Large.&lt;/strong&gt; Raise &lt;code&gt;client_max_body_size&lt;/code&gt; in Nginx. For bodies above 16 MiB, raise &lt;code&gt;N8N_PAYLOAD_SIZE_MAX&lt;/code&gt; in &lt;code&gt;compose.yaml&lt;/code&gt; as well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schedules fire at the wrong hour.&lt;/strong&gt; &lt;code&gt;GENERIC_TIMEZONE&lt;/code&gt; is unset or wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cannot write to /files.&lt;/strong&gt; The &lt;code&gt;local-files&lt;/code&gt; folder is owned by root. Run &lt;code&gt;chown 1000:1000 /opt/n8n/local-files&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;502 Bad Gateway.&lt;/strong&gt; Nginx is up but n8n is still starting or has stopped. Check &lt;code&gt;docker compose ps&lt;/code&gt; and the logs.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  Is self-hosted n8n free?
&lt;/h3&gt;

&lt;p&gt;The Community Edition costs nothing to run for your own business, and you pay only for the server. n8n is released under the Sustainable Use License, which is source-available and not an open-source license. Read it before you offer n8n to others as a hosted service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need PostgreSQL to run n8n?
&lt;/h3&gt;

&lt;p&gt;No. n8n’s Compose guide uses the built-in SQLite database, which is what this guide sets up. PostgreSQL is supported and is the usual step up when workloads get heavy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why use Nginx when n8n’s docs use Traefik?
&lt;/h3&gt;

&lt;p&gt;Either works. Nginx makes sense when it is already on the server for other sites or tools, because one proxy then handles every subdomain and certificate. If n8n is the only thing on the server, the official Traefik file is less to set up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I open n8n by IP address without a domain?
&lt;/h3&gt;

&lt;p&gt;Not with this setup, and we would not expose it that way. For a private test, tunnel the port over SSH with &lt;code&gt;ssh -L 5678:localhost:5678 root@your-server-ip&lt;/code&gt; and browse to &lt;code&gt;http://localhost:5678&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;On our test server n8n 2.42.4 used about 353 MB at idle. Memory rises with the amount of data a workflow processes and the number of workflows running at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  What else is worth running on the same VPS?
&lt;/h3&gt;

&lt;p&gt;A rank tracker, a metasearch engine and a reporting dashboard all sit comfortably beside n8n. Our guide to the &lt;a href="https://kwikserver.com/tutorials/vps-for-seo-tools/" rel="noopener noreferrer"&gt;best VPS for SEO tools and automation&lt;/a&gt; lists what each tool needs.&lt;/p&gt;

&lt;p&gt;Originally published on the &lt;a href="https://kwikserver.com/tutorials/self-host-n8n-docker-nginx/" rel="noopener noreferrer"&gt;KwikServer tutorials blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>docker</category>
      <category>nginx</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>How to Install SerpBear on a VPS with Docker (2026)</title>
      <dc:creator>Kwik Server</dc:creator>
      <pubDate>Wed, 07 Oct 2026 08:58:48 +0000</pubDate>
      <link>https://dev.to/kwik_server_42069a6ad5b18/how-to-install-serpbear-on-a-vps-with-docker-2026-43c5</link>
      <guid>https://dev.to/kwik_server_42069a6ad5b18/how-to-install-serpbear-on-a-vps-with-docker-2026-43c5</guid>
      <description>&lt;p&gt;To install SerpBear on a VPS, install Docker, create a &lt;code&gt;.env&lt;/code&gt; file and a &lt;code&gt;docker-compose.yaml&lt;/code&gt; file, then run &lt;code&gt;docker compose up -d&lt;/code&gt;. Put Nginx with a Let’s Encrypt certificate in front of it, log in, and connect a scraping API such as ScrapingRobot or Serper.dev. We suggest starting on a 1 vCore, 3 GB RAM server.&lt;/p&gt;

&lt;p&gt;This guide is for SEO freelancers and agencies who want their own rank tracker without per-keyword fees. It goes past the install command and covers the domain and SSL setup, the scraper, Search Console, backups, updates and the errors you are most likely to hit.&lt;/p&gt;

&lt;p&gt;Tested on Ubuntu 24.04.5 LTS with SerpBear v3.1.0 in October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is SerpBear and why self-host it?
&lt;/h2&gt;

&lt;p&gt;SerpBear is an open-source rank tracker that records where your domains appear in Google for the keywords you choose. You run it on your own server, so the keyword history stays with you and there is no per-keyword or per-seat charge from the software.&lt;/p&gt;

&lt;p&gt;It has email notifications for position changes, a Google Search Console integration and its own API for reporting tools. One thing to know before you start: SerpBear does not query Google from your server. Its &lt;a href="https://docs.serpbear.com/getting-started" rel="noopener noreferrer"&gt;getting started guide&lt;/a&gt; says you need a third-party scraping account or proxy IPs, because scraping Google directly gets an IP banned. Your VPS runs the dashboard, the schedule and the database. The scraping service fetches the results.&lt;/p&gt;

&lt;p&gt;If you only track a handful of keywords for one site, you can run SerpBear on your own PC for free. The docs note the limits of that: no access from your phone and no API for reporting apps. A VPS is for when you want it running every day without your laptop being open.&lt;/p&gt;

&lt;h2&gt;
  
  
  What size VPS does SerpBear need?
&lt;/h2&gt;

&lt;p&gt;SerpBear publishes no minimum hardware figure in its README or docs. Because the scraping happens on someone else’s infrastructure, the server only runs a Node.js app, a scheduler and a small database, so a small server is enough to start, and you can upgrade if memory runs short.&lt;/p&gt;

&lt;p&gt;On our test install, a 4 GB server running Ubuntu 24.04.5 LTS, the SerpBear container used between 112 MB and 144 MB of memory across our readings, which is under 4% of the server’s RAM. A manual keyword refresh added about 20 MB while it ran. The image takes 461 MB on disk.&lt;/p&gt;

&lt;p&gt;HDD storage is fine for a rank tracker. Server location does not change your ranking data, since you set the country for each keyword inside SerpBear. Pick the location closest to you so the dashboard loads quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do you need before you install SerpBear on a VPS?
&lt;/h2&gt;

&lt;p&gt;Three things: a Linux VPS with root SSH access, a domain or subdomain, and a scraping API key.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A VPS running Ubuntu.&lt;/strong&gt; The commands below are written for Ubuntu and work the same way on Debian.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A subdomain pointed at the server.&lt;/strong&gt; Create an A record such as &lt;code&gt;serpbear.yourdomain.com&lt;/code&gt; that points to your VPS IP address. Do this first so DNS has time to update before you request the SSL certificate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A scraping API key.&lt;/strong&gt; The SerpBear docs list ScrapingRobot (5,000 free requests per month) and Serper.dev (2,500 free requests) as places to start.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How do you install Docker on Ubuntu?
&lt;/h2&gt;

&lt;p&gt;Log in over SSH as root, update the system and run Docker’s install script. Docker Compose comes with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade &lt;span class="nt"&gt;-y&lt;/span&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;Check that both are installed:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  How do you set up SerpBear with Docker Compose?
&lt;/h2&gt;

&lt;p&gt;Create a folder, add two files and start the container. The files below follow the &lt;a href="https://docs.serpbear.com/deployment/deploying-serpbear-on-a-server" rel="noopener noreferrer"&gt;official deployment guide&lt;/a&gt; with two changes, both explained underneath.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create the folder and generate two keys
&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; /opt/serpbear &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; /opt/serpbear
openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 34
openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first value is your &lt;code&gt;SECRET&lt;/code&gt;, which SerpBear uses to encrypt the API keys and passwords you save in it. The second is your &lt;code&gt;APIKEY&lt;/code&gt; for SerpBear’s own API. Copy both.&lt;/p&gt;

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



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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;SERPBEAR_USER&lt;/span&gt;=&lt;span class="n"&gt;admin&lt;/span&gt;
&lt;span class="n"&gt;PASSWORD&lt;/span&gt;=&lt;span class="n"&gt;choose&lt;/span&gt;-&lt;span class="n"&gt;a&lt;/span&gt;-&lt;span class="n"&gt;long&lt;/span&gt;-&lt;span class="n"&gt;password&lt;/span&gt;
&lt;span class="n"&gt;SECRET&lt;/span&gt;=&lt;span class="n"&gt;paste&lt;/span&gt;-&lt;span class="n"&gt;the&lt;/span&gt;-&lt;span class="n"&gt;first&lt;/span&gt;-&lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="n"&gt;APIKEY&lt;/span&gt;=&lt;span class="n"&gt;paste&lt;/span&gt;-&lt;span class="n"&gt;the&lt;/span&gt;-&lt;span class="n"&gt;second&lt;/span&gt;-&lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="n"&gt;SESSION_DURATION&lt;/span&gt;=&lt;span class="m"&gt;24&lt;/span&gt;
&lt;span class="n"&gt;NEXT_PUBLIC_APP_URL&lt;/span&gt;=&lt;span class="n"&gt;https&lt;/span&gt;://&lt;span class="n"&gt;serpbear&lt;/span&gt;.&lt;span class="n"&gt;yourdomain&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restrict who can read it:&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;chmod &lt;/span&gt;600 .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SESSION_DURATION&lt;/code&gt; is how many hours a login lasts. &lt;code&gt;NEXT_PUBLIC_APP_URL&lt;/code&gt; must be the exact address you will open in the browser, including &lt;code&gt;https://&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Create the docker-compose.yaml file
&lt;/h3&gt;



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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;towfiqi/serpbear: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;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:3000:3000"&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;USER_NAME=${SERPBEAR_USER:-admin}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PASSWORD=${PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SECRET=${SECRET}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;APIKEY=${APIKEY}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SESSION_DURATION=${SESSION_DURATION:-24}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000}&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;serpbear_data:/app/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;wget&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-qO-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&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;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The two changes from the official file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The username variable is called SERPBEAR_USER, not USER.&lt;/strong&gt; A Linux shell already has a &lt;code&gt;USER&lt;/code&gt; variable set to your login name, and Docker Compose reads the shell before it reads &lt;code&gt;.env&lt;/code&gt;. If you run the official file as root, your SerpBear username can end up as &lt;code&gt;root&lt;/code&gt; instead of &lt;code&gt;admin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The port is bound to 127.0.0.1.&lt;/strong&gt; Ports that Docker publishes skip UFW rules, so a plain &lt;code&gt;3000:3000&lt;/code&gt; leaves the login page open to the internet over unencrypted HTTP. Binding to localhost means only Nginx on the same server can reach it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Start SerpBear
&lt;/h3&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 ps
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait until &lt;code&gt;docker compose ps&lt;/code&gt; shows the container as healthy. Press Ctrl+C to leave the logs. The container keeps running.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you add a domain, Nginx and SSL?
&lt;/h2&gt;

&lt;p&gt;Install Nginx as a reverse proxy in front of port 3000, then let Certbot add a free Let’s Encrypt certificate. This is the Nginx option from the SerpBear docs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx &lt;span class="nt"&gt;-y&lt;/span&gt;
nano /etc/nginx/sites-available/serpbear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;serpbear.yourdomain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;'upgrade'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_cache_bypass&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable the site and check the config:&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;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/serpbear /etc/nginx/sites-enabled/
nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the certificate and confirm renewal works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;certbot python3-certbot-nginx &lt;span class="nt"&gt;-y&lt;/span&gt;
certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; serpbear.yourdomain.com
certbot renew &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Certbot edits the Nginx config to redirect HTTP to HTTPS. Last, turn on the firewall. Allow SSH before you enable it, or you will lock yourself out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ufw allow ssh
ufw allow &lt;span class="s1"&gt;'Nginx Full'&lt;/span&gt;
ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;https://serpbear.yourdomain.com&lt;/code&gt;, log in with the username and password from your &lt;code&gt;.env&lt;/code&gt; file, and add your first domain when SerpBear asks for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which scraper service should you connect?
&lt;/h2&gt;

&lt;p&gt;Start with a free tier from ScrapingRobot or Serper.dev, and move to a paid service or proxies when you outgrow it. Until a scraper is connected, SerpBear cannot fetch any positions.&lt;/p&gt;

&lt;p&gt;We used Scraping Robot for our test in October 2026 and it worked. Its scraping API is now provided through Rayobyte, so you create the account and copy the API key there. SerpBear’s README currently lists scrapingrobot.com as no longer working, so if your keywords stay empty with it, switch to Serper.dev. Check the current free allowance on the provider’s site before you plan around the figures in the SerpBear docs.&lt;/p&gt;

&lt;p&gt;In SerpBear, open Settings from the top right corner, choose the service, paste your API key and save. The docs say to refresh the page afterwards. Then click Add Keyword, enter your keywords, and choose the country and device for each.&lt;/p&gt;

&lt;p&gt;To estimate what you need, count one request per keyword per day. At that rate 100 keywords use about 3,000 requests a month, which fits inside ScrapingRobot’s 5,000 free requests. Desktop and mobile are tracked as separate keywords, so tracking both doubles the count. SerpBear’s &lt;a href="https://docs.serpbear.com/integrations" rel="noopener noreferrer"&gt;integrations page&lt;/a&gt; lists the other supported services.&lt;/p&gt;

&lt;p&gt;You can also use your own proxies. Select Proxy in the scraper list and enter them as &lt;code&gt;http://username:password@IP_ADDRESS:PORT&lt;/code&gt;. The docs say datacenter IPs work and residential IPs are not required.&lt;/p&gt;

&lt;p&gt;For position-change emails, add SMTP details in the same Settings panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you connect Google Search Console?
&lt;/h2&gt;

&lt;p&gt;Create a Google Cloud service account, give it access to your Search Console property, and paste its two credentials into SerpBear. This adds real clicks, impressions and average position next to each tracked keyword.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a project in Google Cloud and enable the Google Search Console API for it.&lt;/li&gt;
&lt;li&gt;Create a service account in that project, open Manage Keys, and add a JSON key. The downloaded file holds the &lt;code&gt;client_email&lt;/code&gt; and &lt;code&gt;private_key&lt;/code&gt; values.&lt;/li&gt;
&lt;li&gt;In Search Console, open Settings, then Users and permissions, and add the &lt;code&gt;client_email&lt;/code&gt; address as a user with Full permission. Repeat this for every property you want in SerpBear.&lt;/li&gt;
&lt;li&gt;In SerpBear, open Settings, go to the Search Console tab, and paste both values. They apply to all your domains.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full walkthrough with screenshots is in the &lt;a href="https://docs.serpbear.com/miscellaneous/integrate-google-search-console" rel="noopener noreferrer"&gt;SerpBear Search Console guide&lt;/a&gt;. We prefer pasting the credentials in the Settings screen over putting the private key in &lt;code&gt;.env&lt;/code&gt;, because a multi-line key is easy to break in an environment file.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you back up and update SerpBear?
&lt;/h2&gt;

&lt;p&gt;Everything SerpBear stores lives in one Docker volume, so a backup is a single archive of that volume and an update is two commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Back up the data volume
&lt;/h3&gt;

&lt;p&gt;Compose prefixes the volume with the folder name, so it is called &lt;code&gt;serpbear_serpbear_data&lt;/code&gt; if you used &lt;code&gt;/opt/serpbear&lt;/code&gt;. Confirm with &lt;code&gt;docker volume ls&lt;/code&gt;. Save this as &lt;code&gt;/root/serpbear-backup.sh&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;#!/bin/sh&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /root/backups
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/serpbear
docker compose stop
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; serpbear_serpbear_data:/data &lt;span class="nt"&gt;-v&lt;/span&gt; /root/backups:/backup alpine &lt;span class="nb"&gt;tar &lt;/span&gt;czf /backup/serpbear-&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;.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /data &lt;span class="nb"&gt;.&lt;/span&gt;
docker compose start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it executable and schedule it weekly with &lt;code&gt;crontab -e&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /root/serpbear-backup.sh
0 3 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; 0 /root/serpbear-backup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script stops SerpBear for a few seconds so the database is not copied mid-write. A backup that sits on the same server does not protect you from losing that server, so copy the archives somewhere else with &lt;code&gt;scp&lt;/code&gt; or &lt;code&gt;rsync&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Restore a backup
&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;cd&lt;/span&gt; /opt/serpbear
docker compose down
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; serpbear_serpbear_data:/data &lt;span class="nt"&gt;-v&lt;/span&gt; /root/backups:/backup alpine sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"rm -rf /data/* &amp;amp;&amp;amp; tar xzf /backup/serpbear-YYYY-MM-DD.tar.gz -C /data"&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same steps move SerpBear to a new server: install Docker, copy over the folder and the archive, and restore.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update SerpBear
&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;cd&lt;/span&gt; /opt/serpbear
docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker image prune &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your data stays in the volume through updates. Take a backup first anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the common SerpBear install problems?
&lt;/h2&gt;

&lt;p&gt;Most problems come from the environment file, the scraper settings or DNS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Login fails with the right password.&lt;/strong&gt; Run &lt;code&gt;docker compose config | grep USER_NAME&lt;/code&gt; to see the username the container received. If it shows your Linux login name, you used &lt;code&gt;USER&lt;/code&gt; in the compose file. Rename the variable as shown above and run &lt;code&gt;docker compose up -d&lt;/code&gt; again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password or secret looks cut off.&lt;/strong&gt; Compose treats &lt;code&gt;$&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; values as the start of a variable. Avoid &lt;code&gt;$&lt;/code&gt; in the password, or wrap the value in single quotes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;502 Bad Gateway.&lt;/strong&gt; Nginx is up but SerpBear is not. Check &lt;code&gt;docker compose ps&lt;/code&gt; and &lt;code&gt;docker compose logs&lt;/code&gt;. Give the container a little time after a start or update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certbot cannot issue a certificate.&lt;/strong&gt; The A record does not point to this server yet, or port 80 is blocked. Check with &lt;code&gt;dig +short serpbear.yourdomain.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keywords never get a position.&lt;/strong&gt; No scraper is saved, the API key is wrong, or the free quota has run out. Recheck Settings and refresh the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search Console returns “Forbidden”.&lt;/strong&gt; The docs give two causes: the Search Console API is not enabled in the Google Cloud project, or the property type chosen in SerpBear (Domain or URL) does not match the property in Search Console.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site thumbnails stop loading.&lt;/strong&gt; SerpBear fetches them through a shared thum.io key limited to 1,000 a month. Create a free thum.io account and set your own key in the &lt;code&gt;SCREENSHOT_API&lt;/code&gt; variable.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  Is SerpBear free?
&lt;/h3&gt;

&lt;p&gt;The software is free and open source under the MIT license. Your costs are the server it runs on and, once you pass a free tier, the scraping API or proxies it uses to read Google results.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many keywords can SerpBear track?
&lt;/h3&gt;

&lt;p&gt;SerpBear sets no limit on domains or keywords. The practical limit is your scraping quota, since each keyword check uses a request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does SerpBear need proxies?
&lt;/h3&gt;

&lt;p&gt;It needs either a scraping API or proxies. Most people start with an API key from ScrapingRobot or Serper.dev because there is nothing to manage. If you choose proxies, the docs say datacenter IPs are enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I run SerpBear without a domain name?
&lt;/h3&gt;

&lt;p&gt;Yes. With the setup in this guide the app listens only on the server itself, so open an SSH tunnel with &lt;code&gt;ssh -L 3000:localhost:3000 root@your-server-ip&lt;/code&gt;, set &lt;code&gt;NEXT_PUBLIC_APP_URL&lt;/code&gt; to &lt;code&gt;http://localhost:3000&lt;/code&gt; and browse to that address. A subdomain with SSL is better for a team or for mobile access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can SerpBear share a VPS with n8n or other tools?
&lt;/h3&gt;

&lt;p&gt;Yes. Each tool runs in its own container on its own port behind the same Nginx. For SerpBear plus a workflow tool we suggest a server with 4 GB RAM. Our guide to the &lt;a href="https://kwikserver.com/tutorials/vps-for-seo-tools/" rel="noopener noreferrer"&gt;best VPS for SEO tools and automation&lt;/a&gt; covers sizing for a full stack.&lt;/p&gt;

&lt;p&gt;Originally published on the &lt;a href="https://kwikserver.com/tutorials/install-serpbear-vps/" rel="noopener noreferrer"&gt;KwikServer tutorials blog&lt;/a&gt;.&lt;/p&gt;

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