<?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: Oscar Ricardo Sánche Gutierréz</title>
    <description>The latest articles on DEV Community by Oscar Ricardo Sánche Gutierréz (@oscar_ricardosncheguti).</description>
    <link>https://dev.to/oscar_ricardosncheguti</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3804416%2F2f910017-a0c5-45f4-99ba-cef5a096c7de.jpg</url>
      <title>DEV Community: Oscar Ricardo Sánche Gutierréz</title>
      <link>https://dev.to/oscar_ricardosncheguti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oscar_ricardosncheguti"/>
    <language>en</language>
    <item>
      <title>Build an AI Email Assistant with n8n and Telegram</title>
      <dc:creator>Oscar Ricardo Sánche Gutierréz</dc:creator>
      <pubDate>Fri, 01 May 2026 23:16:32 +0000</pubDate>
      <link>https://dev.to/oscar_ricardosncheguti/build-an-ai-email-assistant-with-n8n-and-telegram-3ld5</link>
      <guid>https://dev.to/oscar_ricardosncheguti/build-an-ai-email-assistant-with-n8n-and-telegram-3ld5</guid>
      <description>&lt;p&gt;Is your inbox flooded with repetitive emails? Support queries, FAQs, information requests… they all eat up time and require manual replies.&lt;/p&gt;

