<?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: Fernando Chagas</title>
    <description>The latest articles on DEV Community by Fernando Chagas (@fernandochagas).</description>
    <link>https://dev.to/fernandochagas</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%2F4239123%2F4dfe22a7-ee70-4825-9a0a-13f9be48b448.png</url>
      <title>DEV Community: Fernando Chagas</title>
      <link>https://dev.to/fernandochagas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fernandochagas"/>
    <language>en</language>
    <item>
      <title>Como instalar o n8n com Docker numa VPS</title>
      <dc:creator>Fernando Chagas</dc:creator>
      <pubDate>Wed, 11 Nov 2026 13:30:05 +0000</pubDate>
      <link>https://dev.to/fernandochagas/como-instalar-o-n8n-com-docker-numa-vps-9ik</link>
      <guid>https://dev.to/fernandochagas/como-instalar-o-n8n-com-docker-numa-vps-9ik</guid>
      <description>&lt;p&gt;Este guia monta o n8n self-hosted para produção leve numa VPS Ubuntu 24.04: o n8n com PostgreSQL, o task runner num contêiner separado (como a documentação do n8n recomenda para produção) e o Caddy na frente, emitindo e renovando o certificado HTTPS. Tudo roda com Docker Compose a partir de três arquivos, e o guia também cobre atualização e backup.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que você vai precisar
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Uma VPS com Ubuntu 24.04 de 64 bits e acesso SSH com um usuário que tenha &lt;code&gt;sudo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Um domínio em que você consiga criar registros DNS. Os exemplos usam &lt;code&gt;n8n.example.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Um e-mail para a conta ACME do certificado.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quanto ao tamanho da máquina, a documentação do n8n não fixa um mínimo para este cenário. O guia oficial de Docker Compose pede 2 vCPU e 4 GB de RAM quando a instalação inclui o sandbox do n8n Assistant. Aqui são quatro contêineres (Caddy, n8n, runner e PostgreSQL), então trate 2 vCPU e 4 GB como piso. Com 4 vCPU e 8 GB sobra espaço para execuções com mais dados e para o banco crescer.&lt;/p&gt;

&lt;p&gt;Se você só quer experimentar o n8n, a &lt;a href="https://nuvits.net" rel="noopener noreferrer"&gt;Nuvits&lt;/a&gt; também entrega a VPS com o n8n pré-instalado na contratação. Nessa opção, o n8n roda num único contêiner, sem PostgreSQL nem runner externo, e só responde dentro da VPS: o editor abre por um túnel SSH. Para receber webhooks da internet com domínio e HTTPS, siga este guia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparar o servidor
&lt;/h2&gt;

&lt;p&gt;Se o provedor entrega a VPS com acesso como &lt;code&gt;root&lt;/code&gt;, crie um usuário com &lt;code&gt;sudo&lt;/code&gt; e copie a sua chave SSH para ele:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Abra outra sessão como &lt;code&gt;deploy&lt;/code&gt; e confira que o login funciona antes de fechar a de &lt;code&gt;root&lt;/code&gt;. Depois, libere SSH, HTTP e HTTPS no ufw e ative o firewall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443
sudo ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
sudo ufw status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O &lt;code&gt;ufw enable&lt;/code&gt; pede confirmação porque pode derrubar a conexão SSH; com a regra da porta 22 já criada, responda &lt;code&gt;y&lt;/code&gt;. A regra &lt;code&gt;443&lt;/code&gt; sem protocolo libera TCP e UDP. O UDP serve ao HTTP/3, que o Caddy também atende.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;O Docker passa por cima do ufw.&lt;/strong&gt; Uma porta publicada com &lt;code&gt;ports:&lt;/code&gt; no Compose fica acessível de fora mesmo sem regra no ufw, porque o Docker desvia o tráfego antes que ele chegue às regras do firewall. Por isso, neste guia só o Caddy publica portas (80 e 443). O n8n e o PostgreSQL ficam apenas na rede interna do Compose.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Instalar o Docker
&lt;/h2&gt;

&lt;p&gt;Na Nuvits, dá para pedir a VPS com o Docker já instalado na contratação. Nesse caso, pule para o &lt;code&gt;docker compose version&lt;/code&gt; no fim desta seção.&lt;/p&gt;

&lt;p&gt;O caminho que a documentação do Docker indica para servidores é o repositório &lt;code&gt;apt&lt;/code&gt; oficial. Primeiro, remova pacotes não oficiais que possam conflitar (numa instalação limpa, o &lt;code&gt;apt&lt;/code&gt; costuma responder que não há nada para remover):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sudo apt remove &lt;span class="nv"&gt;$(&lt;/span&gt;dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc &lt;span class="o"&gt;|&lt;/span&gt; cut -f1&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adicione o repositório e a chave GPG do Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

sudo tee /etc/apt/sources.list.d/docker.sources &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: &lt;span class="nv"&gt;$(&lt;/span&gt;. /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"${UBUNTU_CODENAME:-$VERSION_CODENAME}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
Components: stable
Architectures: &lt;span class="nv"&gt;$(&lt;/span&gt;dpkg --print-architecture&lt;span class="o"&gt;)&lt;/span&gt;
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instale o Docker Engine e o plugin do Compose, depois teste:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo docker run hello-world
docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Existe também o script &lt;code&gt;get.docker.com&lt;/code&gt;, mas a própria documentação do Docker o indica só para ambientes de teste e desenvolvimento, não para produção.&lt;/p&gt;

&lt;p&gt;Os comandos deste guia usam &lt;code&gt;sudo docker&lt;/code&gt;. Você pode adicionar seu usuário ao grupo &lt;code&gt;docker&lt;/code&gt; para dispensar o &lt;code&gt;sudo&lt;/code&gt;, mas a documentação do Docker avisa que esse grupo dá privilégios equivalentes aos de root.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configurar o n8n
&lt;/h2&gt;

&lt;p&gt;Crie uma pasta para o projeto. O Compose usa o nome dela como prefixo de volumes e contêineres:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkdir -p ~/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;
  
  
  Segredos no .env
&lt;/h3&gt;

&lt;p&gt;Gere três valores aleatórios, um para a chave de criptografia, um para o token do runner e um para a senha do banco:&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 -hex 32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rode o comando três vezes e cole os resultados no &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="na"&gt;N8N_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.47&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="na"&gt;N8N_DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n8n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="na"&gt;ACME_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;voce&lt;/span&gt;@&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="na"&gt;GENERIC_TIMEZONE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;America&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;Sao_Paulo&lt;/span&gt;

&lt;span class="na"&gt;N8N_ENCRYPTION_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cole&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;aqui&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;primeiro&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;valor&lt;/span&gt;
&lt;span class="na"&gt;N8N_RUNNERS_AUTH_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cole&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;aqui&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;segundo&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;valor&lt;/span&gt;

&lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n8n&lt;/span&gt;
&lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cole&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;aqui&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;terceiro&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;valor&lt;/span&gt;
&lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n8n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;N8N_VERSION&lt;/code&gt; fixa a versão das imagens do n8n e do runner, que precisam ser iguais. Use a versão &lt;code&gt;stable&lt;/code&gt; mais recente listada nas &lt;a href="https://github.com/n8n-io/n8n/releases" rel="noopener noreferrer"&gt;releases do n8n&lt;/a&gt;; &lt;code&gt;2.47.4&lt;/code&gt; era a estável quando este texto foi escrito.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Guarde a chave.&lt;/strong&gt; O n8n usa &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; para criptografar as credenciais salvas no banco. Sem ela, um backup restaurado abre, mas nenhuma credencial pode ser lida. Guarde uma cópia do &lt;code&gt;.env&lt;/code&gt; fora da VPS, num gerenciador de senhas, por exemplo.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Caddyfile
&lt;/h3&gt;

&lt;p&gt;O Caddy obtém o certificado sozinho quando encontra um nome de domínio no endereço do site. O &lt;code&gt;flush_interval -1&lt;/code&gt; desliga o buffer da resposta, como no exemplo oficial do n8n com Caddy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    email {$ACME_EMAIL}
}

{$N8N_DOMAIN} {
    reverse_proxy n8n:5678 {
        flush_interval -1
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O Caddy substitui &lt;code&gt;{$N8N_DOMAIN}&lt;/code&gt; e &lt;code&gt;{$ACME_EMAIL}&lt;/code&gt; pelas variáveis de ambiente do contêiner, que o Compose preenche a partir do &lt;code&gt;.env&lt;/code&gt;. O &lt;code&gt;reverse_proxy&lt;/code&gt; já envia os cabeçalhos &lt;code&gt;X-Forwarded-For&lt;/code&gt;, &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; e &lt;code&gt;X-Forwarded-Host&lt;/code&gt;, que o n8n precisa receber atrás de um proxy.&lt;/p&gt;

&lt;h3&gt;
  
  
  docker-compose.yml
&lt;/h3&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="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caddy:2.11&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="p"&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="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"80:80"&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"443:443"&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"443:443/udp"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_DOMAIN=${N8N_DOMAIN}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ACME_EMAIL=${ACME_EMAIL}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./Caddyfile:/etc/caddy/Caddyfile:ro&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_data:/data&lt;/span&gt;
      &lt;span class="p"&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="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;n8n&lt;/span&gt;

  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:18&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PGDATA=/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_storage:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CMD-SHELL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&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="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

  &lt;span class="na"&gt;n8n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.n8n.io/n8nio/n8n:${N8N_VERSION}&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_HOST=${N8N_DOMAIN}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PORT=5678&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PROTOCOL=https&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_WEBHOOK_URL=https://${N8N_DOMAIN}/&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PROXY_HOPS=1&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GENERIC_TIMEZONE=${GENERIC_TIMEZONE}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TZ=${GENERIC_TIMEZONE}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_TYPE=postgresdb&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_HOST=postgres&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PORT=5432&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_DATABASE=${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_USER=${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_RUNNERS_MODE=external&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;n8n_data:/home/node/.n8n&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;

  &lt;span class="na"&gt;n8n-runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8nio/runners:${N8N_VERSION}&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_RUNNERS_TASK_BROKER_URI=http://n8n:5679&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&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="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db_storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O que vale saber sobre esse arquivo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;N8N_WEBHOOK_URL&lt;/code&gt; e &lt;code&gt;N8N_PROXY_HOPS=1&lt;/code&gt; fazem o n8n montar as URLs de webhook com o domínio público e confiar nos cabeçalhos do Caddy. Sem isso, o editor mostra URLs com a porta interna 5678. Em versões anteriores à 2.30.0, a variável se chama &lt;code&gt;WEBHOOK_URL&lt;/code&gt;, hoje obsoleta.&lt;/li&gt;
&lt;li&gt;O runner externo executa o código dos nós Code fora do processo do n8n. O modo interno, que é o padrão, está obsoleto e a documentação o desaconselha em produção: código que escape do sandbox fica com o mesmo acesso do n8n, inclusive às credenciais.&lt;/li&gt;
&lt;li&gt;O &lt;code&gt;PGDATA&lt;/code&gt; fixo mantém os dados do Postgres 18 dentro do volume. Não remova essa linha: sem ela, o Postgres 18 grava fora do volume e o banco começa vazio.&lt;/li&gt;
&lt;li&gt;O n8n continua com o volume em &lt;code&gt;/home/node/.n8n&lt;/code&gt; mesmo usando PostgreSQL. A documentação recomenda manter essa pasta persistente.&lt;/li&gt;
&lt;li&gt;Nenhuma porta do n8n ou do Postgres é publicada. O Caddy alcança o n8n pelo nome do serviço na rede do Compose.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Apontar o DNS e subir
&lt;/h2&gt;

&lt;p&gt;No painel DNS do seu domínio, crie um registro &lt;code&gt;A&lt;/code&gt; com o nome &lt;code&gt;n8n&lt;/code&gt; (ou o subdomínio escolhido) apontando para o IPv4 da VPS. Antes de subir, confira se o nome já resolve para o IP certo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short n8n.example.com A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Com o DNS resolvendo, suba a stack:&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
sudo docker compose up -d
sudo docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O n8n só sobe depois que o Postgres passa no healthcheck. Acompanhe os logs do n8n e do Caddy até ver o editor disponível e o certificado emitido:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Se o certificado não sair, confira se o DNS já propagou e se as portas 80 e 443 também estão abertas no firewall do provedor, quando houver um. Depois de corrigir, rode &lt;code&gt;sudo docker compose restart caddy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Por fim, teste de fora da VPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -fsS https://n8n.example.com/healthz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Abra &lt;code&gt;https://n8n.example.com&lt;/code&gt; no navegador e crie a conta de owner. Para conferir os webhooks, adicione um nó Webhook a um workflow: a URL de produção deve começar com &lt;code&gt;https://n8n.example.com/webhook/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Atualizar e fazer backup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Backup
&lt;/h3&gt;

&lt;p&gt;Um backup completo do n8n com PostgreSQL tem duas partes: o banco e a pasta &lt;code&gt;.n8n&lt;/code&gt;, que guarda a configuração e a chave de criptografia. Com a stack no ar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/n8n
&lt;span class="nb"&gt;set&lt;/span&gt; -a &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; . ./.env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; +a

sudo docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; -T postgres pg_dumpall -U &lt;span class="s2"&gt;"$POSTGRES_USER"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; n8n-db-&lt;span class="nv"&gt;$(&lt;/span&gt;date +%F&lt;span class="o"&gt;)&lt;/span&gt;.sql
grep -c &lt;span class="s2"&gt;"PostgreSQL database cluster dump complete"&lt;/span&gt; n8n-db-&lt;span class="nv"&gt;$(&lt;/span&gt;date +%F&lt;span class="o"&gt;)&lt;/span&gt;.sql

sudo docker run --rm --volumes-from &lt;span class="s2"&gt;"$(sudo docker compose ps -q n8n)"&lt;/span&gt; -v &lt;span class="s2"&gt;"$PWD"&lt;/span&gt;:/backup ubuntu:24.04 tar czf /backup/n8n-data-&lt;span class="nv"&gt;$(&lt;/span&gt;date +%F&lt;span class="o"&gt;)&lt;/span&gt;.tar.gz /home/node/.n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O &lt;code&gt;grep&lt;/code&gt; precisa imprimir &lt;code&gt;1&lt;/code&gt;. Se imprimir &lt;code&gt;0&lt;/code&gt;, o dump ficou incompleto. Depois, copie os dois arquivos e o &lt;code&gt;.env&lt;/code&gt; para fora da VPS, a partir da sua máquina:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkdir -p backups-n8n
scp &lt;span class="s1"&gt;'deploy@IP_DA_VPS:n8n/n8n-*'&lt;/span&gt; &lt;span class="s1"&gt;'deploy@IP_DA_VPS:n8n/.env'&lt;/span&gt; ./backups-n8n/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Um backup guardado só no disco da própria VPS se perde junto com ela. Para restaurar, a ordem é parar o n8n, devolver a pasta &lt;code&gt;.n8n&lt;/code&gt; e o banco e subir de novo com o mesmo &lt;code&gt;.env&lt;/code&gt;. A página &lt;a href="https://docs.n8n.io/deploy/host-n8n/keep-n8n-running/backup-and-restore" rel="noopener noreferrer"&gt;Back up and restore&lt;/a&gt; detalha cada passo.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nunca rode &lt;code&gt;docker compose down -v&lt;/code&gt; nesta pasta.&lt;/strong&gt; O &lt;code&gt;-v&lt;/code&gt; apaga também os volumes, incluindo o &lt;code&gt;n8n_data&lt;/code&gt; com a chave de criptografia e o &lt;code&gt;db_storage&lt;/code&gt; com o banco.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Atualizar
&lt;/h3&gt;

&lt;p&gt;O n8n lança uma versão nova quase toda semana. A documentação sugere atualizar pelo menos uma vez por mês, para não pular muitas versões de uma vez, e ler as &lt;a href="https://docs.n8n.io/changelog/release-notes-2.x" rel="noopener noreferrer"&gt;notas de versão&lt;/a&gt; antes. O roteiro:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Faça o backup acima.&lt;/li&gt;
&lt;li&gt;Troque &lt;code&gt;N8N_VERSION&lt;/code&gt; no &lt;code&gt;.env&lt;/code&gt; pela nova versão &lt;code&gt;stable&lt;/code&gt;. O runner acompanha, porque usa a mesma variável.&lt;/li&gt;
&lt;li&gt;Baixe as imagens e recrie os contêineres:
&lt;/li&gt;
&lt;/ol&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
sudo docker compose pull
sudo docker compose down
sudo docker compose up -d
sudo docker compose logs -f n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O n8n aplica as migrações do banco ao iniciar. Trocar a tag de volta não desfaz essas migrações, e é por isso que o backup vem antes. O &lt;code&gt;docker compose pull&lt;/code&gt; também traz a última versão 2.11 do Caddy e a última versão 18 do PostgreSQL. Mudar o Postgres de versão principal (de 18 para 19, por exemplo) exige dump e restauração, não só trocar a tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Próximos passos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ative a autenticação em dois fatores na conta de owner e configure SMTP para convites e redefinição de senha, com as variáveis de &lt;a href="https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/use-environment-variables/user-management-and-2fa" rel="noopener noreferrer"&gt;user management&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Agende o backup com &lt;code&gt;cron&lt;/code&gt; e mande as cópias para outro lugar automaticamente.&lt;/li&gt;
&lt;li&gt;Separe um usuário do Postgres sem privilégios de superusuário para o n8n, como no exemplo &lt;a href="https://github.com/n8n-io/n8n-hosting/tree/main/docker-compose/withPostgres" rel="noopener noreferrer"&gt;withPostgres&lt;/a&gt; do repositório oficial.&lt;/li&gt;
&lt;li&gt;Leia o guia de &lt;a href="https://docs.n8n.io/deploy/host-n8n/configure-n8n/security/harden-task-runners" rel="noopener noreferrer"&gt;hardening dos task runners&lt;/a&gt; se mais pessoas forem editar workflows na instância.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>braziliandevs</category>
      <category>portugues</category>
      <category>docker</category>
      <category>n8n</category>
    </item>
    <item>
      <title>How to install n8n with Docker on a VPS</title>
      <dc:creator>Fernando Chagas</dc:creator>
      <pubDate>Wed, 11 Nov 2026 13:01:14 +0000</pubDate>
      <link>https://dev.to/fernandochagas/how-to-install-n8n-with-docker-on-a-vps-2obo</link>
      <guid>https://dev.to/fernandochagas/how-to-install-n8n-with-docker-on-a-vps-2obo</guid>
      <description>&lt;p&gt;This guide sets up self-hosted n8n for light production use on an Ubuntu 24.04 VPS: n8n backed by PostgreSQL, the task runner in its own container (which is what the n8n docs recommend for production), and Caddy in front to issue and renew the HTTPS certificate. The whole stack runs on Docker Compose from three files, and the guide also covers updates and backups.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A VPS running 64-bit Ubuntu 24.04, with SSH access as a user that has &lt;code&gt;sudo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A domain where you can create DNS records. The examples use &lt;code&gt;n8n.example.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An email address for the certificate's ACME account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The n8n docs don't set a hardware minimum for this exact setup. The official Docker Compose guide asks for 2 vCPUs and 4 GB of RAM once you add the n8n Assistant sandbox. This stack runs four containers (Caddy, n8n, the runner and PostgreSQL), so treat 2 vCPUs and 4 GB as the floor. With 4 vCPUs and 8 GB you have room for executions that move more data and for the database to grow.&lt;/p&gt;

&lt;p&gt;If you just want to try n8n, &lt;a href="https://nuvits.net/en" rel="noopener noreferrer"&gt;Nuvits&lt;/a&gt; can also deliver the VPS with n8n preinstalled when you order it. That option runs n8n in a single container, without PostgreSQL or an external runner, and it only answers inside the VPS: you open the editor through an SSH tunnel. To receive webhooks from the internet with a domain and HTTPS, follow this guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prepare the server
&lt;/h2&gt;

&lt;p&gt;If your provider hands you the VPS with &lt;code&gt;root&lt;/code&gt; access, create a user with &lt;code&gt;sudo&lt;/code&gt; and copy your SSH key over:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Open a second session as &lt;code&gt;deploy&lt;/code&gt; and make sure you can log in before closing the &lt;code&gt;root&lt;/code&gt; one. Then allow SSH, HTTP and HTTPS in ufw and turn the firewall on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443
sudo ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
sudo ufw status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ufw enable&lt;/code&gt; asks for confirmation because it may drop the SSH connection; with the port 22 rule already in place, answer &lt;code&gt;y&lt;/code&gt;. The &lt;code&gt;443&lt;/code&gt; rule has no protocol, so it covers both TCP and UDP. UDP is for HTTP/3, which Caddy also serves.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Docker bypasses ufw.&lt;/strong&gt; A port published with &lt;code&gt;ports:&lt;/code&gt; in Compose is reachable from outside even without a ufw rule, because Docker routes that traffic before it reaches the firewall's rules. That's why only Caddy publishes ports (80 and 443) in this guide. n8n and PostgreSQL stay on the internal Compose network.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Install Docker
&lt;/h2&gt;

&lt;p&gt;At Nuvits you can have Docker preinstalled when you order the VPS. If that's your case, skip ahead to &lt;code&gt;docker compose version&lt;/code&gt; at the end of this section.&lt;/p&gt;

&lt;p&gt;For servers, the Docker docs point to the official &lt;code&gt;apt&lt;/code&gt; repository. First remove any unofficial packages that could conflict (on a fresh install, &lt;code&gt;apt&lt;/code&gt; will usually report there's nothing to remove):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sudo apt remove &lt;span class="nv"&gt;$(&lt;/span&gt;dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc &lt;span class="o"&gt;|&lt;/span&gt; cut -f1&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add Docker's GPG key and repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

sudo tee /etc/apt/sources.list.d/docker.sources &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: &lt;span class="nv"&gt;$(&lt;/span&gt;. /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"${UBUNTU_CODENAME:-$VERSION_CODENAME}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
Components: stable
Architectures: &lt;span class="nv"&gt;$(&lt;/span&gt;dpkg --print-architecture&lt;span class="o"&gt;)&lt;/span&gt;
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install Docker Engine and the Compose plugin, then check that everything works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo docker run hello-world
docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also the &lt;code&gt;get.docker.com&lt;/code&gt; convenience script, but Docker's own docs recommend it only for testing and development, not production.&lt;/p&gt;

&lt;p&gt;The commands in this guide use &lt;code&gt;sudo docker&lt;/code&gt;. You can add your user to the &lt;code&gt;docker&lt;/code&gt; group to drop the &lt;code&gt;sudo&lt;/code&gt;, but the Docker docs warn that the group grants root-level privileges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure n8n
&lt;/h2&gt;

&lt;p&gt;Create a project directory. Compose uses its name as the prefix for volumes and containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkdir -p ~/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;
  
  
  Secrets in .env
&lt;/h3&gt;

&lt;p&gt;Generate three random values: one for the encryption key, one for the runner token and one for the database password.&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 -hex 32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it three times and paste the results into &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="na"&gt;N8N_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.47&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="na"&gt;N8N_DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n8n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="na"&gt;ACME_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;you&lt;/span&gt;@&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="na"&gt;GENERIC_TIMEZONE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;America&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;New_York&lt;/span&gt;

&lt;span class="na"&gt;N8N_ENCRYPTION_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;paste&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;here&lt;/span&gt;
&lt;span class="na"&gt;N8N_RUNNERS_AUTH_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;paste&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;here&lt;/span&gt;

&lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n8n&lt;/span&gt;
&lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;paste&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;third&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;here&lt;/span&gt;
&lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n8n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;N8N_VERSION&lt;/code&gt; pins both the n8n and the runner images, which must run the same version. Use the latest &lt;code&gt;stable&lt;/code&gt; release listed on &lt;a href="https://github.com/n8n-io/n8n/releases" rel="noopener noreferrer"&gt;n8n's releases page&lt;/a&gt;; &lt;code&gt;2.47.4&lt;/code&gt; was the stable version when this was written. Set &lt;code&gt;GENERIC_TIMEZONE&lt;/code&gt; to your own IANA time zone name, such as &lt;code&gt;Europe/Berlin&lt;/code&gt; or &lt;code&gt;America/Sao_Paulo&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keep the key safe.&lt;/strong&gt; n8n uses &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; to encrypt the credentials it stores in the database. Without it, a restored backup will load, but none of its credentials can be decrypted. Keep a copy of &lt;code&gt;.env&lt;/code&gt; somewhere off the VPS, such as a password manager.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Caddyfile
&lt;/h3&gt;

&lt;p&gt;Caddy gets the certificate on its own as soon as it sees a domain name in the site address. &lt;code&gt;flush_interval -1&lt;/code&gt; turns off response buffering, as in n8n's official Caddy example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    email {$ACME_EMAIL}
}

{$N8N_DOMAIN} {
    reverse_proxy n8n:5678 {
        flush_interval -1
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caddy fills in &lt;code&gt;{$N8N_DOMAIN}&lt;/code&gt; and &lt;code&gt;{$ACME_EMAIL}&lt;/code&gt; from the container's environment, which Compose sets from &lt;code&gt;.env&lt;/code&gt;. &lt;code&gt;reverse_proxy&lt;/code&gt; already sends the &lt;code&gt;X-Forwarded-For&lt;/code&gt;, &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; and &lt;code&gt;X-Forwarded-Host&lt;/code&gt; headers that n8n needs behind a proxy.&lt;/p&gt;

&lt;h3&gt;
  
  
  docker-compose.yml
&lt;/h3&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="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caddy:2.11&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="p"&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="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"80:80"&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"443:443"&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"443:443/udp"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_DOMAIN=${N8N_DOMAIN}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ACME_EMAIL=${ACME_EMAIL}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./Caddyfile:/etc/caddy/Caddyfile:ro&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_data:/data&lt;/span&gt;
      &lt;span class="p"&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="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;n8n&lt;/span&gt;

  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:18&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PGDATA=/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_storage:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CMD-SHELL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&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="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

  &lt;span class="na"&gt;n8n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.n8n.io/n8nio/n8n:${N8N_VERSION}&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_HOST=${N8N_DOMAIN}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PORT=5678&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PROTOCOL=https&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_WEBHOOK_URL=https://${N8N_DOMAIN}/&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PROXY_HOPS=1&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GENERIC_TIMEZONE=${GENERIC_TIMEZONE}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TZ=${GENERIC_TIMEZONE}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_TYPE=postgresdb&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_HOST=postgres&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PORT=5432&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_DATABASE=${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_USER=${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_RUNNERS_MODE=external&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;n8n_data:/home/node/.n8n&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;

  &lt;span class="na"&gt;n8n-runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8nio/runners:${N8N_VERSION}&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_RUNNERS_TASK_BROKER_URI=http://n8n:5679&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&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="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db_storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth knowing about this file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;N8N_WEBHOOK_URL&lt;/code&gt; and &lt;code&gt;N8N_PROXY_HOPS=1&lt;/code&gt; make n8n build webhook URLs with the public domain and trust Caddy's forwarded headers. Without them, the editor shows URLs with the internal port 5678. Before n8n 2.30.0 the variable was called &lt;code&gt;WEBHOOK_URL&lt;/code&gt;, which is now deprecated.&lt;/li&gt;
&lt;li&gt;The external runner executes Code node scripts outside the n8n process. Internal mode, the default, is deprecated, and the docs advise against it in production: code that escapes the sandbox gets the same access as n8n, credentials included.&lt;/li&gt;
&lt;li&gt;Pinning &lt;code&gt;PGDATA&lt;/code&gt; keeps Postgres 18's data inside the volume. Don't remove that line. Without it, Postgres 18 writes outside the volume and the database starts empty.&lt;/li&gt;
&lt;li&gt;n8n keeps its volume at &lt;code&gt;/home/node/.n8n&lt;/code&gt; even with PostgreSQL. The docs recommend keeping that folder persistent.&lt;/li&gt;
&lt;li&gt;Neither n8n nor Postgres publishes a port. Caddy reaches n8n by its service name on the Compose network.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Point DNS and start
&lt;/h2&gt;

&lt;p&gt;In your domain's DNS settings, create an &lt;code&gt;A&lt;/code&gt; record named &lt;code&gt;n8n&lt;/code&gt; (or whatever subdomain you picked) pointing to the VPS's IPv4 address. Before starting the stack, check that the name already resolves to the right IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short n8n.example.com A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once DNS resolves, bring the stack up:&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
sudo docker compose up -d
sudo docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;n8n only starts after Postgres passes its healthcheck. Follow the n8n and Caddy logs until the editor is up and the certificate has been issued:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;If the certificate doesn't come through, check that DNS has propagated and that ports 80 and 443 are also open in your provider's firewall, if it has one. Once that's fixed, run &lt;code&gt;sudo docker compose restart caddy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then test from outside the VPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -fsS https://n8n.example.com/healthz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;https://n8n.example.com&lt;/code&gt; in your browser and create the owner account. To check webhooks, add a Webhook node to a workflow: its production URL should start with &lt;code&gt;https://n8n.example.com/webhook/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update and back up
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Backups
&lt;/h3&gt;

&lt;p&gt;A full backup of n8n on PostgreSQL has two parts: the database, and the &lt;code&gt;.n8n&lt;/code&gt; folder that holds the configuration and the encryption key. With the stack running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/n8n
&lt;span class="nb"&gt;set&lt;/span&gt; -a &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; . ./.env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; +a

sudo docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; -T postgres pg_dumpall -U &lt;span class="s2"&gt;"$POSTGRES_USER"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; n8n-db-&lt;span class="nv"&gt;$(&lt;/span&gt;date +%F&lt;span class="o"&gt;)&lt;/span&gt;.sql
grep -c &lt;span class="s2"&gt;"PostgreSQL database cluster dump complete"&lt;/span&gt; n8n-db-&lt;span class="nv"&gt;$(&lt;/span&gt;date +%F&lt;span class="o"&gt;)&lt;/span&gt;.sql

sudo docker run --rm --volumes-from &lt;span class="s2"&gt;"$(sudo docker compose ps -q n8n)"&lt;/span&gt; -v &lt;span class="s2"&gt;"$PWD"&lt;/span&gt;:/backup ubuntu:24.04 tar czf /backup/n8n-data-&lt;span class="nv"&gt;$(&lt;/span&gt;date +%F&lt;span class="o"&gt;)&lt;/span&gt;.tar.gz /home/node/.n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;grep&lt;/code&gt; must print &lt;code&gt;1&lt;/code&gt;. If it prints &lt;code&gt;0&lt;/code&gt;, the dump is incomplete. Then, from your own machine, copy both files and &lt;code&gt;.env&lt;/code&gt; off the VPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkdir -p backups-n8n
scp &lt;span class="s1"&gt;'deploy@VPS_IP:n8n/n8n-*'&lt;/span&gt; &lt;span class="s1"&gt;'deploy@VPS_IP:n8n/.env'&lt;/span&gt; ./backups-n8n/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A backup that only lives on the VPS's disk is gone if you lose the VPS. To restore, stop n8n, put back the &lt;code&gt;.n8n&lt;/code&gt; folder and the database, and start again with the same &lt;code&gt;.env&lt;/code&gt;. The &lt;a href="https://docs.n8n.io/deploy/host-n8n/keep-n8n-running/backup-and-restore" rel="noopener noreferrer"&gt;Back up and restore&lt;/a&gt; page walks through each step.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Never run &lt;code&gt;docker compose down -v&lt;/code&gt; in this directory.&lt;/strong&gt; The &lt;code&gt;-v&lt;/code&gt; also deletes the volumes, including &lt;code&gt;n8n_data&lt;/code&gt; with the encryption key and &lt;code&gt;db_storage&lt;/code&gt; with the database.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Updates
&lt;/h3&gt;

&lt;p&gt;n8n ships a new minor version most weeks. The docs suggest updating at least once a month, so you don't have to jump several versions at once, and reading the &lt;a href="https://docs.n8n.io/changelog/release-notes-2.x" rel="noopener noreferrer"&gt;release notes&lt;/a&gt; first. The routine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take the backup above.&lt;/li&gt;
&lt;li&gt;Change &lt;code&gt;N8N_VERSION&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; to the new &lt;code&gt;stable&lt;/code&gt; version. The runner follows along, since it uses the same variable.&lt;/li&gt;
&lt;li&gt;Pull the images and recreate the containers:
&lt;/li&gt;
&lt;/ol&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
sudo docker compose pull
sudo docker compose down
sudo docker compose up -d
sudo docker compose logs -f n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;n8n runs its database migrations on startup. Switching the tag back doesn't undo them, which is why the backup comes first. &lt;code&gt;docker compose pull&lt;/code&gt; also fetches the latest Caddy 2.11 and PostgreSQL 18 images. Moving Postgres to a new major version (18 to 19, for example) takes a dump and restore, not just a tag change.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Turn on two-factor authentication for the owner account and set up SMTP for invites and password resets, using the &lt;a href="https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/use-environment-variables/user-management-and-2fa" rel="noopener noreferrer"&gt;user management&lt;/a&gt; variables.&lt;/li&gt;
&lt;li&gt;Schedule the backup with &lt;code&gt;cron&lt;/code&gt; and ship the copies somewhere else automatically.&lt;/li&gt;
&lt;li&gt;Give n8n its own Postgres user without superuser rights, as in the official &lt;a href="https://github.com/n8n-io/n8n-hosting/tree/main/docker-compose/withPostgres" rel="noopener noreferrer"&gt;withPostgres&lt;/a&gt; example.&lt;/li&gt;
&lt;li&gt;Read the &lt;a href="https://docs.n8n.io/deploy/host-n8n/configure-n8n/security/harden-task-runners" rel="noopener noreferrer"&gt;task runner hardening&lt;/a&gt; guide if more people will be editing workflows on the instance.&lt;/li&gt;
&lt;/ul&gt;

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