&lt;p&gt;In this article you'll build an &lt;strong&gt;automated email assistant&lt;/strong&gt; that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads incoming emails via IMAP&lt;/li&gt;
&lt;li&gt;Analyzes them with &lt;strong&gt;DeepSeek&lt;/strong&gt; using a custom knowledge base&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replies automatically&lt;/strong&gt; when the model has enough context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notifies you via Telegram&lt;/strong&gt; when it needs human review&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo07j8llinogs5ngd9yw6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo07j8llinogs5ngd9yw6.png" alt="Architecture" width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Diagram created with &lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;https://savnet.co&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;ul&gt;
&lt;li&gt;A server with &lt;strong&gt;n8n up and running&lt;/strong&gt; (if you don't have one, follow &lt;a href="https://dev.to/oscar_ricardosncheguti/como-instalar-n8n-en-un-servidor-ubuntu-produccion-lista-2kf2"&gt;this installation guide&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;email account&lt;/strong&gt; with IMAP access (ZohoMail, Gmail, Outlook, or any provider that supports it)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;DeepSeek API key&lt;/strong&gt; (&lt;a href="https://platform.deepseek.com" rel="noopener noreferrer"&gt;platform.deepseek.com&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Telegram bot&lt;/strong&gt; created via &lt;a href="https://t.me/BotFather" rel="noopener noreferrer"&gt;https://t.me/BotFather&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Telegram bots have no usage costs, making them ideal for automations with no message limits.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. Getting your credentials
&lt;/h2&gt;

&lt;h3&gt;
  
  
  DeepSeek API
&lt;/h3&gt;

&lt;p&gt;Sign up at &lt;a href="https://platform.deepseek.com" rel="noopener noreferrer"&gt;platform.deepseek.com&lt;/a&gt;, head to the API Keys section and create a new one. You'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DEEPSEEK_API_KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Endpoint URL: &lt;code&gt;https://api.deepseek.com/v1/chat/completions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Recommended model: &lt;code&gt;deepseek-chat&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Telegram Bot
&lt;/h3&gt;

&lt;p&gt;Creating a Telegram bot is quick and requires no company registration or credit cards. Open Telegram, search for &lt;code&gt;@BotFather&lt;/code&gt;, send &lt;code&gt;/newbot&lt;/code&gt; and follow the steps. At the end you'll get a token like &lt;code&gt;4839574812:AAFD39kkdpWt3ywyRZergyOLMaJhac60qc&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For more details on bot creation: &lt;a href="https://dev.to/simplr_sh/telegram-bot-creation-handbook-g5g"&gt;Telegram Bot Creation Handbook&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Getting your Chat ID
&lt;/h4&gt;

&lt;p&gt;You need to know your &lt;strong&gt;Chat ID&lt;/strong&gt; (your Telegram user ID) so the bot knows where to send messages.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Telegram and search for &lt;code&gt;@userinfobot&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Start the conversation with &lt;strong&gt;Start&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The bot replies automatically with your info. Look for the line that says &lt;strong&gt;Id:&lt;/strong&gt; — that number is your Chat ID
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Id: 1234567890   &amp;lt;&amp;lt;&amp;lt; This is your Chat ID
First: YourName
Lang: en
....
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save both values (Bot Token and Chat ID) for later use in n8n.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Building the workflow in n8n
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3471rbtkax4tjelgo6lm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3471rbtkax4tjelgo6lm.png" alt="Workflow" width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're new to n8n, the first step is creating a workflow. At each stage I'll tell you which node to add so you can find it like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjnpm3er70jhpxgmas7if.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjnpm3er70jhpxgmas7if.gif" alt="Add node" width="731" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step by step:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Node 1 — IMAP Trigger&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add the &lt;strong&gt;Email Trigger (IMAP)&lt;/strong&gt; node, which will be our workflow's trigger and will check for unread emails in our inbox.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the Credential field, configure the following (values and setup vary by email provider):&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User&lt;/strong&gt;: your email&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password&lt;/strong&gt;: your password or app password&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host&lt;/strong&gt;: your IMAP server (e.g. &lt;code&gt;imap.gmail.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port&lt;/strong&gt;: &lt;code&gt;993&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure&lt;/strong&gt;: &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;In the rest of the form, fill in:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mailbox Name:&lt;/strong&gt; the mailbox to pull emails from, defaults to &lt;code&gt;INBOX&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action:&lt;/strong&gt; Mark as Read — this way every processed email gets marked as read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F41mvk2gm2vinuiiplulk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F41mvk2gm2vinuiiplulk.png" alt="Imap setup" width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By default, once the workflow is published the node will poll periodically (every 60 seconds by default) to detect new emails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node 2 — Code (Knowledge Base)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvhslhoxd1fnb11v5ffcm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvhslhoxd1fnb11v5ffcm.png" alt="Code Know" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The "secret sauce" of the assistant lives here. Instead of letting DeepSeek improvise, you define a clear context with the responses you expect.&lt;/p&gt;

&lt;p&gt;Add a &lt;strong&gt;Code&lt;/strong&gt; node (&lt;code&gt;Code in Javascript&lt;/code&gt;), switch the mode to &lt;strong&gt;"Run Once for All Items"&lt;/strong&gt; (top-right of the editor) and connect it to Node 1 (IMAP).&lt;/p&gt;

&lt;p&gt;The code must &lt;strong&gt;preserve the email data we care about&lt;/strong&gt; and add the &lt;code&gt;knowledgeBase&lt;/code&gt; field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;knowledgeBase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
Eres un asistente de soporte técnico de [Tu Empresa].
Responde solo si encuentras una coincidencia clara en las políticas siguientes.
Si no estás seguro, responde exactamente: NO_PUEDO_RESPONDER
POLÍTICAS:
1. Horario de atención: Lunes a viernes de 9:00 a 18:00 (GMT-5).
2. Tiempo de respuesta estimado: 24 horas hábiles.
3. Contraseñas: Nunca solicites ni compartas contraseñas por correo.
4. Reembolsos: Se procesan dentro de los primeros 30 días. El usuario debe enviar un comprobante.
5. Problemas técnicos comunes:
   - Error 403: El usuario debe limpiar caché y cookies.
   - Error 500: Reportar al equipo interno, pedir al usuario que espere 1 hora.
   - Olvido de contraseña: Enviar enlace de restablecimiento a su correo registrado.
6. Facturación: Para cambios de plan o facturación, responder que el usuario ingrese al panel de pago.
INSTRUCCIONES:
- Responde de forma amable y profesional.
- Usa el mismo idioma del correo recibido.
- Si el correo contiene más de una pregunta, responde cada punto.
- Si el correo es grosero o agresivo, responde: NO_PUEDO_RESPONDER
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;json&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;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;knowledgeBase&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;blockquote&gt;
&lt;p&gt;You can make this as detailed as you want. The more context, the better the responses. Using &lt;code&gt;...item.json&lt;/code&gt; preserves the original email data so Node 3 (content extraction) receives it without issues.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Name this node, for example, &lt;code&gt;CodeKnow&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node 3 — Code (Extract Content)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use another &lt;strong&gt;Code&lt;/strong&gt; node (&lt;code&gt;Code in Javascript&lt;/code&gt;) to extract the relevant information from each email and generate the &lt;code&gt;chatInput&lt;/code&gt; with the prompt we'll send to our AI. Switch the mode to &lt;strong&gt;"Run for Each Item"&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textPlain&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textPlain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textHtml&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textHtml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;chatInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`From: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nAsunto: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nCuerpo: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59kmhs8r2vb0mu3fr5ft.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59kmhs8r2vb0mu3fr5ft.png" alt="Process Emails" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node 4 — DeepSeek Chat Model (Configuration)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add a &lt;strong&gt;DeepSeek Chat Model&lt;/strong&gt; node. It will likely auto-generate 3 nodes: &lt;strong&gt;When chatmessage received&lt;/strong&gt;, &lt;strong&gt;Basic LLM Chain&lt;/strong&gt; and &lt;strong&gt;DeepSeek Chat Model&lt;/strong&gt;. Keep only the LLM and DeepSeek — the extra one isn't needed. Connect the code block to the Basic LLM node like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqiry1ag515xd3ymu4fry.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqiry1ag515xd3ymu4fry.gif" alt="Setup Deepseek" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the remaining blocks, set up the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In DeepSeek Chat Model, just configure the credentials:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credential&lt;/strong&gt;: select or create a &lt;strong&gt;DeepSeek account&lt;/strong&gt; credential

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Key&lt;/strong&gt;: your DeepSeek key&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Model&lt;/strong&gt;: &lt;code&gt;deepseek-v4-flash&lt;/code&gt;
&lt;/li&gt;

&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;In Basic LLM Chain (processor):&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source for Prompt&lt;/strong&gt;: Define below&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt (User Message)&lt;/strong&gt;: Analyze this email and provide a short, clear response to send via Telegram.
{{ $json.chatInput }}&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat Messages&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type&lt;/strong&gt;: &lt;code&gt;System&lt;/code&gt; → &lt;strong&gt;Message&lt;/strong&gt;: &lt;code&gt;{{ $json.knowledgeBase }}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Require Specific Output Format&lt;/strong&gt;: disabled&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;What we've done is send a prompt with our intent and the email data defined in the &lt;code&gt;CodeProcessEmail&lt;/code&gt; block, along with a system message containing our knowledge base — the info we defined in the &lt;code&gt;CodeKnow&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5aa054pbbgfytmaso6kh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5aa054pbbgfytmaso6kh.png" alt="Setup ai" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What about cost?&lt;/strong&gt; DeepSeek v4 charges ~$0.14 per million input tokens and ~$0.28 per output. A typical email (~1,000 tokens) costs ~$0.0002. With 1,000 emails a month you'd spend ~$0.20. In the production tweaks section I'll show you how to filter to avoid unnecessary calls.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The node takes the &lt;code&gt;chatInput&lt;/code&gt; from Node 3 (knowledge base + email) and returns the response generated by DeepSeek.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node 5 — Code (Map Assistant Response)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ix7vgknjavu179ysms7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ix7vgknjavu179ysms7.png" alt="Code Normalize" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The DeepSeek block only returns the AI's raw response, which means we lose the full email data we'll need later. That's why we add this &lt;strong&gt;Code&lt;/strong&gt; node in &lt;strong&gt;"Run Once for All Items"&lt;/strong&gt; mode to merge the AI response with the original email data and determine whether it can reply or not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aiItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CodeProcessEmail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aiItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;emailItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aiText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;json&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;span class="nx"&gt;originalEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;aiResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;aiText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;canReply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;aiText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NO_PUEDO_RESPONDER&lt;/span&gt;&lt;span class="dl"&gt;'&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;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This block is key because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It grabs the original email data (&lt;code&gt;subject&lt;/code&gt;, &lt;code&gt;from&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;It adds &lt;code&gt;aiResponse&lt;/code&gt; with the clean response from DeepSeek&lt;/li&gt;
&lt;li&gt;It adds &lt;code&gt;canReply&lt;/code&gt; (&lt;code&gt;true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt;) to decide whether to auto-reply&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Node 6 — IF (Can it reply?)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbr4v6kau04egpkc8ix2t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbr4v6kau04egpkc8ix2t.png" alt="Condition" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the AI response and email data at hand, we add an &lt;code&gt;if&lt;/code&gt; block. This node evaluates the &lt;code&gt;{{ $json.canReply }}&lt;/code&gt; field with the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (&lt;code&gt;true&lt;/code&gt;) → goes to SMTP send&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No&lt;/strong&gt; (&lt;code&gt;false&lt;/code&gt;) → goes to Telegram notification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Node 7.1 — Email (SMTP) — Reply&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ymimhbfavejgcwvtztj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ymimhbfavejgcwvtztj.png" alt="Smtp" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Configure the &lt;code&gt;Send Email&lt;/code&gt; node to send the automatic reply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credential:&lt;/strong&gt; a window will open for you to register your SMTP credentials:

&lt;ul&gt;
&lt;li&gt;User&lt;/li&gt;
&lt;li&gt;Password&lt;/li&gt;
&lt;li&gt;Host&lt;/li&gt;
&lt;li&gt;Port&lt;/li&gt;
&lt;li&gt;SSL&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;From Email&lt;/strong&gt;: we'll tell it to reply from the same address they wrote to: &lt;code&gt;{{ $json.to }}&lt;/code&gt;
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;To&lt;/strong&gt;: the person who wrote in: &lt;code&gt;{{ $json.from }}&lt;/code&gt;
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Subject&lt;/strong&gt;: &lt;code&gt;Re: {{ $('CodeProcessEmail').item.json.subject }}&lt;/code&gt;
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Email Format:&lt;/strong&gt; HTML&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Text&lt;/strong&gt;: &lt;code&gt;{{ $json.aiResponse }}&lt;/code&gt;
&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Node 7.2 — Telegram — Send Notification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For emails the AI can't handle, we won't send an automatic reply. Instead, you'll receive an alert via &lt;strong&gt;Telegram&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;However, we first need to make sure we're not sending messages longer than 4,000 characters — Telegram's limit. To handle this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a &lt;strong&gt;Code&lt;/strong&gt; node (JavaScript) in &lt;code&gt;Run Once for All Items&lt;/code&gt; mode with the following code:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;escapeHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;amp;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;escapeHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sin asunto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aiResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;escapeHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aiResponse&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;aiResponse&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n━━━━━━━━━━━━━━━━━━━━\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ---- split into chunks ----&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3900&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`━━━━━━━━━━━━━━━━━━━━\n&amp;lt;b&amp;gt;Recibiste los siguientes correos:&amp;lt;/b&amp;gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fullText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalParts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fullText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;fullText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;totalParts&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fullText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp0djnri63hf41ogfbfmn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp0djnri63hf41ogfbfmn.png" alt="Telegram Code" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add an &lt;code&gt;HTTP Request&lt;/code&gt; node to send each of the messages we defined:&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method:&lt;/strong&gt; POST&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://api.telegram.org/bot{YourTelegramToken,format:0000:xxxxxx}/sendMessage" rel="noopener noreferrer"&gt;https://api.telegram.org/bot{YourTelegramToken,format:0000:xxxxxx}/sendMessage&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication:&lt;/strong&gt; none&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SendBody:&lt;/strong&gt; true&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Body Content Type:&lt;/strong&gt; JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specify Body:&lt;/strong&gt; Using Fields Below

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;chat_id:&lt;/strong&gt; your Telegram conversation ID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;text:&lt;/strong&gt; {{ $json.text }}&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;parse_mode:&lt;/strong&gt; HTML&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;disable_web_page_preview:&lt;/strong&gt; TRUE&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs05klyth8sifcm5mt8h9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs05klyth8sifcm5mt8h9.png" alt="Telegram1" width="800" height="329"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fewk2kcrhs8cc41qprauo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fewk2kcrhs8cc41qprauo.png" alt="Telegram2" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;That's it — our workflow is done :D&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftys3dy12p6y5wtmes5n5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftys3dy12p6y5wtmes5n5.png" alt="Workflow" width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Testing the workflow
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Before going live, run controlled tests.&lt;/strong&gt; Don't connect this workflow directly to your personal or company inbox. Use a test email account (IMAP + SMTP) that contains no sensitive or confidential information. If something goes wrong — a bad AI interpretation, a misconfigured domain — you could end up auto-replying to real customers with incorrect information. Better safe than sorry.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With the workflow ready, do the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run it manually&lt;/strong&gt; by clicking the &lt;strong&gt;Execute Workflow&lt;/strong&gt; button (top right). This triggers the workflow immediately without waiting for automatic polling.&lt;/li&gt;
&lt;li&gt;Send yourself a test email with a question that's covered in your knowledge base.&lt;/li&gt;
&lt;li&gt;Check that you receive the automatic reply in the test sender's inbox.&lt;/li&gt;
&lt;li&gt;Send another email with something outside your knowledge base.&lt;/li&gt;
&lt;li&gt;You should receive a Telegram notification with the email content.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When everything works as expected, just click &lt;strong&gt;Publish&lt;/strong&gt; to enable polling and put it into production.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Production tweaks
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Filter by sender
&lt;/h3&gt;

&lt;p&gt;Add an &lt;strong&gt;IF&lt;/strong&gt; node after the trigger to only process emails from specific domains or addresses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowedDomains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yourdomain.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;allowedDomains&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rate limiting
&lt;/h3&gt;

&lt;p&gt;DeepSeek has rate limits per minute. If you expect a high volume of emails, add a &lt;strong&gt;Wait&lt;/strong&gt; node of 1-2 seconds between processing batches, or use a queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conversation history
&lt;/h3&gt;

&lt;p&gt;If you want DeepSeek to remember previous conversations, you can store the history in a database (PostgreSQL, Redis) and pass it as additional context on each iteration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling attachments
&lt;/h3&gt;

&lt;p&gt;DeepSeek doesn't process files directly. If you receive emails with attachments, you can use a &lt;strong&gt;Code&lt;/strong&gt; node to extract text from PDFs or images (via OCR) and pass it as context.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You now have an automated email assistant that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reads emails via IMAP without missing any&lt;/li&gt;
&lt;li&gt;Analyzes them with DeepSeek against your own knowledge base&lt;/li&gt;
&lt;li&gt;Replies automatically when it can&lt;/li&gt;
&lt;li&gt;Notifies you via Telegram when it needs your input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This doesn't replace a support team, but it absorbs the volume of repetitive questions and frees up time for what really matters.&lt;/p&gt;

&lt;p&gt;If you want to visually document this workflow or more complex infrastructure diagrams, &lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;Savnet&lt;/a&gt; helps you design them. And if you need to continuously validate this kind of automation, &lt;a href="https://savfalconeye.com" rel="noopener noreferrer"&gt;SavFalconEye&lt;/a&gt; lets you keep confidence in every change.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Ready to give it a try? Let me know how it went in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Crea un asistente de correo con IA usando n8n y Telegram</title>
      <dc:creator>Oscar Ricardo Sánche Gutierréz</dc:creator>
      <pubDate>Fri, 01 May 2026 23:10:52 +0000</pubDate>
      <link>https://dev.to/oscar_ricardosncheguti/crea-un-asistente-de-correo-con-ia-usando-n8n-y-telegram-2oed</link>
      <guid>https://dev.to/oscar_ricardosncheguti/crea-un-asistente-de-correo-con-ia-usando-n8n-y-telegram-2oed</guid>
      <description>&lt;p&gt;¿Tu bandeja de entrada está llena de correos repetitivos? Consultas de soporte, preguntas frecuentes, solicitudes de información... todo requiere tiempo y respuestas manuales.&lt;/p&gt;

&lt;p&gt;En este artículo vas a construir un &lt;strong&gt;asistente automatizado de correo electrónico&lt;/strong&gt; que:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lee los correos entrantes vía IMAP&lt;/li&gt;
&lt;li&gt;Los analiza con &lt;strong&gt;DeepSeek&lt;/strong&gt; usando una base de conocimiento personalizada&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responde automáticamente&lt;/strong&gt; si el modelo tiene suficiente contexto&lt;/li&gt;
&lt;li&gt;Te &lt;strong&gt;notifica por Telegram&lt;/strong&gt; si necesita revisión manual&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fadehev3uzqlti0129n2l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fadehev3uzqlti0129n2l.png" alt="Architecture" width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Diagrama realizado con &lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;https://savnet.co&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  ¿Qué necesitas?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Un servidor con &lt;strong&gt;n8n funcionando&lt;/strong&gt; (si no lo tienes, sigue &lt;a href="https://dev.to/oscar_ricardosncheguti/como-instalar-n8n-en-un-servidor-ubuntu-produccion-lista-2kf2"&gt;esta guía de instalación&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Una &lt;strong&gt;cuenta de correo&lt;/strong&gt; con acceso IMAP (ZohoMail, Gmail, Outlook, o cualquier proveedor que lo soporte)&lt;/li&gt;
&lt;li&gt;Una &lt;strong&gt;clave API de DeepSeek&lt;/strong&gt; (&lt;a href="https://platform.deepseek.com" rel="noopener noreferrer"&gt;platform.deepseek.com&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Un &lt;strong&gt;bot de Telegram&lt;/strong&gt; creado con &lt;a href="https://t.me/BotFather" rel="noopener noreferrer"&gt;https://t.me/BotFather&lt;/a&gt;
&amp;gt; &lt;strong&gt;Nota:&lt;/strong&gt; Telegram no tiene costos asociados por uso de bots, ideal para automatizaciones sin límites de mensajes.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Obtener credenciales
&lt;/h2&gt;

&lt;h3&gt;
  
  
  DeepSeek API
&lt;/h3&gt;

&lt;p&gt;Regístrate en &lt;a href="https://platform.deepseek.com" rel="noopener noreferrer"&gt;platform.deepseek.com&lt;/a&gt;, ve a la sección de API Keys y crea una nueva. Vas a necesitar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DEEPSEEK_API_KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;URL del endpoint: &lt;code&gt;https://api.deepseek.com/v1/chat/completions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Modelo recomendado: &lt;code&gt;deepseek-chat&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Bot de Telegram
&lt;/h3&gt;

&lt;p&gt;Crear un bot en Telegram es rápido y no requiere registro de empresa ni tarjetas. Para crear el bot abre Telegram, busca &lt;code&gt;@BotFather&lt;/code&gt;, envía &lt;code&gt;/newbot&lt;/code&gt; y sigue los pasos. Al finalizar te dará un token como &lt;code&gt;4839574812:AAFD39kkdpWt3ywyRZergyOLMaJhac60qc&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Para más detalle sobre la creación del bot: &lt;a href="https://dev.to/simplr_sh/telegram-bot-creation-handbook-g5g"&gt;Telegram Bot Creation Handbook&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Obtener el Chat ID
&lt;/h4&gt;

&lt;p&gt;Necesitas saber el &lt;strong&gt;Chat ID&lt;/strong&gt; (tu ID de usuario en Telegram) para que el bot sepa a quién enviarle los mensajes. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Abre Telegram y busca &lt;code&gt;@userinfobot&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Inicia la conversación con &lt;strong&gt;Start&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;El bot te responde automáticamente con tu información. Busca la línea que dice &lt;strong&gt;Id:&lt;/strong&gt;, ese número es tu Chat ID
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Id: 1234567890   &amp;lt;&amp;lt;&amp;lt; Este es tu Chat ID
First: TuNombre
Lanf: es
....
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guarda ambos valores (Bot Token y Chat ID) para usarlos en n8n.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Construir el flujo en n8n
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9dr1ilf9auorrjfmswd1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9dr1ilf9auorrjfmswd1.png" alt="Workflow" width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Si eres nuevo en n8n lo primero es crear un flujo. En cada paso te diré el nombre del nodo a agregar para que lo puedas buscar así:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyoub7ne3p2975mo32h3c.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyoub7ne3p2975mo32h3c.gif" alt="Add node" width="731" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Paso a paso:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Nodo 1 - Trigger IMAP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Agrega el nodo &lt;strong&gt;Email Trigger (IMAP)&lt;/strong&gt;, el cual será el activador de nuestro flujo y revisará los correos sin leer en nuestro buzón.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;En el campo Credential debes configurar (los valores y la forma de activación varían según el proveedor de correo):&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User&lt;/strong&gt;: tu correo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password&lt;/strong&gt;: tu contraseña o contraseña de aplicación&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host&lt;/strong&gt;: tu servidor IMAP (ej: &lt;code&gt;imap.gmail.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port&lt;/strong&gt;: &lt;code&gt;993&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure&lt;/strong&gt;: &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;En el resto de formulario ingresa:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mailbox Name:&lt;/strong&gt; nombre del buzón donde se deben extraer los correos, por defecto  &lt;code&gt;INBOX&lt;/code&gt;  (Bandeja de entrada)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action:&lt;/strong&gt; Mark as Read, con esto cada mensaje que procesemos será marcado como leído.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fltxryb9d6qcxo0nxkzyg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fltxryb9d6qcxo0nxkzyg.png" alt="Imap config" width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Por defecto, cuando publiquemos el workflow el nodo hará polling cada cierto tiempo (por defecto cada 60 segundos) para detectar correos nuevos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nodo 2 - Code (Base de conocimiento)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F07wu8omxuqcjep9i25a7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F07wu8omxuqcjep9i25a7.png" alt="Code Know" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;El "secreto" del asistente está aquí. En lugar de dejar que DeepSeek improvise, le defines un contexto claro con las respuestas que esperas.&lt;/p&gt;

&lt;p&gt;Agrega un nodo &lt;strong&gt;Code&lt;/strong&gt; (&lt;code&gt;Code in Javascript&lt;/code&gt;), cambia el modo a &lt;strong&gt;"Run Once for All Items"&lt;/strong&gt; (arriba a la derecha del editor) y conéctalo al Nodo 1 (IMAP).&lt;/p&gt;

&lt;p&gt;El código debe &lt;strong&gt;preservar los datos del correo que nos interesan&lt;/strong&gt; y agregar el campo &lt;code&gt;knowledgeBase&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;knowledgeBase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
Eres un asistente de soporte técnico de [Tu Empresa].
Responde solo si encuentras una coincidencia clara en las políticas siguientes.
Si no estás seguro, responde exactamente: NO_PUEDO_RESPONDER
POLÍTICAS:
1. Horario de atención: Lunes a viernes de 9:00 a 18:00 (GMT-5).
2. Tiempo de respuesta estimado: 24 horas hábiles.
3. Contraseñas: Nunca solicites ni compartas contraseñas por correo.
4. Reembolsos: Se procesan dentro de los primeros 30 días. El usuario debe enviar un comprobante.
5. Problemas técnicos comunes:
   - Error 403: El usuario debe limpiar caché y cookies.
   - Error 500: Reportar al equipo interno, pedir al usuario que espere 1 hora.
   - Olvido de contraseña: Enviar enlace de restablecimiento a su correo registrado.
6. Facturación: Para cambios de plan o facturación, responder que el usuario ingrese al panel de pago.
INSTRUCCIONES:
- Responde de forma amable y profesional.
- Usa el mismo idioma del correo recibido.
- Si el correo contiene más de una pregunta, responde cada punto.
- Si el correo es grosero o agresivo, responde: NO_PUEDO_RESPONDER
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;json&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;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;knowledgeBase&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;blockquote&gt;
&lt;p&gt;Puedes hacer esto tan detallado como quieras. Mientras más contexto, mejor responderá. Al usar &lt;code&gt;...item.json&lt;/code&gt; los datos del correo original se conservan y el Nodo 3 (extraer contenido) los recibe sin problema.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Este nodo se llamará, por ejemplo, &lt;code&gt;CodeKnow&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nodo 3 - Code (extraer contenido)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Usa otro nodo &lt;strong&gt;Code&lt;/strong&gt; (&lt;code&gt;Code in Javascript&lt;/code&gt;) para extraer la información necesaria de cada correo y generar el &lt;code&gt;chatInput&lt;/code&gt; con el prompt que enviaremos a nuestra IA. Cambia el modo a &lt;strong&gt;"Run for Each Item"&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textPlain&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textPlain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textHtml&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textHtml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;chatInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`From: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nAsunto: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nCuerpo: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3nnuicwf4znvj6p639o3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3nnuicwf4znvj6p639o3.png" alt="Process emails" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nodo 4 - DeepSeek Chat Model (configuración)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Agrega un nodo &lt;strong&gt;DeepSeek Chat Model&lt;/strong&gt;. Es probable que esto te genere 3 nodos automáticamente: &lt;strong&gt;When chatmessage received&lt;/strong&gt;, &lt;strong&gt;Basic LLM Chain&lt;/strong&gt; y &lt;strong&gt;DeepSeek Chat Model&lt;/strong&gt;. Quédate solo con el LLM y DeepSeek, el otro sobra. Conecta el bloque de código con el de Basic LLM, así:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvpxmt063v8xg6qz22a4u.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvpxmt063v8xg6qz22a4u.gif" alt="Setup Deepseek" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;En los bloques que nos quedan debemos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;En DeepSeek Chat Model, solo especificar las credenciales:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credential&lt;/strong&gt;: selecciona o crea una credencial &lt;strong&gt;DeepSeek account&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Key&lt;/strong&gt;: tu clave de DeepSeek&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Model&lt;/strong&gt;: &lt;code&gt;deepseek-v4-flash&lt;/code&gt;
&lt;/li&gt;

&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;En Basic LLM Chain (procesador):&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source for Prompt&lt;/strong&gt;: Define below&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt (User Message)&lt;/strong&gt;: Analyze this email and provide a short, clear response to send via Telegram.
{{ $json.chatInput }}&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat Messages&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type&lt;/strong&gt;: &lt;code&gt;System&lt;/code&gt; → &lt;strong&gt;Message&lt;/strong&gt;: &lt;code&gt;{{ $json.knowledgeBase }}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Require Specific Output Format&lt;/strong&gt;: desactivado&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Lo que hemos hecho es enviarle un prompt con nuestra intención y la data del correo que definimos en el bloque &lt;code&gt;CodeProcessEmail&lt;/code&gt; junto con un mensaje de sistema con nuestra base de conocimiento, información que definimos en el bloque &lt;code&gt;CodeKnow&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg19gjl3yox2rpepp8sn2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg19gjl3yox2rpepp8sn2.png" alt="IA config" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;¿Y el costo?&lt;/strong&gt; DeepSeek v4 cobra ~$0.14 por millón de tokens de entrada y ~$0.28 por salida. Un correo típico (~1,000 tokens) cuesta ~$0.0002. Con 1,000 correos al mes gastarías ~$0.20. En ajustes para producción te muestro cómo filtrar para evitar llamadas innecesarias.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;El nodo toma el &lt;code&gt;chatInput&lt;/code&gt; del Nodo 3 (base de conocimiento + correo) y devuelve la respuesta generada por DeepSeek.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nodo 5 - Code (mapear respuesta del asistente)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvu7noozm3dsy9i46ua19.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvu7noozm3dsy9i46ua19.png" alt="Code Normalize" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;El bloque de DeepSeek solo nos retorna la respuesta de la IA con lo cual perdemos la información completa del correo que necesitamos más adelante. Por eso agregamos este nodo &lt;strong&gt;Code&lt;/strong&gt; en modo &lt;strong&gt;"Run Once for All Items"&lt;/strong&gt; que combina la respuesta de la IA con los datos del correo original y determina si puede responder o no:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aiItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CodeProcessEmail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aiItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;emailItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aiText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;json&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;span class="nx"&gt;originalEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;aiResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;aiText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;canReply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;aiText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NO_PUEDO_RESPONDER&lt;/span&gt;&lt;span class="dl"&gt;'&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;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este bloque es clave porque:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Toma los datos originales del correo (&lt;code&gt;subject&lt;/code&gt;, &lt;code&gt;from&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Agrega &lt;code&gt;aiResponse&lt;/code&gt; con la respuesta limpia de DeepSeek&lt;/li&gt;
&lt;li&gt;Agrega &lt;code&gt;canReply&lt;/code&gt; (&lt;code&gt;true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt;) para decidir si se responde automáticamente&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Nodo 6 - IF (¿Puede responder?)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftkdf388u901qtze0770m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftkdf388u901qtze0770m.png" alt="Condition" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Teniendo a la mano la respuesta de la IA y la información del correo, agregamos un bloque &lt;code&gt;if&lt;/code&gt;. Este bloque evalúa el campo &lt;code&gt;{{ $json.canReply }}&lt;/code&gt; con la condición:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sí&lt;/strong&gt; (&lt;code&gt;true&lt;/code&gt;) → va a envío SMTP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No&lt;/strong&gt; (&lt;code&gt;false&lt;/code&gt;) → va a notificación Telegram&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Nodo 7.1 - Email (SMTP) - Responder&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwujwcxfscrswocy5fk4u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwujwcxfscrswocy5fk4u.png" alt="Smtp" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Configura el nodo &lt;code&gt;Send Email&lt;/code&gt; para enviar la respuesta automática:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credential:&lt;/strong&gt; te abrirá una ventana para registrar tus credenciales SMTP:

&lt;ul&gt;
&lt;li&gt;User&lt;/li&gt;
&lt;li&gt;Password&lt;/li&gt;
&lt;li&gt;Host&lt;/li&gt;
&lt;li&gt;Port&lt;/li&gt;
&lt;li&gt;SSL&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;From Email&lt;/strong&gt;:  le diremos que responda desde el mismo correo al que escribieron: &lt;code&gt;{{ $json.to}}&lt;/code&gt;
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;To&lt;/strong&gt;: la persona que escribió &lt;code&gt;{{ $json.from }}&lt;/code&gt;
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Subject&lt;/strong&gt;: &lt;code&gt;Re: {{ $('CodeProcessEmail').item.json.subject }}&lt;/code&gt;
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Email Format:&lt;/strong&gt; HTML&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Text&lt;/strong&gt;: &lt;code&gt;{{ $json.aiResponse }}&lt;/code&gt;
&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Nodo 7.2 - Telegram - Enviar notificación&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Para los correos que la IA no puede responder, no enviaremos una respuesta automática. En su lugar, recibirás una alerta por &lt;strong&gt;Telegram&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Sin embargo, antes debemos verificar que no vamos a enviar mensajes de más de 4.000 caracteres —el límite de Telegram—. Para ello:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agregamos un nodo &lt;strong&gt;Code&lt;/strong&gt; (JavaScript) en modo &lt;code&gt;Run Once for All Items&lt;/code&gt; con el código:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;escapeHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;amp;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;escapeHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sin asunto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aiResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;escapeHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aiResponse&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;aiResponse&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n━━━━━━━━━━━━━━━━━━━━\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ---- dividir en partes ----&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3900&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`━━━━━━━━━━━━━━━━━━━━\n&amp;lt;b&amp;gt;Recibiste los siguientes correos:&amp;lt;/b&amp;gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fullText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalParts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fullText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;fullText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;totalParts&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fullText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyuv6puaf9ak8sr2zs2l0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyuv6puaf9ak8sr2zs2l0.png" alt="Telegram code" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agregamos un nodo de tipo &lt;code&gt;HTTP Request&lt;/code&gt; para enviar cada uno de los mensajes que definimos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method:&lt;/strong&gt; POST&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://api.telegram.org/bot{TuTokenTelegram,formato:0000:xxxxxx}/sendMessage" rel="noopener noreferrer"&gt;https://api.telegram.org/bot{TuTokenTelegram,formato:0000:xxxxxx}/sendMessage&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication:&lt;/strong&gt; none&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SendBody:&lt;/strong&gt; true&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Body Content Type:&lt;/strong&gt; JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specify Body:&lt;/strong&gt; Using Fields Below

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;chat_id:&lt;/strong&gt; tu ID de conversación en Telegram&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;text:&lt;/strong&gt; {{ $json.text }}&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;parse_mode:&lt;/strong&gt; HTML&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;disable_web_page_preview:&lt;/strong&gt; TRUE&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqaoxhiqww37pwqk7f7p6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqaoxhiqww37pwqk7f7p6.png" alt="Setup telegram1" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyu1xb4l5jupjwqsbjn0v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyu1xb4l5jupjwqsbjn0v.png" alt="Setup telegram2" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Listo, con esto hemos terminado nuestro flujo :D&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx0f3h6diztsgj6i4p0qh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx0f3h6diztsgj6i4p0qh.png" alt="Workflow" width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Probar el flujo
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Antes de lanzarte a producción, haz pruebas controladas.&lt;/strong&gt; No conectes este flujo directamente a tu buzón personal o corporativo. Usa una cuenta de correo de pruebas (IMAP + SMTP) que no contenga información sensible ni confidencial. Si algo sale mal — una mala interpretación de la IA, un dominio mal configurado — podrías terminar respondiendo automáticamente a clientes reales con información incorrecta. Mejor prevenir.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Con el flujo listo, haz lo siguiente:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ejecuta manualmente&lt;/strong&gt; haciendo clic en el botón &lt;strong&gt;Execute Workflow&lt;/strong&gt; (arriba a la derecha). Esto dispara el flujo de inmediato sin esperar el polling automático.&lt;/li&gt;
&lt;li&gt;Envíate un correo de prueba con una pregunta que esté en tu base de conocimiento.&lt;/li&gt;
&lt;li&gt;Revisa que recibas la respuesta automática en la bandeja del remitente de prueba.&lt;/li&gt;
&lt;li&gt;Envíate otro correo con algo fuera de tu base de conocimiento.&lt;/li&gt;
&lt;li&gt;Deberías recibir la notificación en Telegram con el contenido del correo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cuando todo funcione como esperas, solo da clic en &lt;strong&gt;Publish&lt;/strong&gt; para activar el polling y dejarlo en producción.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Ajustes para producción
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Filtrar por remitente
&lt;/h3&gt;

&lt;p&gt;Agrega un nodo &lt;strong&gt;IF&lt;/strong&gt; después del trigger para procesar solo correos de dominios o direcciones específicas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowedDomains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tudominio.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cliente.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;allowedDomains&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rate limiting
&lt;/h3&gt;

&lt;p&gt;DeepSeek tiene límites de peticiones por minuto. Si esperas muchos correos, agrega un nodo &lt;strong&gt;Wait&lt;/strong&gt; de 1-2 segundos entre procesamiento y procesamiento, o usa una cola.&lt;/p&gt;

&lt;h3&gt;
  
  
  Historial de conversación
&lt;/h3&gt;

&lt;p&gt;Si quieres que DeepSeek recuerde conversaciones anteriores, puedes almacenar el historial en una base de datos (PostgreSQL, Redis) y pasarlo como contexto adicional en cada iteración.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manejo de adjuntos
&lt;/h3&gt;

&lt;p&gt;DeepSeek no procesa archivos directamente. Si recibes correos con adjuntos, puedes usar un nodo &lt;strong&gt;Code&lt;/strong&gt; para extraer el texto de PDFs o imágenes (con OCR) y pasarlo como contexto.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Tienes un asistente de correo automatizado que:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lee correos vía IMAP sin perder ninguno&lt;/li&gt;
&lt;li&gt;Los analiza con DeepSeek contra tu propia base de conocimiento&lt;/li&gt;
&lt;li&gt;Responde automáticamente cuando puede&lt;/li&gt;
&lt;li&gt;Te notifica por Telegram cuando necesita tu intervención&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esto no reemplaza un equipo de soporte, pero sí absorbe el volumen de preguntas repetitivas y libera tiempo para lo que realmente importa.&lt;/p&gt;

&lt;p&gt;Si quieres documentar visualmente este flujo o diagramas de infraestructura más complejos, &lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;Savnet&lt;/a&gt; te ayuda a diseñarlos. Y si necesitas validar este tipo de automatizaciones de forma continua, &lt;a href="https://savfalconeye.com" rel="noopener noreferrer"&gt;SavFalconEye&lt;/a&gt; te permite mantener la confianza en cada cambio.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;¿Te animas a probarlo? Cuéntame en los comentarios cómo te fue.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>spanish</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Install n8n on an Ubuntu Server (Production-Ready)</title>
      <dc:creator>Oscar Ricardo Sánche Gutierréz</dc:creator>
      <pubDate>Wed, 29 Apr 2026 19:41:34 +0000</pubDate>
      <link>https://dev.to/oscar_ricardosncheguti/how-to-install-n8n-on-an-ubuntu-server-production-ready-djb</link>
      <guid>https://dev.to/oscar_ricardosncheguti/how-to-install-n8n-on-an-ubuntu-server-production-ready-djb</guid>
      <description>&lt;p&gt;&lt;strong&gt;n8n&lt;/strong&gt; is an open-source workflow automation platform. It lets you connect services, APIs, and databases through a visual editor without writing hundreds of lines of code — though you can still use JavaScript or Python when custom logic is needed.&lt;/p&gt;

&lt;p&gt;In this guide, you'll deploy n8n on an Ubuntu server using Docker Compose, with PostgreSQL as the database, Redis as the queue manager, dedicated workers for background execution, Caddy as a reverse proxy with automatic SSL, and best practices for a production environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fap6d26ytfjd815re8k0b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fap6d26ytfjd815re8k0b.png" alt="Architecture" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Diagram made with &lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;https://savnet.co&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;ul&gt;
&lt;li&gt;A server running &lt;strong&gt;Ubuntu 22.04 or 24.04&lt;/strong&gt; (1 vCPU, 2 GB RAM minimum; 2 vCPU, 4 GB RAM recommended)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root&lt;/strong&gt; access for initial setup&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;domain or subdomain&lt;/strong&gt; pointing (A record) to your server's IP&lt;/li&gt;
&lt;li&gt;Ports &lt;strong&gt;80&lt;/strong&gt; and &lt;strong&gt;443&lt;/strong&gt; open in the firewall&lt;/li&gt;
&lt;li&gt;Basic command-line knowledge&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't have a server yet?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If you're looking for affordability and good performance, DigitalOcean is a solid choice. You can get started with this referral link:&lt;br&gt;&lt;br&gt;
&lt;a href="https://m.do.co/c/2c579acd7121" rel="noopener noreferrer"&gt;https://m.do.co/c/2c579acd7121&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. Initial Connection and System Update
&lt;/h2&gt;

&lt;p&gt;SSH in as root and update the system packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@your-server
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;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Create a Dedicated Admin User
&lt;/h2&gt;

&lt;p&gt;We'll create an &lt;code&gt;n8n&lt;/code&gt; user with sudo privileges. From this point on, all work will be done with this user, not root.&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;# Create the user&lt;/span&gt;
adduser &lt;span class="nt"&gt;--gecos&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; n8n

&lt;span class="c"&gt;# Add to sudo group&lt;/span&gt;
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;n8n

&lt;span class="c"&gt;# Verify&lt;/span&gt;
&lt;span class="nb"&gt;id &lt;/span&gt;n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before logging out, copy your public key from root to the new user so you can connect directly as &lt;code&gt;n8n&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;# Copy authorized SSH key from root to n8n&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/n8n/.ssh
&lt;span class="nb"&gt;cp&lt;/span&gt; ~/.ssh/authorized_keys /home/n8n/.ssh/authorized_keys
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; n8n:n8n /home/n8n/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 /home/n8n/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /home/n8n/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now log out and reconnect as &lt;code&gt;n8n&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;exit
&lt;/span&gt;ssh n8n@your-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why a dedicated user?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Running services directly as root is a bad security practice. If someone compromises the n8n process, they'd have full access to the server. A dedicated user limits the potential damage.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. Install Docker and Docker Compose
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Required packages&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ca-certificates curl gnupg

&lt;span class="c"&gt;# Add Docker's official GPG key&lt;/span&gt;
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/ubuntu/gpg | &lt;span class="nb"&gt;sudo &lt;/span&gt;gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.gpg
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.gpg

&lt;span class="c"&gt;# Add repository&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; stable"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/docker.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

&lt;span class="c"&gt;# Install Docker Engine and Compose plugin&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

&lt;span class="c"&gt;# Add your user to the docker group (to avoid using sudo with every command)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt;

&lt;span class="c"&gt;# Verify&lt;/span&gt;
docker &lt;span class="nt"&gt;--version&lt;/span&gt;
docker compose version

&lt;span class="c"&gt;# Reboot to apply group changes&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;init 6
ssh n8n@your-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Create Directory Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /opt/n8n/&lt;span class="o"&gt;{&lt;/span&gt;data/postgres,data/redis,data/n8n,data/caddy,backups&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nv"&gt;$USER&lt;/span&gt;:&lt;span class="nv"&gt;$USER&lt;/span&gt; /opt/n8n/data/&lt;span class="o"&gt;{&lt;/span&gt;postgres,redis,data/caddy,backups&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# The data/n8n directory must belong to UID 1000 (the 'node' user inside the container)&lt;/span&gt;
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 1000:1000 /opt/n8n/data/n8n
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Configure Environment Variables
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file to store sensitive configuration:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tips before you start:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Make sure your domain points to the server.&lt;/strong&gt; Before proceeding, go to your DNS provider and add an A record pointing to your server's IP. Caddy needs to resolve the domain to obtain the SSL certificate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate a secure PostgreSQL password&lt;/strong&gt; and an &lt;strong&gt;encryption key&lt;/strong&gt; for n8n with these commands:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 24   &lt;span class="c"&gt;# For POSTGRES_PASSWORD&lt;/span&gt;
  openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 20      &lt;span class="c"&gt;# For N8N_ENCRYPTION_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the values for the &lt;code&gt;.env&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Domain
N8N_DOMAIN=n8n.yourdomain.com

# PostgreSQL
POSTGRES_USER=n8n
POSTGRES_PASSWORD=your_secure_postgres_password
POSTGRES_DB=n8n

# n8n
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n
DB_POSTGRESDB_PASSWORD=your_secure_postgres_password
N8N_ENCRYPTION_KEY=your_encryption_key
N8N_HOST=${N8N_DOMAIN}
N8N_PROTOCOL=https
WEBHOOK_URL=https://${N8N_DOMAIN}/
N8N_PORT=5678

# Queue mode (Redis)
EXECUTIONS_MODE=queue
QUEUE_BULL_REDIS_HOST=redis
QUEUE_BULL_REDIS_PORT=6379
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Protect the file:&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 /opt/n8n/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Create the docker-compose.yml File
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano /opt/n8n/docker-compose.yml
&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;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n-postgres&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;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=${POSTGRES_DB}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/postgres:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_DB}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;networks&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-network&lt;/span&gt;

  &lt;span class="na"&gt;redis&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;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n-redis&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;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;./data/redis:/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"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;networks&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-network&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:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n-app&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;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_TYPE=${DB_TYPE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_HOST=${DB_POSTGRESDB_HOST}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PORT=${DB_POSTGRESDB_PORT}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_DATABASE=${DB_POSTGRESDB_DATABASE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_USER=${DB_POSTGRESDB_USER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PASSWORD=${DB_POSTGRESDB_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_HOST=${N8N_HOST}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PROTOCOL=${N8N_PROTOCOL}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEBHOOK_URL=${WEBHOOK_URL}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PORT=${N8N_PORT}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_METRICS=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EXECUTIONS_DATA_PRUNE=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EXECUTIONS_DATA_MAX_AGE=168&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EXECUTIONS_MODE=${EXECUTIONS_MODE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QUEUE_BULL_REDIS_HOST=${QUEUE_BULL_REDIS_HOST}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QUEUE_BULL_REDIS_PORT=${QUEUE_BULL_REDIS_PORT}&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;./data/n8n:/home/node/.n8n&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;networks&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-network&lt;/span&gt;

  &lt;span class="na"&gt;n8n-worker&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:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n-worker&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;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&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;EXECUTIONS_MODE=${EXECUTIONS_MODE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QUEUE_BULL_REDIS_HOST=${QUEUE_BULL_REDIS_HOST}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QUEUE_BULL_REDIS_PORT=${QUEUE_BULL_REDIS_PORT}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_TYPE=${DB_TYPE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_HOST=${DB_POSTGRESDB_HOST}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PORT=${DB_POSTGRESDB_PORT}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_DATABASE=${DB_POSTGRESDB_DATABASE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_USER=${DB_POSTGRESDB_USER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PASSWORD=${DB_POSTGRESDB_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
    &lt;span class="na"&gt;networks&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-network&lt;/span&gt;

  &lt;span class="na"&gt;caddy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caddy:2-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n-caddy&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443/udp"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./Caddyfile:/etc/caddy/Caddyfile&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/caddy:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/caddy:/config&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;n8n&lt;/span&gt;
    &lt;span class="na"&gt;networks&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-network&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n-network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why use queue mode from the start?
&lt;/h3&gt;

&lt;p&gt;n8n can run workflows in two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct mode (default)&lt;/strong&gt;: the main container handles everything. If a workflow takes too long, it blocks subsequent executions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue mode&lt;/strong&gt;: jobs are queued in Redis and processed by independent workers. You can have multiple workers, scale horizontally, and heavy executions won't affect the main container.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the &lt;code&gt;docker-compose.yml&lt;/code&gt; above, &lt;strong&gt;queue mode&lt;/strong&gt; is active from the first &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Create the Caddyfile
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano /opt/n8n/Caddyfile
&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;n8n.yourdomain.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;reverse_proxy&lt;/span&gt; &lt;span class="nf"&gt;n8n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5678&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caddy will automatically obtain an SSL certificate from Let's Encrypt and renew it without manual intervention.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Configure the Firewall (UFW)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default deny incoming
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default allow outgoing
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow ssh
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nb"&gt;enable
sudo &lt;/span&gt;ufw status verbose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  9. Start the Stack
&lt;/h2&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 up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk4qujzsoo055a0nwdznl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk4qujzsoo055a0nwdznl.png" alt="docker compose up" width="800" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Verify all services are running:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;You should see something like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjs1ywb9qaay1mbv767mw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjs1ywb9qaay1mbv767mw.png" alt="docker compose ps" width="800" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check the worker logs to confirm it's picking up jobs:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp7occsdrzmbqil80noe7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp7occsdrzmbqil80noe7.png" alt="docker compose logs" width="800" height="185"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Access n8n and Initial Setup
&lt;/h2&gt;

&lt;p&gt;Open your browser at &lt;code&gt;https://n8n.yourdomain.com&lt;/code&gt;. You'll see the registration screen. Create your admin account:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Email&lt;/strong&gt;: your email&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full name&lt;/strong&gt;: your name&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password&lt;/strong&gt;: a strong password&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0yy6o43wzo0a90ji4x55.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0yy6o43wzo0a90ji4x55.png" alt="n8n dashboard" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You're ready to start creating workflows. All workflows will run through the worker.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Scale Workers
&lt;/h2&gt;

&lt;p&gt;If your workflows grow, scaling is as simple as adding more replicas of the &lt;code&gt;n8n-worker&lt;/code&gt; service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--scale&lt;/span&gt; n8n-worker&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each additional worker picks up jobs from the same Redis queue. No other changes needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Production Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use a fixed tag&lt;/strong&gt;: Instead of &lt;code&gt;n8nio/n8n:latest&lt;/code&gt;, use a specific version like &lt;code&gt;n8nio/n8n:1.80.3&lt;/code&gt; to avoid surprises from updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt;: n8n exposes Prometheus metrics at &lt;code&gt;/metrics&lt;/code&gt;. You can integrate them with a monitoring stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regular backups&lt;/strong&gt;: Schedule a cron job to back up the &lt;code&gt;data/postgres&lt;/code&gt;, &lt;code&gt;data/redis&lt;/code&gt;, &lt;code&gt;data/n8n&lt;/code&gt;, and &lt;code&gt;data/caddy&lt;/code&gt; directories. Since they're bind mounts, you can copy them directly with &lt;code&gt;rsync&lt;/code&gt; or &lt;code&gt;tar&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: Don't expose port 5678 directly. Caddy handles the proxy. If you need local access, use a VPN or SSH tunnels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution pruning&lt;/strong&gt;: The &lt;code&gt;EXECUTIONS_DATA_PRUNE=true&lt;/code&gt; and &lt;code&gt;EXECUTIONS_DATA_MAX_AGE=168&lt;/code&gt; variables automatically delete old executions after 7 days.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You now have n8n running in production on Ubuntu with PostgreSQL as a persistent database, Redis as the queue manager, dedicated workers, automatic SSL with Caddy, and a structure ready to scale. All running under a dedicated system user — not root.&lt;/p&gt;

&lt;p&gt;This queue-mode setup is the same one we use in real-world environments to automate client processes that require high availability and predictable performance, from notifications and CRMs to complex integrations with external APIs.&lt;/p&gt;

&lt;p&gt;If you work with networks or visual infrastructure documentation, you can use &lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;Savnet&lt;/a&gt; to design diagrams for your deployed workflows. And if you need to continuously validate or test automations, &lt;a href="https://savfalconeye.com" rel="noopener noreferrer"&gt;SavFalconEye&lt;/a&gt; helps maintain confidence in every deployment.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Any questions? Drop them in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>docker</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cómo instalar n8n en un servidor Ubuntu (listo para produción)</title>
      <dc:creator>Oscar Ricardo Sánche Gutierréz</dc:creator>
      <pubDate>Wed, 29 Apr 2026 19:37:49 +0000</pubDate>
      <link>https://dev.to/oscar_ricardosncheguti/como-instalar-n8n-en-un-servidor-ubuntu-produccion-lista-2kf2</link>
      <guid>https://dev.to/oscar_ricardosncheguti/como-instalar-n8n-en-un-servidor-ubuntu-produccion-lista-2kf2</guid>
      <description>&lt;p&gt;&lt;strong&gt;n8n&lt;/strong&gt; es una plataforma de automatización de flujos de trabajo de código abierto. Permite conectar servicios, APIs y bases de datos mediante un editor visual sin necesidad de escribir cientos de líneas de código, aunque sí permite ejecutar JavaScript o Python cuando se necesita lógica personalizada.&lt;/p&gt;

&lt;p&gt;En esta guía vas a desplegar n8n en un servidor Ubuntu usando Docker Compose, con PostgreSQL como base de datos, Redis como gestor de colas, workers dedicados para ejecución en segundo plano, Caddy como proxy inverso con SSL automático, y buenas prácticas para un entorno de producción.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2jcski3per0yk3fxvljz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2jcski3per0yk3fxvljz.png" alt="Architecture" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Diagrama realizado con &lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;https://savnet.co&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Prerrequisitos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Un servidor con &lt;strong&gt;Ubuntu 22.04 o 24.04&lt;/strong&gt; (1 vCPU, 2 GB RAM mínimo; 2 vCPU, 4 GB RAM recomendado)&lt;/li&gt;
&lt;li&gt;Acceso &lt;strong&gt;root&lt;/strong&gt; para la configuración inicial&lt;/li&gt;
&lt;li&gt;Un &lt;strong&gt;dominio o subdominio&lt;/strong&gt; apuntando (registro A) a la IP del servidor&lt;/li&gt;
&lt;li&gt;Puertos &lt;strong&gt;80&lt;/strong&gt; y &lt;strong&gt;443&lt;/strong&gt; abiertos en el firewall&lt;/li&gt;
&lt;li&gt;Conocimientos básicos de línea de comandos&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;¿Aún no tienes servidor?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Si buscas economía y buen rendimiento, DigitalOcean es una opción sólida. Puedes empezar con este enlace de referido:&lt;br&gt;&lt;br&gt;
&lt;a href="https://m.do.co/c/2c579acd7121" rel="noopener noreferrer"&gt;https://m.do.co/c/2c579acd7121&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. Conexión inicial y actualización del sistema
&lt;/h2&gt;

&lt;p&gt;Conéctate vía SSH como root y actualiza los paquetes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@tu-servidor
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;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Crear usuario administrador dedicado
&lt;/h2&gt;

&lt;p&gt;Vamos a crear un usuario &lt;code&gt;n8n&lt;/code&gt; con permisos sudo. A partir de este punto, todo el trabajo se hará con este usuario, no con root.&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;# Crear el usuario&lt;/span&gt;
adduser &lt;span class="nt"&gt;--gecos&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; n8n

&lt;span class="c"&gt;# Agregarlo al grupo sudo&lt;/span&gt;
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;n8n

&lt;span class="c"&gt;# Verificar&lt;/span&gt;
&lt;span class="nb"&gt;id &lt;/span&gt;n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Antes de cerrar la sesión, copia tu clave pública de root al nuevo usuario para poder conectarte directamente como &lt;code&gt;n8n&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;# Copiar la llave autorizada para conexión SSH de root a n8n&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/n8n/.ssh
&lt;span class="nb"&gt;cp&lt;/span&gt; ~/.ssh/authorized_keys /home/n8n/.ssh/authorized_keys
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; n8n:n8n /home/n8n/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 /home/n8n/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /home/n8n/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahora cierra sesión y reconéctate como &lt;code&gt;n8n&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;exit
&lt;/span&gt;ssh n8n@tu-servidor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;¿Por qué un usuario dedicado?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Porque ejecutar servicios directamente con root es una mala práctica de seguridad. Si alguien compromete el proceso de n8n, tendría acceso total al servidor. Con un usuario dedicado, el daño queda acotado.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. Instalar Docker y Docker Compose
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Paquetes necesarios&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ca-certificates curl gnupg

&lt;span class="c"&gt;# Agregar clave GPG oficial de Docker&lt;/span&gt;
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/ubuntu/gpg | &lt;span class="nb"&gt;sudo &lt;/span&gt;gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.gpg
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.gpg

&lt;span class="c"&gt;# Agregar repositorio&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; stable"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/docker.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

&lt;span class="c"&gt;# Instalar Docker Engine y Compose plugin&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

&lt;span class="c"&gt;# Agregar tu usuario al grupo docker (para no usar sudo con cada comando)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt;

&lt;span class="c"&gt;# Verificar&lt;/span&gt;
docker &lt;span class="nt"&gt;--version&lt;/span&gt;
docker compose version

&lt;span class="c"&gt;# Reingresar para hacer efectivos los cambios de grupo&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;init 6
ssh n8n@tu-servidor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Crear estructura de directorios
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /opt/n8n/&lt;span class="o"&gt;{&lt;/span&gt;data/postgres,data/redis,data/n8n,data/caddy,backups&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nv"&gt;$USER&lt;/span&gt;:&lt;span class="nv"&gt;$USER&lt;/span&gt; /opt/n8n/data/&lt;span class="o"&gt;{&lt;/span&gt;postgres,redis,data/caddy,backups&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# El directorio data/n8n debe pertenecer al UID 1000 (usuario 'node' dentro del contenedor)&lt;/span&gt;
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 1000:1000 /opt/n8n/data/n8n
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/n8n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Configurar variables de entorno
&lt;/h2&gt;

&lt;p&gt;Crea un archivo &lt;code&gt;.env&lt;/code&gt; para almacenar la configuración sensible:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tips antes de empezar:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Asegúrate de que tu dominio apunte al servidor.&lt;/strong&gt; Antes de continuar, ve a tu proveedor de DNS y agrega un registro A con la IP de tu servidor. Caddy necesita resolver el dominio para obtener el certificado SSL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Genera una contraseña segura para PostgreSQL&lt;/strong&gt; y una &lt;strong&gt;encryption key&lt;/strong&gt; para n8n con estos comandos:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 24   &lt;span class="c"&gt;# Para POSTGRES_PASSWORD&lt;/span&gt;
  openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 20      &lt;span class="c"&gt;# Para N8N_ENCRYPTION_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guarda los valores para el &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 shell"&gt;&lt;code&gt;nano /opt/n8n/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contenido:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Dominio
N8N_DOMAIN=n8n.tudominio.com

# PostgreSQL
POSTGRES_USER=n8n
POSTGRES_PASSWORD=password_seguro_postgres
POSTGRES_DB=n8n

# n8n
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n
DB_POSTGRESDB_PASSWORD=password_seguro_postgres
N8N_ENCRYPTION_KEY=encryption_key_seguro
N8N_HOST=${N8N_DOMAIN}
N8N_PROTOCOL=https
WEBHOOK_URL=https://${N8N_DOMAIN}/
N8N_PORT=5678

# Queue mode (Redis)
EXECUTIONS_MODE=queue
QUEUE_BULL_REDIS_HOST=redis
QUEUE_BULL_REDIS_PORT=6379
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Protege el archivo:&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 /opt/n8n/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Crear el archivo docker-compose.yml
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano /opt/n8n/docker-compose.yml
&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;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n-postgres&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;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=${POSTGRES_DB}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/postgres:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_DB}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;networks&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-network&lt;/span&gt;

  &lt;span class="na"&gt;redis&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;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n-redis&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;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;./data/redis:/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"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;networks&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-network&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:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n-app&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;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_TYPE=${DB_TYPE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_HOST=${DB_POSTGRESDB_HOST}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PORT=${DB_POSTGRESDB_PORT}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_DATABASE=${DB_POSTGRESDB_DATABASE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_USER=${DB_POSTGRESDB_USER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PASSWORD=${DB_POSTGRESDB_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_HOST=${N8N_HOST}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PROTOCOL=${N8N_PROTOCOL}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEBHOOK_URL=${WEBHOOK_URL}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_PORT=${N8N_PORT}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;N8N_METRICS=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EXECUTIONS_DATA_PRUNE=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EXECUTIONS_DATA_MAX_AGE=168&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EXECUTIONS_MODE=${EXECUTIONS_MODE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QUEUE_BULL_REDIS_HOST=${QUEUE_BULL_REDIS_HOST}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QUEUE_BULL_REDIS_PORT=${QUEUE_BULL_REDIS_PORT}&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;./data/n8n:/home/node/.n8n&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;networks&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-network&lt;/span&gt;

  &lt;span class="na"&gt;n8n-worker&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:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n-worker&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;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&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;EXECUTIONS_MODE=${EXECUTIONS_MODE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QUEUE_BULL_REDIS_HOST=${QUEUE_BULL_REDIS_HOST}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QUEUE_BULL_REDIS_PORT=${QUEUE_BULL_REDIS_PORT}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_TYPE=${DB_TYPE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_HOST=${DB_POSTGRESDB_HOST}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PORT=${DB_POSTGRESDB_PORT}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_DATABASE=${DB_POSTGRESDB_DATABASE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_USER=${DB_POSTGRESDB_USER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_POSTGRESDB_PASSWORD=${DB_POSTGRESDB_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
    &lt;span class="na"&gt;networks&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-network&lt;/span&gt;

  &lt;span class="na"&gt;caddy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caddy:2-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n-caddy&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443/udp"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./Caddyfile:/etc/caddy/Caddyfile&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/caddy:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/caddy:/config&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;n8n&lt;/span&gt;
    &lt;span class="na"&gt;networks&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-network&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n-network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ¿Por qué usar queue mode desde el inicio?
&lt;/h3&gt;

&lt;p&gt;n8n puede ejecutar workflows de dos formas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Modo directo (default)&lt;/strong&gt;: el contenedor principal ejecuta todo. Si un workflow tarda mucho, bloquea las ejecuciones siguientes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modo queue&lt;/strong&gt;: los trabajos se encolan en Redis y los procesan workers independientes. Puedes tener varios workers, escalarlos horizontalmente y las ejecuciones pesadas no afectan al contenedor principal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Con el &lt;code&gt;docker-compose.yml&lt;/code&gt; de arriba, ya tienes el &lt;strong&gt;modo queue&lt;/strong&gt; activo desde el primer &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Crear el Caddyfile
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano /opt/n8n/Caddyfile
&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;n8n.tudominio.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;reverse_proxy&lt;/span&gt; &lt;span class="nf"&gt;n8n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5678&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caddy obtendrá automáticamente un certificado SSL de Let's Encrypt y lo renovará sin intervención manual.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Configurar el firewall (UFW)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default deny incoming
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default allow outgoing
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow ssh
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nb"&gt;enable
sudo &lt;/span&gt;ufw status verbose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  9. Iniciar el stack
&lt;/h2&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 up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fft797n4jw64ewu3qygpv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fft797n4jw64ewu3qygpv.png" alt="docker compose up" width="800" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Verifica que todos los servicios estén funcionando:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Deberías ver algo como:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy3dbbnhb0g0o9emp34yl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy3dbbnhb0g0o9emp34yl.png" alt="docker compose ps" width="800" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Revisa los logs del worker para confirmar que está recibiendo trabajos:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F10eao0spef7vkhriy5nr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F10eao0spef7vkhriy5nr.png" alt="docker compose logs" width="800" height="185"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Acceder a n8n y configuración inicial
&lt;/h2&gt;

&lt;p&gt;Abre tu navegador en &lt;code&gt;https://n8n.tudominio.com&lt;/code&gt;. Verás la pantalla de registro. Crea tu cuenta de administrador:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Email&lt;/strong&gt;: tu correo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nombre&lt;/strong&gt;: tu nombre&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contraseña&lt;/strong&gt;: una contraseña segura&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsycmilrj8gvbl6otw4ra.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsycmilrj8gvbl6otw4ra.png" alt="N8n Dashboard" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ya puedes empezar a crear flujos de trabajo. Todos los workflows se ejecutarán a través del worker.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Escalar workers
&lt;/h2&gt;

&lt;p&gt;Si tus workflows crecen, escalar es tan simple como agregar más réplicas del servicio &lt;code&gt;n8n-worker&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--scale&lt;/span&gt; n8n-worker&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada worker adicional toma trabajos de la misma cola en Redis. No necesitas cambiar nada más.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Buenas prácticas en producción
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Usa etiqueta fija&lt;/strong&gt;: En lugar de &lt;code&gt;n8nio/n8n:latest&lt;/code&gt;, usa una versión específica como &lt;code&gt;n8nio/n8n:1.80.3&lt;/code&gt; para evitar sorpresas con actualizaciones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoreo&lt;/strong&gt;: n8n expone métricas Prometheus en &lt;code&gt;/metrics&lt;/code&gt;. Puedes integrarlas con un stack de monitoreo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backups periódicos&lt;/strong&gt;: Programa un cron para respaldar los directorios &lt;code&gt;data/postgres&lt;/code&gt;, &lt;code&gt;data/redis&lt;/code&gt;, &lt;code&gt;data/n8n&lt;/code&gt; y &lt;code&gt;data/caddy&lt;/code&gt;. Al estar en bind mounts, puedes copiarlos directamente con &lt;code&gt;rsync&lt;/code&gt; o &lt;code&gt;tar&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seguridad&lt;/strong&gt;: No expongas el puerto 5678 directamente. Caddy ya se encarga del proxy. Si necesitas acceso local, usa una VPN o túneles SSH.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limpieza de ejecuciones&lt;/strong&gt;: Las variables &lt;code&gt;EXECUTIONS_DATA_PRUNE=true&lt;/code&gt; y &lt;code&gt;EXECUTIONS_DATA_MAX_AGE=168&lt;/code&gt; eliminan automáticamente ejecuciones antiguas después de 7 días.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Tienes n8n corriendo en producción sobre Ubuntu con PostgreSQL como base de datos persistente, Redis como gestor de colas, workers dedicados, SSL automático con Caddy y una estructura lista para escalar. Y todo ejecutándose bajo un usuario del sistema dedicado, no como root.&lt;/p&gt;

&lt;p&gt;Este setup con modo queue es el mismo que usamos en entornos reales para automatizar procesos de clientes que requieren alta disponibilidad y rendimiento predecible, desde notificaciones y CRMs hasta integraciones complejas con APIs externas.&lt;/p&gt;

&lt;p&gt;Si trabajas con redes o documentación visual de infraestructura, puedes usar &lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;Savnet&lt;/a&gt; para diseñar los diagramas de tus flujos desplegados. Y si necesitas validar o testear automatizaciones de forma continua, &lt;a href="https://savfalconeye.com" rel="noopener noreferrer"&gt;SavFalconEye&lt;/a&gt; te ayuda a mantener la confianza en cada despliegue.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;¿Te quedó alguna duda? Déjala en los comentarios.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>docker</category>
      <category>spanish</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Secure a System with mTLS Certificates (Mutual TLS)</title>
      <dc:creator>Oscar Ricardo Sánche Gutierréz</dc:creator>
      <pubDate>Thu, 23 Apr 2026 19:12:06 +0000</pubDate>
      <link>https://dev.to/oscar_ricardosncheguti/how-to-secure-a-system-with-mtls-certificates-mutual-tls-3i39</link>
      <guid>https://dev.to/oscar_ricardosncheguti/how-to-secure-a-system-with-mtls-certificates-mutual-tls-3i39</guid>
      <description>&lt;p&gt;In the world of modern application security, &lt;strong&gt;mTLS (Mutual TLS)&lt;/strong&gt; has become a fundamental standard for protecting communications between services. Unlike traditional TLS where only the server authenticates, mTLS requires &lt;strong&gt;both parties&lt;/strong&gt; (client and server) to present valid certificates, creating a much more robust security layer.&lt;/p&gt;

&lt;p&gt;In this article, I'll show you how to implement mTLS step by step, from certificate generation to configuration in real-world applications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk81u77ng50omdbsfxaew.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk81u77ng50omdbsfxaew.gif" alt="Architecture savnet.co" width="700" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Diagram created with &lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;https://savnet.co&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is mTLS and Why Does It Matter?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;mTLS (Mutual TLS)&lt;/strong&gt; is an extension of the standard TLS protocol that adds mutual authentication. While in traditional TLS only the server presents a certificate, in mTLS both the client and the server must authenticate each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Benefits:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Strong authentication&lt;/strong&gt;: Both ends of the communication are verified&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MITM attack prevention&lt;/strong&gt;: Makes man-in-the-middle attacks significantly harder&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Trust&lt;/strong&gt;: Aligns perfectly with Zero Trust architectures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microservices security&lt;/strong&gt;: Ideal for service-to-service communications&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Common Use Cases:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Internal APIs between microservices&lt;/li&gt;
&lt;li&gt;Service-to-service communication in Kubernetes&lt;/li&gt;
&lt;li&gt;Payment system connections&lt;/li&gt;
&lt;li&gt;Secure B2B communications&lt;/li&gt;
&lt;li&gt;Sensitive API access&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Set Up the Environment
&lt;/h2&gt;

&lt;p&gt;For this tutorial, we'll work on an &lt;strong&gt;Ubuntu server&lt;/strong&gt; that will act as our certificate generator and Certificate Authority (CA). We only need a small server with &lt;strong&gt;512MB RAM and 1 CPU&lt;/strong&gt;, enough to generate and manage certificates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz5t6eevvmvyxovubsahf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz5t6eevvmvyxovubsahf.png" alt="New server" width="800" height="546"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We'll need some basic tools:&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;# Update system&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="c"&gt;# Install OpenSSL (if not already installed)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; openssl curl wget

&lt;span class="c"&gt;# Verify OpenSSL version&lt;/span&gt;
openssl version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6l2pfa6l6g9yvk3n9ok5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6l2pfa6l6g9yvk3n9ok5.png" alt="Version OpenSsl" width="800" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For other operating systems, make sure OpenSSL is installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create the Certificate Authority (CA)
&lt;/h2&gt;

&lt;p&gt;The CA is the entity that will issue and sign all our certificates. Let's create a private CA.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 Create working directory
&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; ~/mtls-ca
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/mtls-ca
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ca/private ca/certs ca/newcerts
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; server/private server/certs
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; client/private client/certs

&lt;span class="c"&gt;# Set secure permissions&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 ca/private server/private client/private
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2.2 Generate CA private key and certificate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate CA private key (password-protected)&lt;/span&gt;
openssl genrsa &lt;span class="nt"&gt;-aes256&lt;/span&gt; &lt;span class="nt"&gt;-out&lt;/span&gt; ca/private/ca.key 4096

&lt;span class="c"&gt;# Generate self-signed CA certificate&lt;/span&gt;
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 7300 &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-key&lt;/span&gt; ca/private/ca.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; ca/certs/ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/C=US/ST=California/L=San Francisco/O=MyCompany/OU=IT/CN=MyCA-Root/emailAddress=admin@mycompany.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Explanation of the -subj parameter&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/C=US&lt;/code&gt;: Country code (2 letters)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/ST=California&lt;/code&gt;: State or province&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/L=San Francisco&lt;/code&gt;: Locality or city&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/O=MyCompany&lt;/code&gt;: Your organization name&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/OU=IT&lt;/code&gt;: Organizational unit&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/CN=MyCA-Root&lt;/code&gt;: Common name of the CA&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/emailAddress=admin@mycompany.com&lt;/code&gt;: Contact email&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Customize these values&lt;/strong&gt; with your actual information.&lt;/p&gt;

&lt;p&gt;During the process you'll be prompted for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A password for the private key (store it in a safe place)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.3 Verify the CA certificate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl x509 &lt;span class="nt"&gt;-in&lt;/span&gt; ca/certs/ca.crt &lt;span class="nt"&gt;-text&lt;/span&gt; &lt;span class="nt"&gt;-noout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see detailed information about your CA, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Issuer: Your own CA&lt;/li&gt;
&lt;li&gt;Validity: 20 years (7300 days)&lt;/li&gt;
&lt;li&gt;Key usage: Certifying other keys&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Generate Server Certificates
&lt;/h2&gt;

&lt;p&gt;Now let's create certificates for our server.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 Create server private key and CSR
&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; ~/mtls-ca

&lt;span class="c"&gt;# Generate server private key&lt;/span&gt;
openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; server/private/server.key 2048

&lt;span class="c"&gt;# Create Certificate Signing Request (CSR)&lt;/span&gt;
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-key&lt;/span&gt; server/private/server.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; server/server.csr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/C=US/ST=California/L=San Francisco/O=MyCompany/OU=Web/CN={YOUR DOMAIN OR IP WHERE YOUR WEB APP RUNS}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Customize the -subj parameter&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/C=US&lt;/code&gt;: Your country code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/ST=California&lt;/code&gt;: Your state or province&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/L=San Francisco&lt;/code&gt;: Your locality or city&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/O=MyCompany&lt;/code&gt;: Your organization&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/OU=Web&lt;/code&gt;: Organizational unit (e.g., Web, API, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/CN=api.mydomain.com&lt;/code&gt;: &lt;strong&gt;IMPORTANT&lt;/strong&gt;: Your server's domain or IP&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.2 Sign the server certificate with the CA
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Sign the server certificate&lt;/span&gt;
openssl x509 &lt;span class="nt"&gt;-req&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 365 &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-in&lt;/span&gt; server/server.csr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-CA&lt;/span&gt; ca/certs/ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-CAkey&lt;/span&gt; ca/private/ca.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-CAcreateserial&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; server/certs/server.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-extfile&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"basicConstraints=CA:FALSE&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;keyUsage=digitalSignature,keyEncipherment&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;extendedKeyUsage=serverAuth"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Verify the signed certificate&lt;/span&gt;
openssl x509 &lt;span class="nt"&gt;-in&lt;/span&gt; server/certs/server.crt &lt;span class="nt"&gt;-text&lt;/span&gt; &lt;span class="nt"&gt;-noout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F735blwhhzixttonlojc8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F735blwhhzixttonlojc8.png" alt="Verificate server certificate" width="800" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 Create certificate chain file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create certificate chain for the server&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;server/certs/server.crt ca/certs/ca.crt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; server/certs/server-chain.crt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Generate Client Certificates
&lt;/h2&gt;

&lt;p&gt;Let's repeat the process for the client.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Create client private key and CSR
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate client private key&lt;/span&gt;
openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; client/private/client.key 2048

&lt;span class="c"&gt;# Create CSR for the client&lt;/span&gt;
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-key&lt;/span&gt; client/private/client.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; client/client.csr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/C=US/ST=California/L=San Francisco/O=MyCompany/OU=Clients/CN=app-client-01"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Customize the -subj parameter&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/C=US&lt;/code&gt;: Your country code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/ST=California&lt;/code&gt;: Your state or province&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/L=San Francisco&lt;/code&gt;: Your locality or city&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/O=MyCompany&lt;/code&gt;: Your organization&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/OU=Clients&lt;/code&gt;: Organizational unit (e.g., Clients, Apps, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/CN=app-client-01&lt;/code&gt;: &lt;strong&gt;IMPORTANT&lt;/strong&gt;: Unique client identifier&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4.2 Sign the client certificate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Sign the client certificate&lt;/span&gt;
openssl x509 &lt;span class="nt"&gt;-req&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 365 &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-in&lt;/span&gt; client/client.csr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-CA&lt;/span&gt; ca/certs/ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-CAkey&lt;/span&gt; ca/private/ca.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-CAcreateserial&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; client/certs/client.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-extfile&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"basicConstraints=CA:FALSE&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;keyUsage=digitalSignature&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;extendedKeyUsage=clientAuth"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Verify the client certificate&lt;/span&gt;
openssl x509 &lt;span class="nt"&gt;-in&lt;/span&gt; client/certs/client.crt &lt;span class="nt"&gt;-text&lt;/span&gt; &lt;span class="nt"&gt;-noout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxjvfebwc5rcs8sk3z9v2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxjvfebwc5rcs8sk3z9v2.png" alt="Verificate client certificate" width="800" height="176"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Create PKCS#12 file for the client
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create PKCS#12 (p12/pfx) file for easy import&lt;/span&gt;
openssl pkcs12 &lt;span class="nt"&gt;-export&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-inkey&lt;/span&gt; client/private/client.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-in&lt;/span&gt; client/certs/client.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-certfile&lt;/span&gt; ca/certs/ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; client/certs/client.p12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Deploy a "Hello World" with Docker Compose and mTLS
&lt;/h2&gt;

&lt;p&gt;Let's create a simple example using &lt;strong&gt;Docker Compose&lt;/strong&gt; with an Nginx server that responds "Hello World" and is protected with mTLS. Remember, this web app must be deployed using the same domain or IP used when generating the server keys in step 3.1.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1 Project structure
&lt;/h3&gt;

&lt;p&gt;Create the following directory structure on your server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/mtls-demo/nginx
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/mtls-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.2 Copy the generated certificates
&lt;/h3&gt;

&lt;p&gt;Copy the certificates we generated in the previous steps:&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;# Create certificate directory in the project&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/mtls-demo/certs

&lt;span class="c"&gt;# Copy server certificates (in our case they're on the same server)&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; ~/mtls-ca/server/certs/server-chain.crt ~/mtls-demo/certs/
&lt;span class="nb"&gt;cp&lt;/span&gt; ~/mtls-ca/server/private/server.key ~/mtls-demo/certs/

&lt;span class="c"&gt;# Copy CA (for client verification)&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; ~/mtls-ca/ca/certs/ca.crt ~/mtls-demo/certs/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.3 Create the HTML page
&lt;/h3&gt;

&lt;p&gt;Create the file &lt;code&gt;~/mtls-demo/nginx/index.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;mTLS - Hello World&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Segoe UI'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tahoma&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Geneva&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Verdana&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;135deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#667eea&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#764ba2&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nc"&gt;.container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="py"&gt;backdrop-filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="m"&gt;32px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nc"&gt;.cert-info&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.9rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nc"&gt;.badge&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#4CAF50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.3rem&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.8rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;🔒 Hello World!&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Secure connection established via &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;mTLS&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cert-info"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Authenticated client:&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"client-cn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;---&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Protocol:&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt; TLS 1.3&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"badge"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;✅ mTLS Enabled&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.4 Configure Nginx with mTLS
&lt;/h3&gt;

&lt;p&gt;Create the file &lt;code&gt;~/mtls-demo/nginx/default.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;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;8443&lt;/span&gt; &lt;span class="s"&gt;ssl&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;localhost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Server certificates&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_certificate&lt;/span&gt; &lt;span class="n"&gt;/etc/nginx/certs/server-chain.crt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="n"&gt;/etc/nginx/certs/server.key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Modern SSL configuration&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_protocols&lt;/span&gt; &lt;span class="s"&gt;TLSv1.2&lt;/span&gt; &lt;span class="s"&gt;TLSv1.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_ciphers&lt;/span&gt; &lt;span class="s"&gt;HIGH:!aNULL:!MD5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_prefer_server_ciphers&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;ssl_session_cache&lt;/span&gt; &lt;span class="s"&gt;shared:SSL:10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_session_timeout&lt;/span&gt; &lt;span class="mi"&gt;10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# mTLS: verify client&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_client_certificate&lt;/span&gt; &lt;span class="n"&gt;/etc/nginx/certs/ca.crt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_verify_client&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_verify_depth&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Document root&lt;/span&gt;
    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/usr/share/nginx/html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.html&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="c1"&gt;# Reject if client certificate is not valid&lt;/span&gt;
        &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ssl_client_verify&lt;/span&gt; &lt;span class="s"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;SUCCESS)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="s"&gt;"Access&lt;/span&gt; &lt;span class="s"&gt;denied:&lt;/span&gt; &lt;span class="s"&gt;client&lt;/span&gt; &lt;span class="s"&gt;certificate&lt;/span&gt; &lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;n"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;# Pass client information to the app&lt;/span&gt;
        &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Client-CN&lt;/span&gt; &lt;span class="nv"&gt;$ssl_client_s_dn&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Client-Verify&lt;/span&gt; &lt;span class="nv"&gt;$ssl_client_verify&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&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;h3&gt;
  
  
  5.5 Create the Docker Compose file
&lt;/h3&gt;

&lt;p&gt;Create the file &lt;code&gt;~/mtls-demo/docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mtls-server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mtls-nginx&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;8443:8443"&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;./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./nginx/index.html:/usr/share/nginx/html/index.html:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./certs/server-chain.crt:/etc/nginx/certs/server-chain.crt:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./certs/server.key:/etc/nginx/certs/server.key:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./certs/ca.crt:/etc/nginx/certs/ca.crt:ro&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkxnpwph1khau75uvdrj9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkxnpwph1khau75uvdrj9.png" alt="Structure website" width="440" height="209"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.6 Deploy the service
&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; ~/mtls-demo

&lt;span class="c"&gt;# Start the container&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Verify it's running&lt;/span&gt;
docker compose ps

&lt;span class="c"&gt;# View logs&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="c"&gt;# Stop the container&lt;/span&gt;
docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.7 Verify the deployment
&lt;/h3&gt;

&lt;p&gt;Go to your browser and navigate to &lt;a href="https://YOUR_WEB_IP_OR_DOMAIN:8443" rel="noopener noreferrer"&gt;https://YOUR_WEB_IP_OR_DOMAIN:8443&lt;/a&gt;. You should see a 400 error since we haven't configured the browser to send the client certificate yet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7yt5qcq9dmum89ywnid1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7yt5qcq9dmum89ywnid1.png" alt="Website no tls" width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If we test with curl, we'll get a similar result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1q8ffn4bzem0a4dnumpa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1q8ffn4bzem0a4dnumpa.png" alt="Curl no tls" width="592" height="172"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Successful Tests
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6.1 CURL with client certificate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cacert&lt;/span&gt; ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cert&lt;/span&gt; client.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key&lt;/span&gt; client.key &lt;span class="se"&gt;\&lt;/span&gt;
  https://YOUR_WEB_IP_OR_DOMAIN:8443/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need to save these files in the path where you'll run the curl command. You can get them from the following server paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ca.crt =&amp;gt; ~/mtls-ca/ca/certs/ca.crt&lt;/li&gt;
&lt;li&gt;client.crt =&amp;gt; ~/mtls-ca/client/certs/client.crt&lt;/li&gt;
&lt;li&gt;client.key =&amp;gt; ~/mtls-ca/client/private/client.key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fftnl3jb950v7wsx4cou4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fftnl3jb950v7wsx4cou4.png" alt="Curl yes tls" width="800" height="817"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6.2 Chrome with client certificate
&lt;/h3&gt;

&lt;p&gt;Chrome and other browsers can use client certificates for mTLS authentication. To test this, you first need to import the &lt;code&gt;.p12&lt;/code&gt; certificate at the &lt;strong&gt;operating system level&lt;/strong&gt;, since Chrome no longer allows importing them directly from its settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Download the certificate from the server:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scp root@server_ip:/root/mtls-ca/client/certs/client.p12 ./client.p12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Import according to your operating system:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On macOS (Keychain Access):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;open client.p12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keychain Access will open. Enter the password you set when creating the PKCS#12 file, and the certificate will be available for Chrome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On Windows (Certificate Manager):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;"Personal"&lt;/strong&gt; → &lt;strong&gt;"Certificates"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Right-click → &lt;strong&gt;"All Tasks"&lt;/strong&gt; → &lt;strong&gt;"Import..."&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;client.p12&lt;/code&gt; and enter the password&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;On Linux:&lt;/strong&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;# Convert .p12 to .pem&lt;/span&gt;
openssl pkcs12 &lt;span class="nt"&gt;-in&lt;/span&gt; client.p12 &lt;span class="nt"&gt;-out&lt;/span&gt; client.pem &lt;span class="nt"&gt;-nodes&lt;/span&gt;

&lt;span class="c"&gt;# Start Chrome with system certificate support&lt;/span&gt;
google-chrome &lt;span class="nt"&gt;--enable-features&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;PlatformCertificateProvider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frav52gry9nto5bguzvq2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frav52gry9nto5bguzvq2.png" alt="Firefox import p12" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You might get an error initially, so it's best to test in an incognito tab or restart the browser so it picks up the newly installed certificate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhbx96oytz1n6g1gy2ot3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhbx96oytz1n6g1gy2ot3.png" alt="Firefox yes tls" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Implementing &lt;strong&gt;mTLS&lt;/strong&gt; is one of the best security investments you can make to protect communications between services. With this practical Docker Compose example, you've seen how to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a private CA&lt;/strong&gt; and generate certificates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure Nginx&lt;/strong&gt; with mTLS in a Docker container&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with curl&lt;/strong&gt; with and without a certificate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with Chrome&lt;/strong&gt; by importing the client certificate&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This same pattern applies to production environments, Kubernetes microservices, internal APIs, and any communication requiring mutual authentication.&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.openssl.org/docs/" rel="noopener noreferrer"&gt;Official OpenSSL documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.nginx.com/waf/configure/secure-mtls/" rel="noopener noreferrer"&gt;mTLS in Nginx&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/ssl/client-certificates/" rel="noopener noreferrer"&gt;Cloudflare mTLS guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04" rel="noopener noreferrer"&gt;Docker Compose installation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Did you enjoy this tutorial?&lt;/strong&gt; Share your experiences implementing mTLS or ask questions in the comments. What other security topics would you like to see in future articles?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Need a server for testing? You can create a Droplet on DigitalOcean using this link and get additional credit: &lt;a href="https://m.do.co/c/2c579acd7121" rel="noopener noreferrer"&gt;https://m.do.co/c/2c579acd7121&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

</description>
      <category>devops</category>
      <category>networking</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cómo asegurar un sistema a través de certificados mTLS (Mutual TLS)</title>
      <dc:creator>Oscar Ricardo Sánche Gutierréz</dc:creator>
      <pubDate>Thu, 23 Apr 2026 19:08:24 +0000</pubDate>
      <link>https://dev.to/oscar_ricardosncheguti/como-asegurar-un-sistema-a-traves-de-certificados-mtls-mutual-tls-30g2</link>
      <guid>https://dev.to/oscar_ricardosncheguti/como-asegurar-un-sistema-a-traves-de-certificados-mtls-mutual-tls-30g2</guid>
      <description>&lt;p&gt;En el mundo de la seguridad de aplicaciones modernas, &lt;strong&gt;mTLS (Mutual TLS)&lt;/strong&gt; se ha convertido en un estándar fundamental para proteger comunicaciones entre servicios. A diferencia del TLS tradicional donde solo el servidor se autentica, mTLS requiere que &lt;strong&gt;ambas partes&lt;/strong&gt; (cliente y servidor) presenten certificados válidos, creando una capa de seguridad mucho más robusta.&lt;/p&gt;

&lt;p&gt;En este artículo te mostraré cómo implementar mTLS paso a paso, desde la generación de certificados hasta la configuración en aplicaciones reales.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwkpgpoap1f93cpn6kmig.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwkpgpoap1f93cpn6kmig.gif" alt="Architecture savnet.co" width="700" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Diagrama realizado con &lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;https://savnet.co&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ¿Qué es mTLS y por qué es importante?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;mTLS (Mutual TLS)&lt;/strong&gt; es una extensión del protocolo TLS estándar que añade autenticación mutua. Mientras que en TLS tradicional solo el servidor presenta un certificado, en mTLS tanto el cliente como el servidor deben autenticarse mutuamente.&lt;/p&gt;

&lt;h3&gt;
  
  
  Beneficios clave:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Autenticación fuerte&lt;/strong&gt;: Ambos extremos de la comunicación se verifican&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevención de ataques MITM&lt;/strong&gt;: Dificulta los ataques de intermediario&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Trust&lt;/strong&gt;: Se alinea perfectamente con arquitecturas Zero Trust&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seguridad en microservicios&lt;/strong&gt;: Ideal para comunicaciones entre servicios&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Casos de uso comunes:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;APIs internas entre microservicios&lt;/li&gt;
&lt;li&gt;Comunicaciones entre servicios en Kubernetes&lt;/li&gt;
&lt;li&gt;Conexiones entre sistemas de pago&lt;/li&gt;
&lt;li&gt;Comunicaciones B2B seguras&lt;/li&gt;
&lt;li&gt;Acceso a APIs sensibles&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Paso 1: Configurar el entorno
&lt;/h2&gt;

&lt;p&gt;Para este tutorial, trabajaremos sobre un &lt;strong&gt;servidor Ubuntu&lt;/strong&gt; que funcionará como nuestro generador de certificados y Autoridad Certificadora (CA). Solo necesitamos un pequeño servidor con &lt;strong&gt;512MB de RAM y 1 CPU&lt;/strong&gt;, suficiente para generar y gestionar certificados.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu3wionjx4ihuuwz1dr4j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu3wionjx4ihuuwz1dr4j.png" alt="New server" width="800" height="546"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Necesitaremos algunas herramientas básicas:&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;# Actualizar sistema&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="c"&gt;# Instalar OpenSSL (si no está instalado)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; openssl curl wget

&lt;span class="c"&gt;# Verificar versión de OpenSSL&lt;/span&gt;
openssl version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0z7e6dd72ieky2bmvj75.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0z7e6dd72ieky2bmvj75.png" alt="Versión OpenSsl" width="800" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Para otros sistemas operativos, asegúrate de tener OpenSSL instalado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso 2: Crear la Autoridad Certificadora (CA)
&lt;/h2&gt;

&lt;p&gt;La CA es la entidad que emitirá y firmará todos nuestros certificados. Vamos a crear una CA privada.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 Crear directorio de trabajo
&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; ~/mtls-ca
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/mtls-ca
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ca/private ca/certs ca/newcerts
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; server/private server/certs
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; client/private client/certs

&lt;span class="c"&gt;# Configurar permisos seguros&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 ca/private server/private client/private
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2.2 Generar clave privada y certificado de la CA
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generar clave privada de la CA (protegida con contraseña)&lt;/span&gt;
openssl genrsa &lt;span class="nt"&gt;-aes256&lt;/span&gt; &lt;span class="nt"&gt;-out&lt;/span&gt; ca/private/ca.key 4096

&lt;span class="c"&gt;# Generar certificado autofirmado de la CA&lt;/span&gt;
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 7300 &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-key&lt;/span&gt; ca/private/ca.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; ca/certs/ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/C=ES/ST=Madrid/L=Madrid/O=MiEmpresa/OU=IT/CN=MiCA-Root/emailAddress=admin@miempresa.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Explicación del parámetro -subj&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/C=ES&lt;/code&gt;: Código de país (2 letras)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/ST=Madrid&lt;/code&gt;: Estado o provincia&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/L=Madrid&lt;/code&gt;: Localidad o ciudad&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/O=MiEmpresa&lt;/code&gt;: Nombre de tu organización&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/OU=IT&lt;/code&gt;: Unidad organizativa&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/CN=MiCA-Root&lt;/code&gt;: Nombre común de la CA&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/emailAddress=admin@miempresa.com&lt;/code&gt;: Email de contacto&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Personaliza estos valores&lt;/strong&gt; con tu información real.&lt;/p&gt;

&lt;p&gt;Durante el proceso se te pedirá:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contraseña para la clave privada (guárdala en un lugar seguro)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.3 Verificar el certificado de la CA
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl x509 &lt;span class="nt"&gt;-in&lt;/span&gt; ca/certs/ca.crt &lt;span class="nt"&gt;-text&lt;/span&gt; &lt;span class="nt"&gt;-noout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deberías ver información detallada sobre tu CA, incluyendo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Emisor: Tu propia CA&lt;/li&gt;
&lt;li&gt;Validez: 20 años (7300 días)&lt;/li&gt;
&lt;li&gt;Uso clave: Certificar otras claves&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Paso 3: Generar certificados para el servidor
&lt;/h2&gt;

&lt;p&gt;Ahora crearemos certificados para nuestro servidor.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 Crear clave privada y CSR del servidor
&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; ~/mtls-ca

&lt;span class="c"&gt;# Generar clave privada del servidor&lt;/span&gt;
openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; server/private/server.key 2048

&lt;span class="c"&gt;# Crear solicitud de firma de certificado (CSR)&lt;/span&gt;
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-key&lt;/span&gt; server/private/server.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; server/server.csr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/C=ES/ST=Madrid/L=Madrid/O=MiEmpresa/OU=Web/CN={ACA DOMINIO O IP DONDE ESTA TU APP WEB}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Personaliza el parámetro -subj&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/C=ES&lt;/code&gt;: Tu código de país&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/ST=Madrid&lt;/code&gt;: Tu estado o provincia&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/L=Madrid&lt;/code&gt;: Tu localidad o ciudad&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/O=MiEmpresa&lt;/code&gt;: Tu organización&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/OU=Web&lt;/code&gt;: Unidad organizativa (ej: Web, API, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/CN=api.midominio.com&lt;/code&gt;: &lt;strong&gt;IMPORTANTE&lt;/strong&gt;: El dominio o IP de tu servidor&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.2 Firmar el certificado del servidor con la CA
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Firmar el certificado del servidor&lt;/span&gt;
openssl x509 &lt;span class="nt"&gt;-req&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 365 &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-in&lt;/span&gt; server/server.csr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-CA&lt;/span&gt; ca/certs/ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-CAkey&lt;/span&gt; ca/private/ca.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-CAcreateserial&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; server/certs/server.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-extfile&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"basicConstraints=CA:FALSE&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;keyUsage=digitalSignature,keyEncipherment&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;extendedKeyUsage=serverAuth"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Verificar el certificado firmado&lt;/span&gt;
openssl x509 &lt;span class="nt"&gt;-in&lt;/span&gt; server/certs/server.crt &lt;span class="nt"&gt;-text&lt;/span&gt; &lt;span class="nt"&gt;-noout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F274ihx91zs1tenh8tr8r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F274ihx91zs1tenh8tr8r.png" alt="Verificación certificado server" width="800" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 Crear archivo de cadena de certificados
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Crear cadena de certificados para el servidor&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;server/certs/server.crt ca/certs/ca.crt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; server/certs/server-chain.crt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Paso 4: Generar certificados para el cliente
&lt;/h2&gt;

&lt;p&gt;Repetimos el proceso para el cliente.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Crear clave privada y CSR del cliente
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generar clave privada del cliente&lt;/span&gt;
openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; client/private/client.key 2048

&lt;span class="c"&gt;# Crear CSR para el cliente&lt;/span&gt;
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-key&lt;/span&gt; client/private/client.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; client/client.csr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/C=ES/ST=Madrid/L=Madrid/O=MiEmpresa/OU=Clientes/CN=app-client-01"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Personaliza el parámetro -subj&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/C=ES&lt;/code&gt;: Tu código de país&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/ST=Madrid&lt;/code&gt;: Tu estado o provincia&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/L=Madrid&lt;/code&gt;: Tu localidad o ciudad&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/O=MiEmpresa&lt;/code&gt;: Tu organización&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/OU=Clientes&lt;/code&gt;: Unidad organizativa (ej: Clientes, Apps, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/CN=app-client-01&lt;/code&gt;: &lt;strong&gt;IMPORTANTE&lt;/strong&gt;: Identificador único del cliente&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4.2 Firmar el certificado del cliente
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Firmar el certificado del cliente&lt;/span&gt;
openssl x509 &lt;span class="nt"&gt;-req&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 365 &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-in&lt;/span&gt; client/client.csr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-CA&lt;/span&gt; ca/certs/ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-CAkey&lt;/span&gt; ca/private/ca.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-CAcreateserial&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; client/certs/client.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-extfile&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"basicConstraints=CA:FALSE&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;keyUsage=digitalSignature&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;extendedKeyUsage=clientAuth"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Verificar el certificado del cliente&lt;/span&gt;
openssl x509 &lt;span class="nt"&gt;-in&lt;/span&gt; client/certs/client.crt &lt;span class="nt"&gt;-text&lt;/span&gt; &lt;span class="nt"&gt;-noout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fouk1kvcb7yd9fy0orh88.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fouk1kvcb7yd9fy0orh88.png" alt="Verificar certificado cliente" width="800" height="176"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Crear archivo PKCS#12 para el cliente
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Crear archivo PKCS#12 (p12/pfx) para fácil importación&lt;/span&gt;
openssl pkcs12 &lt;span class="nt"&gt;-export&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-inkey&lt;/span&gt; client/private/client.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-in&lt;/span&gt; client/certs/client.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-certfile&lt;/span&gt; ca/certs/ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; client/certs/client.p12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Paso 5: Desplegar un "Hola mundo" con Docker Compose y mTLS
&lt;/h2&gt;

&lt;p&gt;Vamos a crear un ejemplo simple de una web usando &lt;strong&gt;Docker Compose&lt;/strong&gt; con un servidor Nginx que responde "Hola mundo" y está protegido con mTLS. Recuerda, esta web debe ser desplegada con el mismo dominio o ip que se uso al generar las llaves del server en el punto 3.1.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1 Estructura del proyecto
&lt;/h3&gt;

&lt;p&gt;Crea la siguiente estructura de directorios en tu servidor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/mtls-demo/nginx
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/mtls-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.2 Copiar los certificados generados
&lt;/h3&gt;

&lt;p&gt;Copia los certificados que generamos en los pasos anteriores:&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;# Crear directorio para certificados en el proyecto&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/mtls-demo/certs

&lt;span class="c"&gt;# Copiar certificados del servidor, en nuestro caso están en el mismo servidor&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; ~/mtls-ca/server/certs/server-chain.crt ~/mtls-demo/certs/
&lt;span class="nb"&gt;cp&lt;/span&gt; ~/mtls-ca/server/private/server.key ~/mtls-demo/certs/

&lt;span class="c"&gt;# Copiar CA (para verificar clientes)&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; ~/mtls-ca/ca/certs/ca.crt ~/mtls-demo/certs/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.3 Crear la página HTML
&lt;/h3&gt;

&lt;p&gt;Crea el archivo &lt;code&gt;~/mtls-demo/nginx/index.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"es"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;mTLS - Hola Mundo&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Segoe UI'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tahoma&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Geneva&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Verdana&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;135deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#667eea&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#764ba2&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nc"&gt;.container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="py"&gt;backdrop-filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="m"&gt;32px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nc"&gt;.cert-info&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.9rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nc"&gt;.badge&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#4CAF50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.3rem&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.8rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;🔒 ¡Hola Mundo!&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Conexión segura establecida mediante &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;mTLS&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cert-info"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Cliente autenticado:&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"client-cn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;---&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Protocolo:&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt; TLS 1.3&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"badge"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;✅ mTLS Activado&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.4 Configurar Nginx con mTLS
&lt;/h3&gt;

&lt;p&gt;Crea el archivo &lt;code&gt;~/mtls-demo/nginx/default.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;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;8443&lt;/span&gt; &lt;span class="s"&gt;ssl&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;localhost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Certificados del servidor&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_certificate&lt;/span&gt; &lt;span class="n"&gt;/etc/nginx/certs/server-chain.crt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="n"&gt;/etc/nginx/certs/server.key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Configuración SSL moderna&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_protocols&lt;/span&gt; &lt;span class="s"&gt;TLSv1.2&lt;/span&gt; &lt;span class="s"&gt;TLSv1.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_ciphers&lt;/span&gt; &lt;span class="s"&gt;HIGH:!aNULL:!MD5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_prefer_server_ciphers&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;ssl_session_cache&lt;/span&gt; &lt;span class="s"&gt;shared:SSL:10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_session_timeout&lt;/span&gt; &lt;span class="mi"&gt;10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# mTLS: verificar cliente&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_client_certificate&lt;/span&gt; &lt;span class="n"&gt;/etc/nginx/certs/ca.crt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_verify_client&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_verify_depth&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Raíz de documentos&lt;/span&gt;
    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/usr/share/nginx/html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.html&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="c1"&gt;# Rechazar si el certificado del cliente no es válido&lt;/span&gt;
        &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ssl_client_verify&lt;/span&gt; &lt;span class="s"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;SUCCESS)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="s"&gt;"Acceso&lt;/span&gt; &lt;span class="s"&gt;denegado:&lt;/span&gt; &lt;span class="s"&gt;certificado&lt;/span&gt; &lt;span class="s"&gt;de&lt;/span&gt; &lt;span class="s"&gt;cliente&lt;/span&gt; &lt;span class="s"&gt;requerido&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;n"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;# Pasar información del cliente a la app&lt;/span&gt;
        &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Client-CN&lt;/span&gt; &lt;span class="nv"&gt;$ssl_client_s_dn&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Client-Verify&lt;/span&gt; &lt;span class="nv"&gt;$ssl_client_verify&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&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;h3&gt;
  
  
  5.5 Crear el Docker Compose
&lt;/h3&gt;

&lt;p&gt;Crea el archivo &lt;code&gt;~/mtls-demo/docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mtls-server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mtls-nginx&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;8443:8443"&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;./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./nginx/index.html:/usr/share/nginx/html/index.html:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./certs/server-chain.crt:/etc/nginx/certs/server-chain.crt:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./certs/server.key:/etc/nginx/certs/server.key:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./certs/ca.crt:/etc/nginx/certs/ca.crt:ro&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ah7r18dhh4dgdcm09qr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ah7r18dhh4dgdcm09qr.png" alt="Estructura sitio web" width="440" height="209"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.6 Desplegar el servicio
&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; ~/mtls-demo

&lt;span class="c"&gt;# Iniciar el contenedor&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Verificar que está corriendo&lt;/span&gt;
docker compose ps

&lt;span class="c"&gt;# Ver logs&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="c"&gt;# Bajar contenedor&lt;/span&gt;
docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.7 Verificar el despliegue
&lt;/h3&gt;

&lt;p&gt;Ve al navegador e ingresa a &lt;a href="https://IP_O_DOMOINIO_DE_TU_WEB:8443" rel="noopener noreferrer"&gt;https://IP_O_DOMOINIO_DE_TU_WEB:8443&lt;/a&gt; , deberás ver un error 400 ya que aún no hemos configurado el navegador para enviar el certificado de cliente.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F84c87uterlrfp8v172s5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F84c87uterlrfp8v172s5.png" alt="Sitio web sin tls" width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Si probamos con curl nos pasará algo parecido&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqwtw9scdw94hr9zlz3y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqwtw9scdw94hr9zlz3y.png" alt="Curl sin tls" width="592" height="172"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso 6: Pruebas exitosas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6.1 CURL Con certificado de cliente
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cacert&lt;/span&gt; ca.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cert&lt;/span&gt; client.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key&lt;/span&gt; client.key &lt;span class="se"&gt;\&lt;/span&gt;
  https://IP_O_DOMOINIO_DE_TU_WEB:8443/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Los archivos debes guardarlos en la ruta donde ejecutarás el comando curl y los obtienes de las siguientes rutas del servidor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ca.crt=&amp;gt; ~/mtls-ca/ca/certs/ca.crt&lt;/li&gt;
&lt;li&gt;client.crt=&amp;gt;  ~/mtls-ca/client/certs/client.crt&lt;/li&gt;
&lt;li&gt;client.key=&amp;gt; ~/mtls-ca/client/private/client.key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F06y8fou20hlv05m7jtyy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F06y8fou20hlv05m7jtyy.png" alt="Curl con tls" width="800" height="817"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6.2 Chrome Con certificado de cliente
&lt;/h2&gt;

&lt;p&gt;Chrome y otros navegadores pueden usar certificados de cliente para autenticación mTLS. Para probarlo, primero debes importar el certificado &lt;code&gt;.p12&lt;/code&gt; a nivel del &lt;strong&gt;sistema operativo&lt;/strong&gt;, ya que Chrome ya no permite importarlos directamente desde su configuración.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Descargar el certificado desde el servidor:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scp root@ip_servidor:/root/mtls-ca/client/certs/client.p12 ./client.p12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Importar según tu sistema operativo:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;En macOS (Keychain Access):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;open client.p12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se abrirá Keychain Access. Introduce la contraseña que pusiste al crear el PKCS#12 y el certificado quedará disponible para Chrome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;En Windows (Administrador de certificados):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Ve a &lt;strong&gt;"Personal"&lt;/strong&gt; → &lt;strong&gt;"Certificados"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Haz clic derecho → &lt;strong&gt;"Todas las tareas"&lt;/strong&gt; → &lt;strong&gt;"Importar..."&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Selecciona &lt;code&gt;client.p12&lt;/code&gt; e introduce la contraseña&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;En Linux:&lt;/strong&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;# Convertir .p12 a .pem&lt;/span&gt;
openssl pkcs12 &lt;span class="nt"&gt;-in&lt;/span&gt; client.p12 &lt;span class="nt"&gt;-out&lt;/span&gt; client.pem &lt;span class="nt"&gt;-nodes&lt;/span&gt;

&lt;span class="c"&gt;# Iniciar Chrome con soporte de certificados del sistema&lt;/span&gt;
google-chrome &lt;span class="nt"&gt;--enable-features&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;PlatformCertificateProvider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw9mp355yx0l6shvykyge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw9mp355yx0l6shvykyge.png" alt="Importar llave p12 firefox" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Es probable que te genere error al inicio así que lo mejor es probarlo en una pestaña incógnita o reiniciar el navegador para que tome el nuevo certificado instalado.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa3s3jyx8bpjtmq57yvh9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa3s3jyx8bpjtmq57yvh9.png" alt="Firefox con tls" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Implementar &lt;strong&gt;mTLS&lt;/strong&gt; es una de las mejores inversiones en seguridad que puedes hacer para proteger las comunicaciones entre servicios. Con este ejemplo práctico de Docker Compose has visto cómo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Crear una CA privada&lt;/strong&gt; y generar certificados&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configurar Nginx&lt;/strong&gt; con mTLS en un contenedor Docker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probar desde curl&lt;/strong&gt; con y sin certificado&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probar desde Chrome&lt;/strong&gt; importando el certificado de cliente&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Este mismo patrón se aplica a entornos de producción, microservicios en Kubernetes, APIs internas y cualquier comunicación que requiera autenticación mutua.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recursos adicionales
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.openssl.org/docs/" rel="noopener noreferrer"&gt;Documentación oficial de OpenSSL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.nginx.com/waf/configure/secure-mtls/" rel="noopener noreferrer"&gt;mTLS en Nginx&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/ssl/client-certificates/" rel="noopener noreferrer"&gt;Guía de mTLS de Cloudflare&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04" rel="noopener noreferrer"&gt;Docker Compose instalación&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;¿Te gustó este tutorial?&lt;/strong&gt; Comparte tus experiencias implementando mTLS o haz preguntas en los comentarios. ¿Qué otros temas de seguridad te gustaría ver en futuros artículos?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;¿Necesitas un servidor para tus pruebas? Puedes crear un Droplet en DigitalOcean usando este enlace y obtener crédito adicional: &lt;a href="https://m.do.co/c/2c579acd7121" rel="noopener noreferrer"&gt;https://m.do.co/c/2c579acd7121&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

</description>
      <category>networking</category>
      <category>security</category>
      <category>spanish</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>RustDesk: How to Create a Secure Private Remote Access Network</title>
      <dc:creator>Oscar Ricardo Sánche Gutierréz</dc:creator>
      <pubDate>Tue, 31 Mar 2026 00:47:18 +0000</pubDate>
      <link>https://dev.to/oscar_ricardosncheguti/rustdesk-how-to-create-a-secure-private-remote-access-network-1dj4</link>
      <guid>https://dev.to/oscar_ricardosncheguti/rustdesk-how-to-create-a-secure-private-remote-access-network-1dj4</guid>
      <description>&lt;p&gt;RustDesk is the perfect solution for secure remote access, open-source and under your complete control. In this article I'll show you how to deploy your own server, configure it as a closed network, and secure it for corporate use.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgctjvatzp24vwqaqafx3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgctjvatzp24vwqaqafx3.gif" alt="Architecture" width="700" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Diagram created with &lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;https://savnet.co&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;RustDesk is an open-source alternative to tools like TeamViewer or AnyDesk, but with a key advantage: &lt;strong&gt;you can self-host it&lt;/strong&gt;. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complete control&lt;/strong&gt; over your data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No connection limits&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero licensing costs&lt;/strong&gt; (only server costs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Prepare the Server
&lt;/h2&gt;

&lt;p&gt;You'll need a Linux server with Ubuntu 22.04 or 24.04 LTS with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1 vCPU, 2 GB RAM&lt;/strong&gt; (enough for dozens of connections)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ubuntu 22.04 LTS&lt;/strong&gt; or higher&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fixed public IP&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc1nozchz2tacuisj03xp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc1nozchz2tacuisj03xp.png" alt="New DigitalOcean server" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Initial Server Configuration
&lt;/h2&gt;

&lt;p&gt;Connect via SSH and update the system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@your_server_ip
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;
timedatectl set-timezone America/New_York  &lt;span class="c"&gt;# Adjust to your timezone&lt;/span&gt;
reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Install Docker and Docker Compose
&lt;/h2&gt;

&lt;p&gt;Follow the official Docker installation:&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;# Install dependencies&lt;/span&gt;
apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ca-certificates curl gnupg

&lt;span class="c"&gt;# Add official Docker repository&lt;/span&gt;
&lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/ubuntu/gpg &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.asc
&lt;span class="nb"&gt;chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.asc

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/etc/apt/keyrings/docker.asc] &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  https://download.docker.com/linux/ubuntu &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; stable"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/docker.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

&lt;span class="c"&gt;# Install Docker&lt;/span&gt;
apt update
apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

&lt;span class="c"&gt;# Verify installation&lt;/span&gt;
docker &lt;span class="nt"&gt;--version&lt;/span&gt;
docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx1avbg4jwfgvd0dd1vz3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx1avbg4jwfgvd0dd1vz3.png" alt="Docker versions" width="377" height="134"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Prepare RustDesk Structure
&lt;/h2&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/rustdesk-server/data
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/rustdesk-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Create the docker-compose.yml File
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;nano /opt/rustdesk-server/compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hbbr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hbbr&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;rustdesk/rustdesk-server:latest&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hbbr&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;./data:/root&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host"&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;hbbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hbbs&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;rustdesk/rustdesk-server:latest&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hbbs&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;./data:/root&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;hbbr&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: We use &lt;code&gt;network_mode: "host"&lt;/code&gt; because RustDesk needs to see the real host IP to function correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Start the Services
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start services&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Verify they're running&lt;/span&gt;
docker ps

&lt;span class="c"&gt;# View logs&lt;/span&gt;
docker compose logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmx4j1ehfg2x9p1rti10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmx4j1ehfg2x9p1rti10.png" alt="Docker services" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Get the Server Public Key
&lt;/h2&gt;

&lt;p&gt;This key is crucial for clients to trust your server:&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;cat&lt;/span&gt; /opt/rustdesk-server/data/id_ed25519.pub &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the result. It will look something like:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 8: Configure the Firewall
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Ports needed for RustDesk OSS:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TCP 21115&lt;/strong&gt; - Main service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TCP 21116&lt;/strong&gt; - ID service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UDP 21116&lt;/strong&gt; - For better performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TCP 21117&lt;/strong&gt; - Relay service&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configure UFW on the server:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install firewall ufw&lt;/span&gt;
apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ufw

&lt;span class="c"&gt;# Allow SSH from YOUR public IP&lt;/span&gt;
ufw allow from your_admin_ip to any port 22
&lt;span class="c"&gt;# IF YOU DON'T HAVE a public IP, allow any origin with&lt;/span&gt;
ufw allow 22

&lt;span class="c"&gt;# Allow RustDesk ports&lt;/span&gt;
ufw allow 21115/tcp
ufw allow 21116/tcp
ufw allow 21116/udp
ufw allow 21117/tcp

&lt;span class="c"&gt;# Enable firewall&lt;/span&gt;
ufw &lt;span class="nb"&gt;enable
&lt;/span&gt;ufw status verbose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl8bedz4fx07i1dyb5wjj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl8bedz4fx07i1dyb5wjj.png" alt="Ufw firewall" width="658" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure cloud provider firewall:
&lt;/h3&gt;

&lt;p&gt;In your cloud provider panel, create &lt;strong&gt;Inbound&lt;/strong&gt; rules that allow only from your corporate IPs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw7rgd6qo12hrkpq6pwo1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw7rgd6qo12hrkpq6pwo1.png" alt="DigitalOcean firewall" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Configure Computer/Client
&lt;/h2&gt;

&lt;p&gt;On each computer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open RustDesk&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings → Network&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Unlock Network Settings&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Server ID/Relay&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Configure:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ID Server&lt;/strong&gt;: &lt;code&gt;YourRustServerIP&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relay Server&lt;/strong&gt;: &lt;code&gt;YourRustServerIP&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key&lt;/strong&gt;: Paste the public key obtained in step 7&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Server&lt;/strong&gt;: Leave empty (only for RustDesk Pro)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fecxhizufe5wnpnjo2pvj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fecxhizufe5wnpnjo2pvj.png" alt="RustDesk client setup" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to quickly use your configuration, click the copy icon in the top right corner, and the paste icon to import:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8xo5iywnx2lzegfixfta.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8xo5iywnx2lzegfixfta.png" alt="RustDesk client export/import" width="800" height="219"&gt;&lt;/a&gt;&lt;/p&gt;







&lt;h2&gt;
  
  
  Ways to Improve Your RustDesk Client Security
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fke9a0hzzqwqxt4xe0118.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fke9a0hzzqwqxt4xe0118.png" alt="Security RustDesk client" width="760" height="992"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;RustDesk offers multiple authentication methods to securely control remote access. Here I explain each option:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;One-time password&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Automatically generated for each session and the user must provide it to the remote technician to connect&lt;/li&gt;
&lt;li&gt;Length options: 6, 8, or 10 digits&lt;/li&gt;
&lt;li&gt;The key changes with each session&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ideal use&lt;/strong&gt;: Temporary technical support or occasional access&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Permanent password&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Key you specify manually that doesn't change between sessions&lt;/li&gt;
&lt;li&gt;Allows continuous access without needing to share new keys&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ideal use&lt;/strong&gt;: Frequent remote access to your own computer&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Both passwords&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Flexibility to use temporary OR permanent password&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ideal use&lt;/strong&gt;: Mixed scenarios (personal use + occasional support)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. &lt;strong&gt;Two-Factor Authentication (2FA)&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Additional security layer&lt;/li&gt;
&lt;li&gt;Options: codes from authenticator app or Telegram bot integration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ideal use&lt;/strong&gt;: Computers accessible from public internet&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. &lt;strong&gt;Trusted devices&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Only applies when using 2FA&lt;/li&gt;
&lt;li&gt;Mark specific devices as trusted&lt;/li&gt;
&lt;li&gt;Avoids requesting 2FA on each connection from those devices&lt;/li&gt;
&lt;li&gt;Improves convenience while maintaining security&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. &lt;strong&gt;Additional Security Settings&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Password length&lt;/strong&gt;: Configurable based on security needs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiration time&lt;/strong&gt;: For temporary passwords&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit logging&lt;/strong&gt;: Access monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Practical Recommendations:
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For frequent personal use&lt;/strong&gt;: Permanent password + optional 2FA&lt;br&gt;&lt;br&gt;
&lt;strong&gt;For technical support&lt;/strong&gt;: One-time password&lt;br&gt;&lt;br&gt;
&lt;strong&gt;For corporate environments&lt;/strong&gt;: Mandatory 2FA + trusted devices&lt;br&gt;&lt;br&gt;
&lt;strong&gt;For maximum security&lt;/strong&gt;: Combination of methods + audit logging  &lt;/p&gt;

&lt;p&gt;These methods allow you to balance security and convenience according to your specific needs.&lt;/p&gt;







&lt;h2&gt;
  
  
  How to Make This a Truly Closed Network
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Block Access from Public Internet on the RustDesk Server
&lt;/h3&gt;

&lt;p&gt;Don't allow access from any IP in the firewall ufw rules or cloud provider rules. Only allow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your office IPs&lt;/li&gt;
&lt;li&gt;Your corporate VPN IP&lt;/li&gt;
&lt;li&gt;Specific authorized ranges&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Implement VPN for Remote Access
&lt;/h3&gt;

&lt;p&gt;The most secure way: remote users first connect to the corporate VPN, then access RustDesk.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Create Administrative User Without Root:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adduser adminops
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;adminops

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/adminops/.ssh
&lt;span class="nb"&gt;cp&lt;/span&gt; /root/.ssh/authorized_keys /home/adminops/.ssh/authorized_keys
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; adminops:adminops /home/adminops/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 /home/adminops/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /home/adminops/.ssh/authorized_keys

&lt;span class="c"&gt;# Disable root SSH&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'^PermitRootLogin'&lt;/span&gt; /etc/ssh/sshd_config&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/^PermitRootLogin.*/PermitRootLogin no/'&lt;/span&gt; /etc/ssh/sshd_config
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'PermitRootLogin no'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/ssh/sshd_config
&lt;span class="k"&gt;fi

&lt;/span&gt;sshd &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; systemctl restart ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  RustDesk Pro: For Advanced Enterprise Needs
&lt;/h2&gt;

&lt;p&gt;The RustDesk version we've configured is excellent for basic remote access, but if you need advanced enterprise features, RustDesk offers a Pro version with additional capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Centralized user and group management&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Granular access control&lt;/strong&gt; (role-based permissions)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LDAP/Active Directory authentication&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Detailed auditing and logs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Priority technical support&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mass management functions&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For these organizations, you can check the Pro plans at &lt;a href="https://rustdesk.com/pricing/" rel="noopener noreferrer"&gt;https://rustdesk.com/pricing/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Need a cloud server?&lt;/strong&gt; You can get an Ubuntu Droplet on DigitalOcean using our &lt;a href="https://m.do.co/c/2c579acd7121" rel="noopener noreferrer"&gt;referral link&lt;/a&gt; and receive initial credits to try this tutorial.&lt;/p&gt;




&lt;p&gt;Have you implemented RustDesk in your organization? Share your experience in the comments!&lt;/p&gt;

</description>
      <category>rustdesk</category>
      <category>remotedesktop</category>
      <category>selfhosted</category>
      <category>opensource</category>
    </item>
    <item>
      <title>RustDesk: Cómo crear una red de acceso remoto segura y privada</title>
      <dc:creator>Oscar Ricardo Sánche Gutierréz</dc:creator>
      <pubDate>Tue, 31 Mar 2026 00:35:54 +0000</pubDate>
      <link>https://dev.to/oscar_ricardosncheguti/rustdesk-como-crear-una-red-de-acceso-remoto-segura-y-privada-50df</link>
      <guid>https://dev.to/oscar_ricardosncheguti/rustdesk-como-crear-una-red-de-acceso-remoto-segura-y-privada-50df</guid>
      <description>&lt;p&gt;RustDesk es la solución perfecta para acceso remoto seguro, de código abierto y bajo tu control completo. En este artículo te mostraré cómo desplegar tu propio servidor, configurarlo como una red cerrada y asegurarlo para uso corporativo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi38r6i6fxppelpaqzhpa.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi38r6i6fxppelpaqzhpa.gif" alt="Architecture" width="700" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Diagrama realizado con &lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;https://savnet.co&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;RustDesk es una alternativa de código abierto a herramientas como TeamViewer o AnyDesk, pero con una ventaja clave: &lt;strong&gt;puedes autoalojarlo&lt;/strong&gt;. Esto significa:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Control total&lt;/strong&gt; sobre tus datos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sin límites&lt;/strong&gt; de conexión&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Costo cero&lt;/strong&gt; en licencias (solo el servidor)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Paso 1: Preparar el servidor
&lt;/h2&gt;

&lt;p&gt;Necesitarás un servidor Linux con Ubuntu 22.04 o 24.04 LTS con:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1 vCPU, 2 GB RAM&lt;/strong&gt; (suficiente para decenas de conexiones)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ubuntu 22.04 LTS&lt;/strong&gt; o superior&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP pública fija&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0nrxitd5jg1qxzapucor.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0nrxitd5jg1qxzapucor.png" alt="New DigitalOcean Server" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso 2: Configuración inicial del servidor
&lt;/h2&gt;

&lt;p&gt;Conéctate por SSH y actualiza el sistema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@tu_ip_servidor
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;
timedatectl set-timezone America/Bogota  &lt;span class="c"&gt;# Ajusta a tu zona horaria&lt;/span&gt;
reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Paso 3: Instalar Docker y Docker Compose
&lt;/h2&gt;

&lt;p&gt;Sigue la instalación oficial de Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Instalar dependencias&lt;/span&gt;
apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ca-certificates curl gnupg

&lt;span class="c"&gt;# Agregar repositorio oficial de Docker&lt;/span&gt;
&lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/ubuntu/gpg &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.asc
&lt;span class="nb"&gt;chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.asc

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/etc/apt/keyrings/docker.asc] &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  https://download.docker.com/linux/ubuntu &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; stable"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/docker.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

&lt;span class="c"&gt;# Instalar Docker&lt;/span&gt;
apt update
apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

&lt;span class="c"&gt;# Verificar instalación&lt;/span&gt;
docker &lt;span class="nt"&gt;--version&lt;/span&gt;
docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo6pn1ggqqirum8e9u2r4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo6pn1ggqqirum8e9u2r4.png" alt="Versiones docker" width="377" height="134"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso 4: Preparar la estructura de RustDesk
&lt;/h2&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/rustdesk-server/data
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/rustdesk-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Paso 5: Crear el archivo docker-compose.yml
&lt;/h2&gt;

&lt;p&gt;Crea  &lt;code&gt;nano /opt/rustdesk-server/compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hbbr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hbbr&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;rustdesk/rustdesk-server:latest&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hbbr&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;./data:/root&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host"&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;hbbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hbbs&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;rustdesk/rustdesk-server:latest&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hbbs&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;./data:/root&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;hbbr&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Importante&lt;/strong&gt;: Usamos &lt;code&gt;network_mode: "host"&lt;/code&gt; porque RustDesk necesita ver la IP real del host para funcionar correctamente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso 6: Levantar los servicios
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Levantar servicios&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Verificar que estén corriendo&lt;/span&gt;
docker ps

&lt;span class="c"&gt;# Ver logs&lt;/span&gt;
docker compose logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F939r0p4g61pmlkc6c1ss.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F939r0p4g61pmlkc6c1ss.png" alt="Servicios docker" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso 7: Obtener la clave pública del servidor
&lt;/h2&gt;

&lt;p&gt;Esta clave es crucial para que los clientes confíen en tu servidor:&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;cat&lt;/span&gt; /opt/rustdesk-server/data/id_ed25519.pub &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guarda el resultado. Se verá algo como:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Paso 8: Configurar el firewall
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Puertos necesarios para RustDesk OSS:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TCP 21115&lt;/strong&gt; - Servicio principal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TCP 21116&lt;/strong&gt; - Servicio de ID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UDP 21116&lt;/strong&gt; - Para mejor rendimiento&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TCP 21117&lt;/strong&gt; - Servicio de relay&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configurar UFW en el servidor:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Instalar firewall ufw&lt;/span&gt;
apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ufw

&lt;span class="c"&gt;# Permitir SSH desde TU IP pública&lt;/span&gt;
ufw allow from tu_ip_admin to any port 22
&lt;span class="c"&gt;# SI NO TIENES IP pública, permite cualquier origen con&lt;/span&gt;
ufw allow 22

&lt;span class="c"&gt;# Permitir puertos de RustDesk&lt;/span&gt;
ufw allow 21115/tcp
ufw allow 21116/tcp
ufw allow 21116/udp
ufw allow 21117/tcp

&lt;span class="c"&gt;# Activar firewall&lt;/span&gt;
ufw &lt;span class="nb"&gt;enable
&lt;/span&gt;ufw status verbose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd2t0jrrgs9ozzj3tss1z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd2t0jrrgs9ozzj3tss1z.png" alt="Firewall ufw" width="658" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configurar firewall del proveedor cloud:
&lt;/h3&gt;

&lt;p&gt;En el panel de tu proveedor cloud, crea reglas &lt;strong&gt;Inbound&lt;/strong&gt; que permitan solo desde tus IPs corporativas:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frv9dt0ytzh0h2d3g8lu7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frv9dt0ytzh0h2d3g8lu7.png" alt="Firewall DigitalOcean" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso 9: Configurar computador/cliente
&lt;/h2&gt;

&lt;p&gt;En cada equipo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Abre RustDesk&lt;/li&gt;
&lt;li&gt;Ve a &lt;strong&gt;Settings → Network&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Haz click en &lt;strong&gt;Unlock Network Settings&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Ve a &lt;strong&gt;Servidor ID/Relay&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Configura:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ID Server&lt;/strong&gt;: &lt;code&gt;IpServidorRust&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relay Server&lt;/strong&gt;: &lt;code&gt;IpServidorRust&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key&lt;/strong&gt;: Pega la clave pública obtenida en el paso 7&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Server&lt;/strong&gt;: Déjalo vacío (solo para RustDesk Pro)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb5yb7rwtyhmmggqph587.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb5yb7rwtyhmmggqph587.png" alt="Configuración cliente Rust" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Si quieres usar rápidamente tu configuración da click en el icono de copiar en la parte superior derecha, y el icono de pegar para importar:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbgjimz6pmo0cffgaeuqv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbgjimz6pmo0cffgaeuqv.png" alt="Export/Import cliente RustDesk" width="800" height="219"&gt;&lt;/a&gt;&lt;/p&gt;







&lt;h2&gt;
  
  
  Formas de mejorar la seguridad de tu computador/cliente RustDesk
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6fson7mgpdnrdwh7kvii.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6fson7mgpdnrdwh7kvii.png" alt="Configuración seguridad cliente RustDesk" width="760" height="992"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;RustDesk ofrece múltiples métodos de autenticación para controlar el acceso remoto de forma segura. Aquí te explico cada opción:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Contraseña de un solo uso (One-time password)&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Se genera automáticamente en cada sesión y el usuario debe suministrarla al técnico remoto para que pueda conectarse.&lt;/li&gt;
&lt;li&gt;Opciones de longitud: 6, 8 o 10 dígitos&lt;/li&gt;
&lt;li&gt;La clave cambia en cada sesión.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uso ideal&lt;/strong&gt;: Soporte técnico puntual o accesos temporales&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Contraseña permanente (Permanent password)&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Clave que especificas manualmente y no cambia entre sesiones&lt;/li&gt;
&lt;li&gt;Permite acceso continuo sin necesidad de compartir nuevas claves&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uso ideal&lt;/strong&gt;: Acceso remoto frecuente a tu propio equipo&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Combinación de ambos métodos (Both passwords)&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Flexibilidad para usar contraseña temporal O permanente&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uso ideal&lt;/strong&gt;: Escenarios mixtos (uso personal + soporte ocasional)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. &lt;strong&gt;Autenticación en dos factores (2FA)&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Capa adicional de seguridad&lt;/li&gt;
&lt;li&gt;Opciones: códigos desde app autenticadora o integración con bot de Telegram&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uso ideal&lt;/strong&gt;: Equipos accesibles desde internet público&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. &lt;strong&gt;Dispositivos de confianza (Trusted devices)&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Solo aplica cuando usas 2FA&lt;/li&gt;
&lt;li&gt;Marcar dispositivos específicos como confiables&lt;/li&gt;
&lt;li&gt;Evita solicitar 2FA en cada conexión desde esos dispositivos&lt;/li&gt;
&lt;li&gt;Mejora la comodidad manteniendo seguridad&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. &lt;strong&gt;Configuraciones de seguridad adicionales&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Longitud de contraseña&lt;/strong&gt;: Configurable según necesidades de seguridad&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiempo de expiración&lt;/strong&gt;: Para contraseñas temporales&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registro de auditoría&lt;/strong&gt;: Monitoreo de accesos&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recomendaciones prácticas:
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Para uso personal frecuente&lt;/strong&gt;: Contraseña permanente + 2FA opcional&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Para soporte técnico&lt;/strong&gt;: Contraseña de un solo uso&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Para entornos corporativos&lt;/strong&gt;: 2FA obligatorio + dispositivos de confianza&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Para máxima seguridad&lt;/strong&gt;: Combinación de métodos + registro de auditoría  &lt;/p&gt;

&lt;p&gt;Estos métodos te permiten balancear seguridad y conveniencia según tus necesidades específicas.&lt;/p&gt;







&lt;h2&gt;
  
  
  Cómo convertir esto en una red realmente cerrada
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Bloquear acceso desde Internet público del servidor RustDesk
&lt;/h3&gt;

&lt;p&gt;No permitas acceso a cualquier ip en las reglas del firewall ufw o del operador cloud. Solo permite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IPs de tus oficinas&lt;/li&gt;
&lt;li&gt;IP de tu VPN corporativa&lt;/li&gt;
&lt;li&gt;Rangos específicos autorizados&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Implementar VPN para acceso remoto
&lt;/h3&gt;

&lt;p&gt;La forma más segura: usuarios remotos primero se conectan a la VPN corporativa, luego acceden a RustDesk.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Crear usuario administrativo sin root:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adduser adminops
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;adminops

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/adminops/.ssh
&lt;span class="nb"&gt;cp&lt;/span&gt; /root/.ssh/authorized_keys /home/adminops/.ssh/authorized_keys
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; adminops:adminops /home/adminops/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 /home/adminops/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /home/adminops/.ssh/authorized_keys

&lt;span class="c"&gt;# Deshabilitar root por SSH&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'^PermitRootLogin'&lt;/span&gt; /etc/ssh/sshd_config&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/^PermitRootLogin.*/PermitRootLogin no/'&lt;/span&gt; /etc/ssh/sshd_config
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'PermitRootLogin no'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/ssh/sshd_config
&lt;span class="k"&gt;fi

&lt;/span&gt;sshd &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; systemctl restart ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  RustDesk Pro: Para necesidades empresariales avanzadas
&lt;/h2&gt;

&lt;p&gt;La versión RustDesk que hemos configurado es excelente para acceso remoto básico, pero si necesitas funcionalidades empresariales avanzadas, RustDesk ofrece una versión Pro con características adicionales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gestión centralizada de usuarios y grupos&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control de acceso granular&lt;/strong&gt; (permisos por rol)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Autenticación LDAP/Active Directory&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auditoría y logs detallados&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Soporte técnico prioritario&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Funciones de administración masiva&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para estas organizaciones puedes consultar los planes Pro en &lt;a href="https://rustdesk.com/pricing/" rel="noopener noreferrer"&gt;https://rustdesk.com/pricing/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Necesitas un servidor en la nube?&lt;/strong&gt; Puedes obtener un Droplet Ubuntu en DigitalOcean usando nuestro &lt;a href="https://m.do.co/c/2c579acd7121" rel="noopener noreferrer"&gt;enlace de referido&lt;/a&gt; y recibir créditos iniciales para probar este tutorial.&lt;/p&gt;




&lt;p&gt;¿Has implementado RustDesk en tu organización? ¡Comparte tu experiencia en los comentarios!&lt;/p&gt;

</description>
      <category>rustdesk</category>
      <category>remotedesktop</category>
      <category>selfhosted</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Deploy Dokploy on an Ubuntu Server and Create Your First "Hello World" in Node.js</title>
      <dc:creator>Oscar Ricardo Sánche Gutierréz</dc:creator>
      <pubDate>Fri, 27 Mar 2026 22:41:28 +0000</pubDate>
      <link>https://dev.to/oscar_ricardosncheguti/how-to-deploy-dokploy-on-an-ubuntu-server-and-create-your-first-hello-world-in-nodejs-lg2</link>
      <guid>https://dev.to/oscar_ricardosncheguti/how-to-deploy-dokploy-on-an-ubuntu-server-and-create-your-first-hello-world-in-nodejs-lg2</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Dokploy is a self-hosted deployment platform that simplifies containerized application management using Docker Swarm. In this comprehensive tutorial, I'll show you how to install Dokploy on an &lt;strong&gt;Ubuntu server&lt;/strong&gt;, configure security with firewalls, deploy your first Node.js application, and follow production best practices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jvvlixnrkljsaanq0x3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jvvlixnrkljsaanq0x3.gif" alt="Architecture" width="700" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Diagram created with &lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;https://savnet.co&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;p&gt;Before starting, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;Ubuntu 24.04 LTS server&lt;/strong&gt; (minimum 2 GB RAM for testing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH access&lt;/strong&gt; with configured keys&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;domain or subdomain&lt;/strong&gt; (optional but recommended for HTTPS)&lt;/li&gt;
&lt;li&gt;Basic terminal and Docker knowledge&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Prepare Your Ubuntu Server
&lt;/h2&gt;

&lt;p&gt;If you already have an Ubuntu 24.04 LTS server with SSH access, you can skip this step. If you need to create one, follow these recommendations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Choose a cloud or VPS provider&lt;/strong&gt; that offers Ubuntu 24.04 LTS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Select a plan&lt;/strong&gt; with at least 2 GB of RAM for testing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure SSH key access&lt;/strong&gt; instead of password (more secure)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consider enabling backups and monitoring&lt;/strong&gt; for production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Note the public IP&lt;/strong&gt; of your server&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your provider has an integrated firewall (like DigitalOcean Cloud Firewall, AWS Security Groups, etc.), configure the necessary rules from the control panel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0o655rkgi11xk9gqelp2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0o655rkgi11xk9gqelp2.png" alt="New Server DigitalOcean" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Initial Server Configuration
&lt;/h2&gt;

&lt;p&gt;Connect to the server via SSH:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@YOUR_PUBLIC_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Update system and install utilities
&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;
apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl wget git ufw ca-certificates gnupg lsb-release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create administrator user (recommended)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adduser deploy
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;deploy
rsync &lt;span class="nt"&gt;--archive&lt;/span&gt; &lt;span class="nt"&gt;--chown&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deploy:deploy ~/.ssh /home/deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can connect with the new user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh deploy@YOUR_PUBLIC_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Configure Firewall with UFW
&lt;/h2&gt;

&lt;p&gt;Dokploy needs specific ports open. Configure UFW properly:&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;# Allow SSH first to avoid locking ourselves out&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow OpenSSH

&lt;span class="c"&gt;# Open ports needed for Dokploy and applications&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp     &lt;span class="c"&gt;# HTTP for Traefik&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp    &lt;span class="c"&gt;# HTTPS for Traefik&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/udp    &lt;span class="c"&gt;# HTTPS UDP for Traefik&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 3000/tcp   &lt;span class="c"&gt;# Dokploy panel (temporary only)&lt;/span&gt;

&lt;span class="c"&gt;# Enable firewall&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable
sudo &lt;/span&gt;ufw status numbered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Docker can bypass UFW rules because it manipulates &lt;code&gt;iptables&lt;/code&gt; directly. If your cloud provider offers an integrated firewall (like DigitalOcean Cloud Firewall, AWS Security Groups, etc.), we recommend configuring it as an additional security layer.&lt;/p&gt;

&lt;p&gt;Configure the same firewall rules in your provider's panel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;22/TCP&lt;/strong&gt; from your IP or trusted range (SSH)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;80/TCP&lt;/strong&gt; from Anywhere (HTTP)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;443/TCP&lt;/strong&gt; from Anywhere (HTTPS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;443/UDP&lt;/strong&gt; from Anywhere (HTTPS UDP)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3000/TCP&lt;/strong&gt; temporarily from your IP only (for initial setup)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;assets/dropplet-firewall.png&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 4: Install Dokploy
&lt;/h2&gt;

&lt;p&gt;The official Dokploy installation is straightforward with their script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://dokploy.com/install.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Installs Docker if not present&lt;/li&gt;
&lt;li&gt;Initializes Docker Swarm&lt;/li&gt;
&lt;li&gt;Creates the overlay network &lt;code&gt;dokploy-network&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Deploys Postgres, Redis, and Dokploy services&lt;/li&gt;
&lt;li&gt;Launches Traefik as reverse proxy&lt;/li&gt;
&lt;li&gt;Publishes the panel on port 3000&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Verify the Installation
&lt;/h2&gt;

&lt;p&gt;Check that everything is working:&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;# View Docker Swarm services&lt;/span&gt;
docker service &lt;span class="nb"&gt;ls&lt;/span&gt;

&lt;span class="c"&gt;# View running containers&lt;/span&gt;
docker ps

&lt;span class="c"&gt;# Verify ports are listening&lt;/span&gt;
ss &lt;span class="nt"&gt;-lntup&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'(:80|:443|:3000)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F81yncuovtp3fgsxjdp01.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F81yncuovtp3fgsxjdp01.png" alt="Console up services" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now open your browser and access the Dokploy panel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://YOUR_PUBLIC_IP:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Complete the initial setup wizard by creating your administrator account.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb7dzlkajpvs6xif8zvy8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb7dzlkajpvs6xif8zvy8.png" alt="Dashboard Home" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Configure Domain and HTTPS (optional but recommended)
&lt;/h2&gt;

&lt;p&gt;To access your applications with your own domain and HTTPS:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In your DNS provider's panel (Cloudflare, DigitalOcean, AWS Route53, etc.), add your domain if you haven't already.&lt;/li&gt;
&lt;li&gt;Create DNS records:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A&lt;/strong&gt; record for &lt;code&gt;@&lt;/code&gt; pointing to your server IP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A&lt;/strong&gt; record for &lt;code&gt;www&lt;/code&gt; pointing to your server IP&lt;/li&gt;
&lt;li&gt;Or a subdomain like &lt;code&gt;dokploy.yourdomain.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Wait for DNS propagation (may take a few minutes).&lt;/p&gt;

&lt;h3&gt;
  
  
  Restrict access to port 3000
&lt;/h3&gt;

&lt;p&gt;For security, once Dokploy is configured, restrict access to port 3000:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In your cloud provider's firewall, limit it to your IP only if you need administrative access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Deploy Your First "Hello World" Application in Node.js
&lt;/h2&gt;

&lt;p&gt;Let's create a simple Node.js application and deploy it to Dokploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.1 Create the project locally
&lt;/h3&gt;

&lt;p&gt;Create a folder for your project with the following files:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;package.json&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hello-world-dokploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello World application for Dokploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node app.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"express"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.18.2"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;app.js&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello World from Dokploy!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;&lt;strong&gt;Dockerfile&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:18-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; node&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "app.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;docker-compose.yml&lt;/strong&gt; (for reference):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&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;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&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;NODE_ENV=staging&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7.2 Deploy to Dokploy
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Deploy part 1
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fei5sgxaa5p58rgg5sqgp.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fei5sgxaa5p58rgg5sqgp.gif" alt="Deploy part 1" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Deploy part 2
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk437rlxfxy662zubyddy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk437rlxfxy662zubyddy.gif" alt="Deploy part 2" width="720" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the Dokploy panel, click &lt;strong&gt;Create Project&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give your project a name (e.g., "Tests")&lt;/li&gt;
&lt;li&gt;In the project panel, click &lt;strong&gt;Create Service&lt;/strong&gt; and select Application&lt;/li&gt;
&lt;li&gt;Name your application: &lt;strong&gt;Hello World&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Then access your application and in the bottom section &lt;strong&gt;Build Type&lt;/strong&gt; select Dockerfile and then Save.&lt;/li&gt;
&lt;li&gt;In the top section you have &lt;strong&gt;Provider&lt;/strong&gt;. For this test we'll select the Drop type, however for real cases the best option is through git repository.&lt;/li&gt;
&lt;li&gt;Once Drop is selected, drag to the &lt;strong&gt;Zip file&lt;/strong&gt; area the zip with the code we specified in point 7.1.&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;Deploy&lt;/strong&gt; button&lt;/li&gt;
&lt;li&gt;Once deployment is complete, it will show the &lt;strong&gt;Deployments&lt;/strong&gt; tab where you should see a green dot and &lt;strong&gt;Done&lt;/strong&gt; indicating the process was successful. You can click &lt;strong&gt;View&lt;/strong&gt; to verify the process.&lt;/li&gt;
&lt;li&gt;Now, to deploy our Hello World, go to the &lt;strong&gt;Domains&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;For this test we'll use a free Traefik domain by clicking on the dice icon, specify that the &lt;strong&gt;Container Port&lt;/strong&gt; is 3000 and enable HTTPS with Let's Encrypt provider.&lt;/li&gt;
&lt;li&gt;Then click the &lt;strong&gt;Create&lt;/strong&gt; button&lt;/li&gt;
&lt;li&gt;When finished, it will give us a URL where we can enter and see our web. Important: since we're using a test domain it will give security warnings. For production environments use your own domain.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  7.3 Verify the deployment
&lt;/h3&gt;

&lt;p&gt;Once deployment is complete, access your application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://tests-hello-world-puloud-d13d9c-167-172-as2dsaccc234-151.traefik.me/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the JSON "Hello World" message.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo1t1gls363gjabhue4xk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo1t1gls363gjabhue4xk.png" alt="Browser Helo App" width="784" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Production Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Don't build on production server
&lt;/h3&gt;

&lt;p&gt;Dokploy recommends building Docker images in CI/CD (GitHub Actions, GitLab CI) and then deploying the pre-built image. This saves resources and reduces downtime risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use health checks
&lt;/h3&gt;

&lt;p&gt;Configure health checks in your applications (like the &lt;code&gt;/health&lt;/code&gt; endpoint in our example) so Docker Swarm can monitor status.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Configure backups
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Configure regular server backups&lt;/strong&gt; (if your provider offers this feature)&lt;/li&gt;
&lt;li&gt;Configure regular database backups&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;docker volume&lt;/code&gt; for persistent data&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Monitoring and logs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# View Dokploy logs&lt;/span&gt;
docker service logs dokploy &lt;span class="nt"&gt;--tail&lt;/span&gt; 100 &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="c"&gt;# View application logs&lt;/span&gt;
docker service logs hello-world_app &lt;span class="nt"&gt;--tail&lt;/span&gt; 100 &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="c"&gt;# View service status&lt;/span&gt;
docker service ps hello-world_app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Dokploy offers a robust, self-hosted solution for deploying applications on Docker Swarm. By implementing it on an Ubuntu server, you get a scalable, secure, and professional infrastructure at an accessible cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Need a cloud server?&lt;/strong&gt; You can get an Ubuntu Droplet on DigitalOcean using our &lt;a href="https://m.do.co/c/2c579acd7121" rel="noopener noreferrer"&gt;referral link&lt;/a&gt; and receive initial credits to try this tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.dokploy.com" rel="noopener noreferrer"&gt;Official Dokploy Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.dokploy.com/docs/core/remote-servers/security" rel="noopener noreferrer"&gt;Dokploy Security Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.digitalocean.com/products/networking/firewalls/" rel="noopener noreferrer"&gt;DigitalOcean Firewall Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dokploy/dokploy" rel="noopener noreferrer"&gt;Dokploy GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discord.gg/dokploy" rel="noopener noreferrer"&gt;Dokploy Discord Community&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Enjoyed this tutorial?&lt;/strong&gt; Share your experiences deploying applications with Dokploy in the comments or suggest topics for future articles.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>dokploy</category>
      <category>node</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cómo desplegar Dokploy en un servidor Ubuntu y crear tu primer "Hola Mundo" en Node.js</title>
      <dc:creator>Oscar Ricardo Sánche Gutierréz</dc:creator>
      <pubDate>Fri, 27 Mar 2026 22:38:03 +0000</pubDate>
      <link>https://dev.to/oscar_ricardosncheguti/como-desplegar-dokploy-en-un-servidor-ubuntu-y-crear-tu-primer-hola-mundo-en-nodejs-14b8</link>
      <guid>https://dev.to/oscar_ricardosncheguti/como-desplegar-dokploy-en-un-servidor-ubuntu-y-crear-tu-primer-hola-mundo-en-nodejs-14b8</guid>
      <description>&lt;h2&gt;
  
  
  Introducción
&lt;/h2&gt;

&lt;p&gt;Dokploy es una plataforma de despliegue auto-hospedada que simplifica la gestión de aplicaciones en contenedores Docker usando Docker Swarm. En este tutorial completo, te mostraré cómo instalar Dokploy en un &lt;strong&gt;servidor Ubuntu&lt;/strong&gt;, configurar seguridad con firewall, desplegar tu primera aplicación Node.js y seguir las mejores prácticas para producción.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F85bwgux6b6zb9kbrh6s3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F85bwgux6b6zb9kbrh6s3.gif" alt="Arquitectura" width="700" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Diagrama realizado con &lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;https://savnet.co&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ¿Qué necesitas?
&lt;/h2&gt;

&lt;p&gt;Antes de comenzar, asegúrate de tener:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un &lt;strong&gt;servidor con Ubuntu 24.04 LTS&lt;/strong&gt; (2 GB de RAM mínimo para pruebas)&lt;/li&gt;
&lt;li&gt;Acceso por &lt;strong&gt;SSH&lt;/strong&gt; con claves configuradas&lt;/li&gt;
&lt;li&gt;Un &lt;strong&gt;dominio o subdominio&lt;/strong&gt; (opcional, pero recomendado para HTTPS)&lt;/li&gt;
&lt;li&gt;Conocimientos básicos de terminal y Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Paso 1: Preparar tu servidor Ubuntu
&lt;/h2&gt;

&lt;p&gt;Si ya tienes un servidor Ubuntu 24.04 LTS con acceso SSH, puedes saltar este paso. Si necesitas crear uno, sigue estas recomendaciones:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Elige un proveedor de cloud o VPS&lt;/strong&gt; que ofrezca Ubuntu 24.04 LTS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selecciona un plan&lt;/strong&gt; con al menos 2 GB de RAM para pruebas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configura acceso SSH con claves&lt;/strong&gt; en lugar de contraseña (más seguro)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Considera activar backups y monitoreo&lt;/strong&gt; si es para producción&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anota la IP pública&lt;/strong&gt; de tu servidor&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si usas un proveedor con firewall integrado (como Cloud Firewall de DigitalOcean, Security Groups de AWS, etc.), configura las reglas necesarias desde el panel de control.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fku8nrc9a6pjt628k29w4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fku8nrc9a6pjt628k29w4.png" alt="Servidor DitialOcean" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso 2: Configuración inicial del servidor
&lt;/h2&gt;

&lt;p&gt;Conéctate al servidor por SSH:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@TU_IP_PUBLICA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Actualizar sistema e instalar utilidades
&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;
apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl wget git ufw ca-certificates gnupg lsb-release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Crear usuario administrador (recomendado)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adduser deploy
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;deploy
rsync &lt;span class="nt"&gt;--archive&lt;/span&gt; &lt;span class="nt"&gt;--chown&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deploy:deploy ~/.ssh /home/deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahora puedes conectarte con el nuevo usuario:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh deploy@TU_IP_PUBLICA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Paso 3: Configurar firewall con UFW
&lt;/h2&gt;

&lt;p&gt;Dokploy necesita puertos específicos abiertos. Configura UFW correctamente:&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;# Permitir SSH primero para no bloquearnos&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow OpenSSH

&lt;span class="c"&gt;# Abrir puertos necesarios para Dokploy y aplicaciones&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp     &lt;span class="c"&gt;# HTTP para Traefik&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp    &lt;span class="c"&gt;# HTTPS para Traefik&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/udp    &lt;span class="c"&gt;# HTTPS UDP para Traefik&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 3000/tcp   &lt;span class="c"&gt;# Panel de Dokploy (solo temporal)&lt;/span&gt;

&lt;span class="c"&gt;# Activar firewall&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable
sudo &lt;/span&gt;ufw status numbered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Importante&lt;/strong&gt;: Docker puede saltarse reglas de UFW porque manipula &lt;code&gt;iptables&lt;/code&gt; directamente. Si tu proveedor de cloud ofrece firewall integrado (como Cloud Firewall de DigitalOcean, Security Groups de AWS, etc.), te recomendamos configurarlo también como capa adicional de seguridad.&lt;/p&gt;

&lt;p&gt;Configura las mismas reglas de firewall en el panel de tu proveedor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;22/TCP&lt;/strong&gt; desde tu IP o rango de confianza (SSH)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;80/TCP&lt;/strong&gt; desde Anywhere (HTTP)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;443/TCP&lt;/strong&gt; desde Anywhere (HTTPS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;443/UDP&lt;/strong&gt; desde Anywhere (HTTPS UDP)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3000/TCP&lt;/strong&gt; temporalmente desde tu IP (solo para configuración inicial)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F68fg2sgmhbws48kp8hqv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F68fg2sgmhbws48kp8hqv.png" alt="Firewall de servidor" width="800" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso 4: Instalar Dokploy
&lt;/h2&gt;

&lt;p&gt;La instalación oficial de Dokploy es sencilla con su script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://dokploy.com/install.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este script automáticamente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instala Docker si no está presente&lt;/li&gt;
&lt;li&gt;Inicializa Docker Swarm&lt;/li&gt;
&lt;li&gt;Crea la red overlay &lt;code&gt;dokploy-network&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Despliega servicios de Postgres, Redis y Dokploy&lt;/li&gt;
&lt;li&gt;Levanta Traefik como reverse proxy&lt;/li&gt;
&lt;li&gt;Publica el panel en el puerto 3000&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Paso 5: Verificar la instalación
&lt;/h2&gt;

&lt;p&gt;Comprueba que todo esté funcionando:&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;# Ver servicios de Docker Swarm&lt;/span&gt;
docker service &lt;span class="nb"&gt;ls&lt;/span&gt;

&lt;span class="c"&gt;# Ver contenedores en ejecución&lt;/span&gt;
docker ps

&lt;span class="c"&gt;# Verificar que los puertos estén escuchando&lt;/span&gt;
ss &lt;span class="nt"&gt;-lntup&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'(:80|:443|:3000)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8yyavlrgbn1qy9h58g8l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8yyavlrgbn1qy9h58g8l.png" alt="Dashboard Dokploy" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ahora abre tu navegador y accede al panel de Dokploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://TU_IP_PUBLICA:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Completa el asistente inicial creando tu cuenta de administrador.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13s0ydlj7piamh37mmbg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13s0ydlj7piamh37mmbg.png" alt="Home dokploy" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso 6: Configurar dominio y HTTPS (opcional pero recomendado)
&lt;/h2&gt;

&lt;p&gt;Para acceder a tus aplicaciones con dominio propio y HTTPS:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;En el panel de DNS de tu proveedor (Cloudflare, DigitalOcean, AWS Route53, etc.), añade tu dominio si no lo tienes ya.&lt;/li&gt;
&lt;li&gt;Crea registros DNS:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A&lt;/strong&gt; record para &lt;code&gt;@&lt;/code&gt; apuntando a la IP de tu servidor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A&lt;/strong&gt; record para &lt;code&gt;www&lt;/code&gt; apuntando a la IP de tu servidor&lt;/li&gt;
&lt;li&gt;O un subdominio como &lt;code&gt;dokploy.tudominio.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Espera la propagación DNS (puede tomar unos minutos).&lt;/p&gt;

&lt;h3&gt;
  
  
  Restringir acceso al puerto 3000
&lt;/h3&gt;

&lt;p&gt;Por seguridad, una vez configurado Dokploy, restringe el acceso al puerto 3000:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;En el firewall de tu proveedor de cloud, limítala solo a tu IP si necesitas acceso administrativo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Paso 7: Desplegar tu primera aplicación "Hola Mundo" en Node.js
&lt;/h2&gt;

&lt;p&gt;Vamos a crear una aplicación Node.js simple y desplegarla en Dokploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.1 Crear el proyecto localmente
&lt;/h3&gt;

&lt;p&gt;Crea una carpeta para tu proyecto y los siguientes archivos:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;package.json&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hola-mundo-dokploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Aplicación Hola Mundo para Dokploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node app.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"express"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.18.2"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;app.js&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;¡Hola Mundo desde Dokploy!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Servidor corriendo en puerto &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;&lt;strong&gt;Dockerfile&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:18-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; node&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "app.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;docker-compose.yml&lt;/strong&gt; (para referencia):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&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;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&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;NODE_ENV=staging&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7.2 Desplegar en Dokploy
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Despliegue parte 1
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foo2i64a6w4f9i3jnoix3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foo2i64a6w4f9i3jnoix3.gif" alt="Despliegue parte 1" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Despliegue parte 2
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsxwo1zm9ub2ywxa11jmz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsxwo1zm9ub2ywxa11jmz.gif" alt="Despliegue parte 2" width="720" height="406"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt;En el panel de Dokploy, haz clic en &lt;strong&gt;Create Project&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Dale un nombre a tu proyecto (ej: "Tests")&lt;/li&gt;
&lt;li&gt;En el panel de proyecto, haz click en &lt;strong&gt;Create Service&lt;/strong&gt; y selecciona Application&lt;/li&gt;
&lt;li&gt;Dale un nombre a tu aplicación: &lt;strong&gt;Hello World&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Luego accede a tu aplicación y en la sección inferior &lt;strong&gt;Build Type&lt;/strong&gt; selecciona Dockerfile y luego Save.&lt;/li&gt;
&lt;li&gt;En la parte superior cuentas con la sección &lt;strong&gt;Provider&lt;/strong&gt;. Para esta prueba seleccionaremos el tipo Drop, sin embargo para casos reales la mejor opción es a través de repositorio git.&lt;/li&gt;
&lt;li&gt;Una vez seleccionado Drop, arrastra al área &lt;strong&gt;Zip file&lt;/strong&gt; el zip con el código que especificamos en el punto 7.1.&lt;/li&gt;
&lt;li&gt;Haz clic en el botón &lt;strong&gt;Deploy&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Una vez terminado el despliegue, te mostrará la pestaña &lt;strong&gt;Deployments&lt;/strong&gt; en la cual deberás ver un punto verde y &lt;strong&gt;Done&lt;/strong&gt; que indica que el proceso fue exitoso. Puedes hacer clic en &lt;strong&gt;View&lt;/strong&gt; para verificar el proceso.&lt;/li&gt;
&lt;li&gt;Ahora, para desplegar nuestro Hola Mundo, ve a la pestaña &lt;strong&gt;Domains&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Para esta prueba usaremos un dominio gratuito de Traefik haciendo click en el icono de un par de dados, especificamos que el &lt;strong&gt;Container Port&lt;/strong&gt; es 3000 y habilitamos el HTTPS con proveedor Let's Encrypt.&lt;/li&gt;
&lt;li&gt;Luego haz clic en el botón &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Al terminar nos dará una URL a la cual podremos ingresar y ver nuestra web. Importante: como estamos usando un dominio de prueba nos dará advertencia de seguridad. Para entornos de producción usa un dominio propio.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  7.3 Verificar el despliegue
&lt;/h3&gt;

&lt;p&gt;Una vez completado el despliegue, accede a tu aplicación:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://tests-hello-world-puloud-d13d9c-167-172-as2dsaccc234-151.traefik.me/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deberías ver el mensaje JSON de "Hola Mundo".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0m7kcndy3bhchw2c4ous.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0m7kcndy3bhchw2c4ous.png" alt=" " width="784" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso 8: Buenas prácticas para producción
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. No compilar en el servidor de producción
&lt;/h3&gt;

&lt;p&gt;Dokploy recomienda construir las imágenes Docker en CI/CD (GitHub Actions, GitLab CI) y luego desplegar la imagen ya construida. Esto ahorra recursos y reduce riesgo de downtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Usar health checks
&lt;/h3&gt;

&lt;p&gt;Configura health checks en tus aplicaciones (como el endpoint &lt;code&gt;/health&lt;/code&gt; en nuestro ejemplo) para que Docker Swarm pueda monitorear el estado.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Configurar backups
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Configura backups regulares&lt;/strong&gt; de tu servidor (si tu proveedor ofrece esta funcionalidad)&lt;/li&gt;
&lt;li&gt;Configura backups de bases de datos regularmente&lt;/li&gt;
&lt;li&gt;Usa &lt;code&gt;docker volume&lt;/code&gt; para datos persistentes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Monitoreo y logs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Ver logs de Dokploy&lt;/span&gt;
docker service logs dokploy &lt;span class="nt"&gt;--tail&lt;/span&gt; 100 &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="c"&gt;# Ver logs de tu aplicación&lt;/span&gt;
docker service logs hola-mundo_app &lt;span class="nt"&gt;--tail&lt;/span&gt; 100 &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="c"&gt;# Ver estado de los servicios&lt;/span&gt;
docker service ps hola-mundo_app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Dokploy ofrece una solución robusta y auto-hospedada para el despliegue de aplicaciones en Docker Swarm. Al implementarlo en un servidor Ubuntu, obtienes una infraestructura escalable, segura y profesional a un costo accesible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Necesitas un servidor en la nube?&lt;/strong&gt; Puedes obtener un Droplet Ubuntu en DigitalOcean usando nuestro &lt;a href="https://m.do.co/c/2c579acd7121" rel="noopener noreferrer"&gt;enlace de referido&lt;/a&gt; y recibir créditos iniciales para probar este tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recursos adicionales
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.dokploy.com" rel="noopener noreferrer"&gt;Documentación oficial de Dokploy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.dokploy.com/docs/core/remote-servers/security" rel="noopener noreferrer"&gt;Guía de seguridad de Dokploy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.digitalocean.com/products/networking/firewalls/" rel="noopener noreferrer"&gt;Documentación de DigitalOcean sobre firewalls&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dokploy/dokploy" rel="noopener noreferrer"&gt;Repositorio GitHub de Dokploy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discord.gg/dokploy" rel="noopener noreferrer"&gt;Comunidad Discord de Dokploy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;¿Te gustó este tutorial?&lt;/strong&gt; Comparte tus experiencias desplegando aplicaciones con Dokploy en los comentarios o sugiere temas para futuros artículos.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>dokploy</category>
      <category>node</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Basic Load Balancing for a Web System on DigitalOcean</title>
      <dc:creator>Oscar Ricardo Sánche Gutierréz</dc:creator>
      <pubDate>Fri, 06 Mar 2026 20:43:27 +0000</pubDate>
      <link>https://dev.to/oscar_ricardosncheguti/basic-load-balancing-for-a-web-system-on-digitalocean-2hf7</link>
      <guid>https://dev.to/oscar_ricardosncheguti/basic-load-balancing-for-a-web-system-on-digitalocean-2hf7</guid>
      <description>&lt;p&gt;In this article we’ll build a &lt;strong&gt;simple and inexpensive load-balanced web setup on DigitalOcean&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The architecture is intentionally minimal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 DigitalOcean Load Balancer&lt;/li&gt;
&lt;li&gt;2 small droplets&lt;/li&gt;
&lt;li&gt;A lightweight web app running with &lt;strong&gt;Docker + PHP + Nginx&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to show how quickly you can build a &lt;strong&gt;basic scalable web entry point&lt;/strong&gt; without complex infrastructure.&lt;/p&gt;

&lt;p&gt;Architecture diagram created with &lt;strong&gt;&lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;savnet.co&lt;/a&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1yoatkmjtxd5u51jmo92.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1yoatkmjtxd5u51jmo92.gif" alt="architecture" width="962" height="690"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Creating the first server
&lt;/h1&gt;

&lt;p&gt;Go to your &lt;strong&gt;DigitalOcean account&lt;/strong&gt; and create a small droplet.&lt;/p&gt;

&lt;p&gt;Configuration used in this guide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Image: &lt;strong&gt;Docker latest on Ubuntu 22.04&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Region: choose the closest to your users&lt;/li&gt;
&lt;li&gt;Authentication: &lt;strong&gt;SSH recommended&lt;/strong&gt; (Password also works)&lt;/li&gt;
&lt;li&gt;Hostname: &lt;code&gt;WebServer-1&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2wj8xt2x7tox1kmq5683.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2wj8xt2x7tox1kmq5683.gif" alt="create-server-1" width="720" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the droplet is ready, connect to the server.&lt;/p&gt;




&lt;h1&gt;
  
  
  Preparing the server
&lt;/h1&gt;

&lt;h2&gt;
  
  
  1. Create a user
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adduser web_app
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;web_app
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker web_app
su - web_app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This user will run the application and manage Docker.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Create the application folder
&lt;/h2&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; ~/app
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Create the application files
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;index.php&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A very small script that prints the server IP responding to the request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'Hi. From ip '&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'SERVER_ADDR'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  &lt;code&gt;nginx.conf&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Basic Nginx configuration to serve PHP via PHP-FPM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;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;localhost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.php&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;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;\.php$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;fastcgi_pass&lt;/span&gt; &lt;span class="nf"&gt;127.0.0.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;fastcgi_index&lt;/span&gt; &lt;span class="s"&gt;index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;fastcgi_param&lt;/span&gt; &lt;span class="s"&gt;SCRIPT_FILENAME&lt;/span&gt; &lt;span class="nv"&gt;$document_root$fastcgi_script_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;fastcgi_params&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;h3&gt;
  
  
  &lt;code&gt;docker-compose.yml&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This setup runs &lt;strong&gt;Nginx + PHP-FPM&lt;/strong&gt; using Docker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nginx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx-web&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&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;./:/var/www/html&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./nginx.conf:/etc/nginx/conf.d/default.conf&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;php-fpm&lt;/span&gt;

  &lt;span class="na"&gt;php-fpm&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;php:8.2-fpm&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php-app&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&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;./:/var/www/html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Start the application
&lt;/h2&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;If everything is correct you should see the containers starting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ebfgjd2nbkb3i8cxmgu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ebfgjd2nbkb3i8cxmgu.png" alt="app-start" width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open the &lt;strong&gt;server IP in your browser&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You should see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hi. From ip 10.x.x.x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6q5s7v1ykntmpwgwrze0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6q5s7v1ykntmpwgwrze0.png" alt="app-running" width="446" height="275"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Creating the Load Balancer
&lt;/h1&gt;

&lt;p&gt;Now we create the &lt;strong&gt;DigitalOcean Load Balancer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1cbsc6m7jfhakj3kmpjv.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1cbsc6m7jfhakj3kmpjv.gif" alt="load-balancer" width="600" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Configure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP forwarding&lt;/li&gt;
&lt;li&gt;Attach &lt;strong&gt;WebServer-1&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once created, open the &lt;strong&gt;load balancer IP&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You’ll see something similar to this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fljl8v6qz9wpe63dxr9qz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fljl8v6qz9wpe63dxr9qz.png" alt="lb-response" width="395" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice that the response shows an &lt;strong&gt;internal server IP&lt;/strong&gt;, because traffic between the load balancer and droplets happens inside DigitalOcean’s network.&lt;/p&gt;




&lt;h1&gt;
  
  
  Creating the second server faster
&lt;/h1&gt;

&lt;p&gt;Instead of repeating the setup manually, we can &lt;strong&gt;create a snapshot&lt;/strong&gt; of the first droplet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fawsi0hnl7so42xc3ilhv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fawsi0hnl7so42xc3ilhv.png" alt="snapshot" width="800" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then create a &lt;strong&gt;new droplet from that snapshot&lt;/strong&gt; in the same region.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhaxczvla64clgpfqmr9a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhaxczvla64clgpfqmr9a.png" alt="snapshot-server" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn4ac3ds1ktk5xo3mrzh2.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn4ac3ds1ktk5xo3mrzh2.gif" alt=" " width="600" height="589"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Adding the new node to the Load Balancer
&lt;/h1&gt;

&lt;p&gt;Go back to the Load Balancer configuration and attach the second server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft44g2ezvux5imh69t98b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft44g2ezvux5imh69t98b.png" alt="attach-node" width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After about a minute, DigitalOcean will mark the droplet as &lt;strong&gt;healthy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now refresh the Load Balancer IP multiple times.&lt;/p&gt;

&lt;p&gt;You should see responses coming from &lt;strong&gt;different internal IPs&lt;/strong&gt;, meaning traffic is being distributed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpcbf7jyehw5d1934bm47.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpcbf7jyehw5d1934bm47.png" alt="node1" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5ofclo5h1h1gr4jp5lc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5ofclo5h1h1gr4jp5lc.png" alt="node2" width="616" height="327"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Security recommendations
&lt;/h1&gt;

&lt;p&gt;This example focuses on the &lt;strong&gt;basic load balancing setup&lt;/strong&gt;, but in a real environment you should also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a &lt;strong&gt;DigitalOcean firewall&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Restrict droplets so they are &lt;strong&gt;only reachable from the load balancer&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;HTTPS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Configure &lt;strong&gt;health checks&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These small changes significantly improve the security of the system.&lt;/p&gt;




&lt;h1&gt;
  
  
  Tools used
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OBS&lt;/strong&gt; – screen recording&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kdenlive&lt;/strong&gt; – video editing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://savnet.co" rel="noopener noreferrer"&gt;savnet.co&lt;/a&gt;&lt;/strong&gt; – architecture diagrams used in this article&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>beginners</category>
      <category>cloud</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
