<?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: Ivan Firestone</title>
    <description>The latest articles on DEV Community by Ivan Firestone (@ifirestone).</description>
    <link>https://dev.to/ifirestone</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F586402%2F81648520-139b-4cc1-96b0-96ed14e30d08.jpeg</url>
      <title>DEV Community: Ivan Firestone</title>
      <link>https://dev.to/ifirestone</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ifirestone"/>
    <language>en</language>
    <item>
      <title>SaaS multi-tenant en Laravel: una base de datos, un global scope y las fugas que no detecta: 02 de 08</title>
      <dc:creator>Ivan Firestone</dc:creator>
      <pubDate>Tue, 03 Nov 2026 13:00:06 +0000</pubDate>
      <link>https://dev.to/ifirestone/saas-multi-tenant-en-laravel-una-base-de-datos-un-global-scope-y-las-fugas-que-no-detecta-02-de-2nie</link>
      <guid>https://dev.to/ifirestone/saas-multi-tenant-en-laravel-una-base-de-datos-un-global-scope-y-las-fugas-que-no-detecta-02-de-2nie</guid>
      <description>&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%2Fimages.unsplash.com%2Fphoto-1563986768609-322da13575f3%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDJ8fHNhYXN8ZW58MHx8fHwxNzkyNzg2NjMwfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1563986768609-322da13575f3%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDJ8fHNhYXN8ZW58MHx8fHwxNzkyNzg2NjMwfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="SaaS multi-tenant en Laravel: una base de datos, un global scope y las fugas que no detecta: 02 de 08" width="2000" height="1333"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;blockquote&gt;Cómo aislar empresas dentro de una sola app Laravel: elegir un modelo de tenancy, resolver el tenant desde la URL, aplicarlo con un scope que falla cerrado y probar las fugas, incluidos los jobs y comandos que corren sin usuario.&lt;/blockquote&gt;
&lt;p&gt;Imagina un equipo que lanza un SaaS de facturación en tres meses. Cada consulta lleva un &lt;code&gt;where('company_id', $companyId)&lt;/code&gt;, los revisores lo buscan en cada pull request y durante medio año nada sale mal. Entonces alguien agrega un job nocturno que envía recordatorios de facturas vencidas. Lo ejecuta el scheduler, así que no hay usuario con sesión, y el helper que antes entregaba el id de la empresa devuelve null. La consulta pierde su filtro sin hacer ruido, y el job lee las facturas de todas las empresas.&lt;/p&gt;
&lt;p&gt;Nada de esa historia es exótico. Es lo que pasa cuando el aislamiento entre tenants depende de que cada desarrollador recuerde una línea, y de un contexto de petición que los jobs y los comandos no tienen.&lt;/p&gt;
&lt;p&gt;Este artículo trata de aislar empresas dentro de una sola app Laravel: qué modelo de tenancy elegir, cómo resolver la empresa actual, cómo imponerla para que olvidarla sea imposible y cómo probarlo. Cómo organizar la lógica de negocio (Actions, Data objects, Events) queda para los siguientes artículos de la serie.&lt;/p&gt;
&lt;h2 id="tres-formas-de-darle-a-cada-empresa-su-propio-espacio"&gt;Tres formas de darle a cada empresa su propio espacio&lt;/h2&gt;
&lt;p&gt;La primera decisión es dónde viven físicamente los datos de cada empresa. Hay tres respuestas habituales.&lt;/p&gt;


&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Modelo&lt;/th&gt;
&lt;th&gt;Qué separa a los tenants&lt;/th&gt;
&lt;th&gt;Qué cuesta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base de datos compartida, columna &lt;code&gt;company_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Tu código, opcionalmente respaldado por row-level security en la base de datos&lt;/td&gt;
&lt;td&gt;Lo más barato de operar: una migración, un pool de conexiones, un respaldo. El aislamiento es tan bueno como tu disciplina&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Un schema por empresa (PostgreSQL)&lt;/td&gt;
&lt;td&gt;El schema de la base de datos&lt;/td&gt;
&lt;td&gt;Una frontera real, pero cada migración corre una vez por schema y cada petición tiene que cambiar de schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Una base de datos por empresa&lt;/td&gt;
&lt;td&gt;El servidor de base de datos&lt;/td&gt;
&lt;td&gt;El aislamiento más fuerte y restauración por empresa, pero las migraciones, las conexiones y la factura crecen con cada cliente nuevo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Para un producto que todavía busca su mercado, yo empezaría con la base de datos compartida. Se parece al argumento de &lt;a href="https://blog.ifirestone.dev/modular-monolith-or-microservices-a-framework-for-deciding-without-dogma/" rel="noopener noreferrer"&gt;Modular Monolith or Microservices: A Framework for Deciding Without Dogma&lt;/a&gt; (en inglés): la opción más barata suele dar la mayor parte del beneficio, siempre que dejes abierta la salida. El precio es que el aislamiento ahora vive en tu código. El resto de este artículo trata de hacer que ese código sea difícil de esquivar.&lt;/p&gt;
&lt;h2 id="la-url-decide-qui%C3%A9n-es-el-tenant"&gt;La URL decide quién es el tenant&lt;/h2&gt;
&lt;p&gt;La siguiente pregunta: ¿cómo dice una petición a qué empresa pertenece? Un subdominio, una cabecera, la sesión, un segmento de la ruta. La opción tentadora es una columna "empresa actual" en el usuario. Cambias una vez y todas las peticiones posteriores ya lo saben. Se rompe en cuanto alguien abre dos pestañas. La columna guarda una respuesta por usuario, las pestañas piden dos, y gana la última que cambió, para ambas.&lt;/p&gt;
&lt;p&gt;Yo prefiero la ruta: &lt;code&gt;/{company-slug}/invoices&lt;/code&gt;. La URL es el único estado que pertenece a la petición misma. Sobrevive a los marcadores y a los enlaces compartidos, y pone al tenant en tus logs.&lt;/p&gt;
&lt;p&gt;Un middleware resuelve el slug, comprueba la pertenencia y abre el contexto del tenant para el resto de la petición.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ResolveCompany
{
    public function __construct(private TenantContext $tenant) {}

    public function handle(Request $request, Closure $next): Response
    {
        $user = $request-&amp;gt;user() ?? throw new AuthenticationException;

        // Los bindings aún no se han ejecutado, así que esto sigue siendo el slug crudo.
        $company = Company::where('slug', $request-&amp;gt;route('company'))-&amp;gt;first();

        abort_if(
            $company === null || ! $user-&amp;gt;companies()-&amp;gt;whereKey($company-&amp;gt;id)-&amp;gt;exists(),
            404,
        );

        return $this-&amp;gt;tenant-&amp;gt;runAs($company-&amp;gt;id, fn () =&amp;gt; $next($request));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Devuelvo 404 a quien no es miembro para que la URL no confirme que una empresa existe. Un 403 es más amable con un usuario legítimo que se equivocó al escribir, así que elige uno a conciencia.&lt;/p&gt;
&lt;p&gt;Hay una trampa, y es de orden. El route model binding ocurre en &lt;code&gt;SubstituteBindings&lt;/code&gt;, y cuando se resuelve &lt;code&gt;{invoice}&lt;/code&gt;, esa consulta pasa por los global scopes del modelo. Si el tenant todavía no está definido, el binding corre sin scope. En la &lt;a href="https://laravel.com/docs/13.x/middleware" rel="noopener noreferrer"&gt;lista de prioridad de middleware&lt;/a&gt; por defecto de Laravel, ese middleware también está antes de la autenticación, y por eso el código de arriba lee el usuario por su cuenta. Mueve el middleware del tenant antes de los bindings en &lt;code&gt;bootstrap/app.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-&amp;gt;withMiddleware(function (Middleware $middleware): void {
    $middleware-&amp;gt;prependToPriorityList(
        before: \Illuminate\Routing\Middleware\SubstituteBindings::class,
        prepend: \App\Http\Middleware\ResolveCompany::class,
    );
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y las rutas:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Route::middleware(['auth', ResolveCompany::class])
    -&amp;gt;prefix('{company}')
    -&amp;gt;group(function () {
        Route::resource('invoices', InvoiceController::class);
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El middleware llama a algo llamado &lt;code&gt;TenantContext&lt;/code&gt;. Esa es la siguiente pieza, y la que necesitará el job nocturno.&lt;/p&gt;
&lt;h2 id="un-solo-lugar-que-sabe-cu%C3%A1l-es-la-empresa-actual"&gt;Un solo lugar que sabe cuál es la empresa actual&lt;/h2&gt;
&lt;p&gt;La solución a la historia del principio empieza con una regla: nadie lee el id de la empresa desde &lt;code&gt;auth()-&amp;gt;user()&lt;/code&gt;. Todo se lo pregunta a un objeto pequeño, y quien quiera ejecutar código como una empresa tiene que decirlo en voz alta.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final class TenantContext
{
    private ?int $companyId = null;

    public function id(): ?int
    {
        return $this-&amp;gt;companyId;
    }

    public function runAs(int $companyId, Closure $callback): mixed
    {
        $previous = $this-&amp;gt;companyId;
        $this-&amp;gt;companyId = $companyId;

        try {
            return $callback();
        } finally {
            $this-&amp;gt;companyId = $previous;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Regístralo con &lt;code&gt;scoped&lt;/code&gt;, no con &lt;code&gt;singleton&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$this-&amp;gt;app-&amp;gt;scoped(TenantContext::class);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Según la &lt;a href="https://laravel.com/docs/13.x/container" rel="noopener noreferrer"&gt;documentación del container&lt;/a&gt;, las instancias scoped se descartan cuando un worker de Octane toma una petición nueva o un worker de colas toma un job nuevo. Un singleton normal arrastraría al tenant anterior a la siguiente unidad de trabajo, que es la misma fuga con otro disfraz. El bloque &lt;code&gt;finally&lt;/code&gt; es una segunda línea de defensa para el caso en que el código llame a &lt;code&gt;runAs&lt;/code&gt; y lance una excepción.&lt;/p&gt;
&lt;h2 id="un-scope-que-falla-cerrado"&gt;Un scope que falla cerrado&lt;/h2&gt;
&lt;p&gt;Ahora la parte que todos escriben primero. Un &lt;a href="https://laravel.com/docs/13.x/eloquent" rel="noopener noreferrer"&gt;global scope&lt;/a&gt; filtra todas las consultas de un modelo, y un trait puede engancharlo y rellenar &lt;code&gt;company_id&lt;/code&gt; al crear. La diferencia con la versión habitual está en qué pasa cuando no hay empresa: lanza una excepción en lugar de omitir el filtro (&lt;em&gt;fail closed&lt;/em&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $companyId = app(TenantContext::class)-&amp;gt;id()
            ?? throw new MissingTenantContext($model::class);

        $builder-&amp;gt;where($model-&amp;gt;qualifyColumn('company_id'), $companyId);
    }
}

trait BelongsToCompany
{
    public static function bootBelongsToCompany(): void
    {
        static::addGlobalScope(new TenantScope);

        static::creating(function (Model $model): void {
            $model-&amp;gt;company_id ??= app(TenantContext::class)-&amp;gt;id()
                ?? throw new MissingTenantContext($model::class);
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Acóplalo a una migración donde la columna no pueda ser nula y la empresa vaya primero en los índices, ya que casi todas las consultas filtran por ella:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$table-&amp;gt;foreignId('company_id')-&amp;gt;constrained();
$table-&amp;gt;string('number');
$table-&amp;gt;unique(['company_id', 'number']);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El costo de fallar cerrado es real. Las pantallas de administración, los seeders, los reportes entre todas las empresas y cualquier otra cosa que legítimamente necesite datos sin scope tienen que decirlo con &lt;code&gt;Invoice::withoutGlobalScope(TenantScope::class)&lt;/code&gt;. Yo lo cuento como una ventaja. Cada consulta sin scope es ahora una línea que puedes buscar con grep en la revisión de código, y la lista debería mantenerse corta.&lt;/p&gt;
&lt;p&gt;Un scope también tiene puntos ciegos, y conviene conocerlos antes de confiar en él:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DB::table('invoices')&lt;/code&gt; y el SQL crudo nunca ven los scopes de Eloquent.&lt;/li&gt;
&lt;li&gt;Las reglas de validación &lt;code&gt;unique&lt;/code&gt; y &lt;code&gt;exists&lt;/code&gt; consultan la tabla directamente, así que ignoran el scope. Agrega la condición tú mismo: &lt;code&gt;Rule::unique('invoices', 'number')-&amp;gt;where('company_id', $companyId)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Cualquier cosa que cruce un modelo que no use el trait es tan segura como ese otro modelo.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="el-job-nocturno-otra-vez"&gt;El job nocturno, otra vez&lt;/h2&gt;
&lt;p&gt;Volvamos al job de recordatorios. Con las reglas nuevas no puede correr como estaba escrito, porque nada define la empresa. Así que el job recibe la empresa de forma explícita y un middleware de job abre el contexto a su alrededor. El middleware de jobs, descrito en la &lt;a href="https://laravel.com/docs/13.x/queues" rel="noopener noreferrer"&gt;documentación de colas&lt;/a&gt;, envuelve al job igual que el middleware de rutas envuelve una petición.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class RunsAsCompany
{
    public function handle(object $job, Closure $next): void
    {
        app(TenantContext::class)-&amp;gt;runAs($job-&amp;gt;companyId, fn () =&amp;gt; $next($job));
    }
}

class SendOverdueReminders implements ShouldQueue
{
    use Queueable;

    public function __construct(public int $companyId) {}

    public function middleware(): array
    {
        return [new RunsAsCompany];
    }

    public function handle(): void
    {
        // Invoice::query() ahora está filtrada a $this-&amp;gt;companyId
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El scheduler reparte un job por empresa:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Schedule::call(function () {
    Company::query()-&amp;gt;each(
        fn (Company $company) =&amp;gt; SendOverdueReminders::dispatch($company-&amp;gt;id)
    );
})-&amp;gt;dailyAt('02:00');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tiene un costo: N jobs en lugar de uno. A cambio, una empresa lenta o que falla no bloquea a las demás, los reintentos se quedan dentro de un solo tenant y el tenant es visible en el payload en lugar de esconderse en estado ambiental. &lt;code&gt;Company&lt;/code&gt; en sí no usa el trait, porque es el tenant.&lt;/p&gt;
&lt;h2 id="empresas-usuarios-y-roles"&gt;Empresas, usuarios y roles&lt;/h2&gt;
&lt;p&gt;En un producto multiempresa, una persona suele pertenecer a varias empresas. Eso decide dónde vive el rol. Pertenece a la membresía, una tabla pivote &lt;code&gt;company_user&lt;/code&gt; con una columna &lt;code&gt;role&lt;/code&gt;, y no al usuario. La misma persona puede ser admin en una empresa y viewer en otra, y el middleware ya sabe de qué empresa trata la petición.&lt;/p&gt;
&lt;p&gt;La tentación es darle un rango a los roles y comparar números. Funciona durante un tiempo. Los permisos como cadenas simples por rol (&lt;code&gt;invoice:create&lt;/code&gt;, &lt;code&gt;invoice:void&lt;/code&gt;) envejecen mejor, porque el día que alguien pida un rol que pueda anular facturas pero no editarlas, agregas una fila en lugar de reordenar una jerarquía. Comprueba los permisos con las policies de Laravel, y pasa el rol de la membresía, no una marca a nivel de usuario.&lt;/p&gt;
&lt;h2 id="demu%C3%A9stralo-con-un-test-que-intenta-filtrar"&gt;Demuéstralo con un test que intenta filtrar&lt;/h2&gt;
&lt;p&gt;Un scope que no has atacado es un scope que esperas que funcione. Tres tests cubren la historia hasta aquí, escritos con Pest:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;it('nunca devuelve facturas de otra empresa', function () {
    $acme = Company::factory()-&amp;gt;create();
    $globex = Company::factory()-&amp;gt;create();
    $tenant = app(TenantContext::class);

    $tenant-&amp;gt;runAs($globex-&amp;gt;id, fn () =&amp;gt; Invoice::factory()-&amp;gt;create());

    expect($tenant-&amp;gt;runAs($acme-&amp;gt;id, fn () =&amp;gt; Invoice::count()))-&amp;gt;toBe(0);
});

it('se niega a consultar sin tenant', function () {
    Invoice::count();
})-&amp;gt;throws(MissingTenantContext::class);

it('devuelve 404 para una empresa a la que el usuario no pertenece', function () {
    $other = Company::factory()-&amp;gt;create();

    $this-&amp;gt;actingAs(User::factory()-&amp;gt;create())
        -&amp;gt;get("/{$other-&amp;gt;slug}/invoices")
        -&amp;gt;assertNotFound();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El segundo test es el que habría atrapado la historia del principio. Cuando estos tests existan, agrega una guarda que liste todas las tablas con una columna &lt;code&gt;company_id&lt;/code&gt; y falle si su modelo no usa el trait. Las tablas nuevas son donde nacen las fugas.&lt;/p&gt;
&lt;h2 id="d%C3%B3nde-deja-de-funcionar-el-schema-compartido"&gt;Dónde deja de funcionar el schema compartido&lt;/h2&gt;
&lt;p&gt;Este diseño tiene límites, y es mejor verlos pronto. Un solo tenant grande puede dominar los índices y el pool de conexiones, así que los vecinos ruidosos son una posibilidad real. Restaurar una empresa desde un respaldo significa extraer sus filas de un respaldo de todos. Y algunos clientes, sobre todo los enterprise o regulados, pedirán su propia base de datos por muy bueno que sea tu scope.&lt;/p&gt;
&lt;p&gt;El diseño también te deja una salida. Como todas las consultas pasan por un solo contexto y un solo trait, mover a un cliente grande a su propia base de datos más adelante es un refactor mucho más pequeño que en un código lleno de cláusulas &lt;code&gt;where&lt;/code&gt; escritas a mano. Ese es el argumento para hacerlo así aunque nunca necesites la salida.&lt;/p&gt;
&lt;h2 id="conclusi%C3%B3n"&gt;Conclusión&lt;/h2&gt;
&lt;p&gt;El job nocturno que envió correos a los clientes equivocados no necesitaba un filtro más ingenioso. Necesitaba un sistema donde correr sin empresa sea un error, y donde ese error aparezca en un test mucho antes de que lo vea un cliente. Resuelve el tenant desde la URL, guárdalo en un solo lugar, haz que el scope falle cerrado e intenta filtrar a propósito.&lt;/p&gt;
&lt;p&gt;Si quieres una acción para hoy, busca con grep en tu código &lt;code&gt;auth()-&amp;gt;user()-&amp;gt;company_id&lt;/code&gt; y &lt;code&gt;withoutGlobalScopes&lt;/code&gt;, y escribe el test "se niega a consultar sin tenant". En el siguiente artículo dejamos atrás el aislamiento y vemos dónde debe vivir la lógica de negocio para que los controladores se mantengan delgados.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>laravel</category>
      <category>saas</category>
    </item>
    <item>
      <title>Building a Multi-Tenant SaaS in Laravel: The Series I Wish I Had Read First: 01 of 08</title>
      <dc:creator>Ivan Firestone</dc:creator>
      <pubDate>Tue, 03 Nov 2026 01:00:07 +0000</pubDate>
      <link>https://dev.to/ifirestone/building-a-multi-tenant-saas-in-laravel-the-series-i-wish-i-had-read-first-01-of-08-die</link>
      <guid>https://dev.to/ifirestone/building-a-multi-tenant-saas-in-laravel-the-series-i-wish-i-had-read-first-01-of-08-die</guid>
      <description>&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%2Fimages.unsplash.com%2Fphoto-1556155092-490a1ba16284%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fHNhYXN8ZW58MHx8fHwxNzkyNzkxMTIxfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1556155092-490a1ba16284%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fHNhYXN8ZW58MHx8fHwxNzkyNzkxMTIxfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="Building a Multi-Tenant SaaS in Laravel: The Series I Wish I Had Read First: 01 of 08" width="2000" height="1333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the article I would have wanted to read before I started building a multi-tenant system. It would have saved me time, a lot of rewritten code, and many hours migrating systems that were already running.&lt;/p&gt;
&lt;p&gt;Most of the expensive decisions in a multi-tenant SaaS are cheap on day one and costly on day four hundred. Where the tenant lives in the schema, how a request finds its company, what a background job knows about who it works for: you can get each of them right in an afternoon at the start, or fix them later with a migration, a rewrite and a nervous deploy. I know the second path because I have walked it. This series is what I would do on the first day if I started again, knowing what I know now.&lt;/p&gt;
&lt;p&gt;Here is one of those paths. I started multi-tenant, but not multi-user. Adding multi-user later wasn't a feature. It was a rewrite that touched the whole system. And the guards that kept one tenant's data away from another weren't built as well as they should have been. Both problems sit at the foundation, which is why the first article of the series deals with who belongs to which company and how a request finds out.&lt;/p&gt;
&lt;h2 id="what-this-series-is"&gt;What This Series Is&lt;/h2&gt;
&lt;p&gt;It is a set of decisions with reasons attached, written for developers who already build with Laravel and want to design and deliver a multi-tenant SaaS without learning the hard parts the slow way. Every article follows the same fictional product, an invoicing app where the tenant is a &lt;code&gt;Company&lt;/code&gt;, so the examples connect from one article to the next.&lt;/p&gt;
&lt;p&gt;It is not a package guide, and it is not a framework. You won't find a library to install that solves tenancy for you. You will find the pieces I think are worth building yourself, why, and what each one costs. Where a decision is a judgment call, I say so.&lt;/p&gt;
&lt;h2 id="what-it-assumes"&gt;What It Assumes&lt;/h2&gt;
&lt;p&gt;A few choices are fixed so the series can go deep instead of wide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One Laravel application and one database&lt;/strong&gt;, with a &lt;code&gt;company_id&lt;/code&gt; column on tenant-owned tables. If you are still deciding between a modular monolith and microservices, that is a different question, and I wrote about how to approach it in &lt;a href="https://dev.to/ifirestone/modular-monolith-or-microservices-a-framework-for-deciding-without-dogma-4ppa"&gt;Modular Monolith or Microservices: A Framework for Deciding Without Dogma&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laravel 13 and PostgreSQL&lt;/strong&gt; in the examples, although most of it applies to other versions and to MySQL. The one article that depends on PostgreSQL says so up front.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A reader with some experience.&lt;/strong&gt; I don't explain what a middleware or a queue is.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What it leaves out on purpose: a separate database per tenant, single sign-on, and the details of any specific payment provider. Each is worth its own series, and none of them is where a first version usually hurts.&lt;/p&gt;
&lt;h2 id="what-youll-find"&gt;What You'll Find&lt;/h2&gt;
&lt;p&gt;These are the articles in the order I would read them. Each one stands on its own once you know the tenant context from the first. I update this list as the series is published: a title with a link is already out, and a title without one is still to come.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Tenant SaaS in Laravel: One Database, a Global Scope, and the Leaks It Doesn't Catch.&lt;/strong&gt; The foundation: how a request finds its company, a scope that fails closed, and what the scope still misses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Killing Fat Controllers in Laravel: Actions as the Home for Business Logic.&lt;/strong&gt; Where the rules live, so a controller, a command and an API share one implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Objects, Concerns and Events: The Supporting Cast of a Laravel Action.&lt;/strong&gt; What feeds an Action, what it shares and what it announces, with the tenant context carried into queued listeners.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From Idea to First Paying Customer: A Delivery Roadmap for a Multi-Tenant Laravel SaaS.&lt;/strong&gt; The order of work: walls before rooms, one slice early, plan limits, billing webhooks and safe deploys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing Tenant Isolation in Laravel: Leak Tests, Guard Tests and What They Still Miss.&lt;/strong&gt; How to prove, on every commit, that one company can't see another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability Per Tenant in Laravel: Logs, Metrics and Errors by Company.&lt;/strong&gt; How to answer "what happened to this company?" without drowning in data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row-Level Security in PostgreSQL: A Second Wall for a Multi-Tenant Laravel App.&lt;/strong&gt; An optional database-level layer behind the application scope, and the cases where it is worth its cost.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="how-to-read-it"&gt;How to Read It&lt;/h2&gt;
&lt;p&gt;Two habits will help. First, treat the code as a starting point. The snippets are small on purpose, and I haven't run them against your schema, your policies or your deploy setup. Second, pay attention to the cost sections. Every pattern here has a price, and the honest question is not "is this good practice?" but "is it worth it for my team, at my size, this year?"&lt;/p&gt;
&lt;p&gt;Sometimes the answer is no. A pass-through Action, a Data object for two arguments or a database policy for a prototype are all ways to spend effort you needed elsewhere.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I'm writing this series because the most useful advice I can give my past self is also the advice most teams hear too late: decide the foundations early, make them testable, and keep the rest boring. If you are about to start a multi-tenant product, or you are early enough that the foundations can still change, begin with the tenant context. Everything else in the series is built on it.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>laravel</category>
      <category>saas</category>
    </item>
    <item>
      <title>Migrar de un sistema legacy a microservicios sin documentación ni control total</title>
      <dc:creator>Ivan Firestone</dc:creator>
      <pubDate>Sat, 31 Oct 2026 13:00:07 +0000</pubDate>
      <link>https://dev.to/ifirestone/migrar-de-un-sistema-legacy-a-microservicios-sin-documentacion-ni-control-total-346o</link>
      <guid>https://dev.to/ifirestone/migrar-de-un-sistema-legacy-a-microservicios-sin-documentacion-ni-control-total-346o</guid>
      <description>&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%2Fimages.unsplash.com%2Fphoto-1674027001834-719c347d1eca%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDE1M3x8bWlncmF0ZSUyMHNvZnR3YXJlfGVufDB8fHx8MTc5MjgwOTE3NHww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1674027001834-719c347d1eca%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDE1M3x8bWlncmF0ZSUyMHNvZnR3YXJlfGVufDB8fHx8MTc5MjgwOTE3NHww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="Migrar de un sistema legacy a microservicios sin documentación ni control total" width="2000" height="1125"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;blockquote&gt;Cómo reemplazar un sistema legacy que no puedes leer del todo, cambiar ni apagar: aprende primero su comportamiento real, rodéalo con una fachada strangler y demuestra que los nuevos servicios igualan a los viejos antes de hacer el corte.&lt;/blockquote&gt;
&lt;p&gt;Imagina una empresa cuya tienda en línea lleva doce años funcionando sobre una sola aplicación. Los autores originales se fueron hace mucho. El wiki describe una versión del sistema que ya no existe. Un módulo lo escribió un proveedor y llega como una librería compilada de la que nadie tiene el código fuente. Y aun así la tienda procesa pedidos todos los días, y el negocio repite lo mismo que dijo el año pasado: "funciona, no lo rompas".&lt;/p&gt;
&lt;p&gt;Entonces alguien escribe "migrar a microservicios" en el roadmap.&lt;/p&gt;
&lt;p&gt;Este artículo sigue a esa tienda hipotética por las partes de una migración que los diagramas habituales se saltan: no saber qué hace realmente el sistema, no tener permiso para cambiarlo y tener que superar algo que lleva años probado en batalla. Si todavía estás decidiendo si los microservicios son el destino correcto, empieza por &lt;a href="https://blog.ifirestone.dev/arquitectura-de-microservicios/" rel="noreferrer noopener"&gt;Arquitectura Microservicios&lt;/a&gt;  y &lt;a href="https://blog.ifirestone.dev/monolito-modular-o-microservicios-un-marco-para-decidir-sin-dogmas/" rel="noreferrer noopener"&gt;Monolito modular o microservicios: un marco para decidir sin dogmas&lt;/a&gt;. Aquí damos la decisión por tomada y preguntamos cómo sobrevivirla.&lt;/p&gt;
&lt;h2 id="la-reescritura-que-parec%C3%ADa-un-plan"&gt;La reescritura que parecía un plan&lt;/h2&gt;
&lt;p&gt;El primer instinto es empezar de cero: construir el sistema nuevo al lado del viejo y cambiar cuando esté listo. Tienta porque promete dejar atrás el desorden.&lt;/p&gt;
&lt;p&gt;Falla por una razón que no tiene nada que ver con la calidad del código. Mientras se construye el sistema nuevo, el viejo sigue cambiando: correcciones de errores, reglas de precios, un ajuste regulatorio. El objetivo se mueve. El equipo termina manteniendo dos sistemas sin terminar ninguno, y el cambio se convierte en un solo evento grande e irreversible que no se pudo probar con tráfico real.&lt;/p&gt;
&lt;p&gt;La alternativa tiene nombre: la &lt;strong&gt;higuera estranguladora&lt;/strong&gt; (&lt;em&gt;strangler fig&lt;/em&gt;), descrita por Martin Fowler en &lt;a href="https://martinfowler.com/bliki/StranglerFigApplication.html" rel="noopener noreferrer"&gt;Strangler Fig &lt;/a&gt; y documentada en el &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/strangler-fig" rel="noopener noreferrer"&gt;Azure Architecture Center&lt;/a&gt;. Como la enredadera que crece alrededor de un árbol, el sistema nuevo envuelve al viejo y lo reemplaza pieza por pieza, mientras el viejo sigue atendiendo a los usuarios. Cada paso es pequeño y reversible. Esa propiedad es la que hace posible todo lo demás en este artículo.&lt;/p&gt;
&lt;p&gt;Pero un strangler necesita saber dónde cortar. Y para eso, la tienda primero tiene que responder una pregunta incómoda.&lt;/p&gt;
&lt;h2 id="%C2%BFqu%C3%A9-hace-realmente-el-sistema-viejo"&gt;¿Qué hace realmente el sistema viejo?&lt;/h2&gt;
&lt;p&gt;Sin documentación confiable, la fuente de verdad es el sistema en ejecución. El código te dice lo que puede hacer; producción te dice lo que hace. No son la misma lista.&lt;/p&gt;
&lt;p&gt;El equipo empieza escuchando, más o menos en este orden:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capturar tráfico real.&lt;/strong&gt; Los logs de acceso, los del API gateway o un proxy delante de la aplicación legacy muestran qué endpoints se llaman, con qué frecuencia y con qué payloads. Los endpoints que nadie llama desde hace un año son candidatos a eliminarse, no a migrarse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leer la base de datos como un contrato.&lt;/strong&gt; Las tablas, las llaves foráneas, los triggers y los procedimientos almacenados suelen guardar reglas de negocio que nunca llegaron al código de la aplicación. Un trigger que descuenta stock cuando se inserta un pedido es una funcionalidad, aunque ningún desarrollador recuerde haberlo escrito.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Listar los consumidores ocultos.&lt;/strong&gt; Jobs nocturnos, hojas de cálculo que consultan la base de datos directamente, un socio que llama a un endpoint no documentado. Son los clientes que se rompen en silencio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escribir pruebas de caracterización.&lt;/strong&gt; Michael Feathers llama &lt;em&gt;characterization tests&lt;/em&gt; (en &lt;em&gt;Working Effectively with Legacy Code&lt;/em&gt;) a las pruebas que registran lo que el código hace hoy, bien o mal. Envías entradas conocidas, guardas las salidas, y desde entonces cualquier cambio de comportamiento se hace visible.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;Las pruebas de caracterización no dicen que el comportamiento legacy sea correcto. Dicen que es el comportamiento del que dependen hoy tus clientes. Si una rareza es un error o una funcionalidad es una decisión de negocio, y es mucho más barato tomarla antes de la migración que después.&lt;/blockquote&gt;
&lt;p&gt;Al final de esta fase la tienda tiene algo que nunca tuvo: una descripción escrita y ejecutable de su propio sistema. Solo ahora puede elegir qué extraer primero.&lt;/p&gt;
&lt;h2 id="cortar-donde-no-puedes-editar"&gt;Cortar donde no puedes editar&lt;/h2&gt;
&lt;p&gt;Aquí aparece el segundo problema. Supón que la tienda quiere mover los pagos a su propio servicio. El paso obvio es cambiar el código legacy para que llame al servicio nuevo en lugar de a su módulo interno de pagos. Pero el equipo no puede: el módulo es un binario del proveedor, o el proceso de release de la aplicación legacy tarda meses, o nadie se atreve a tocar el archivo.&lt;/p&gt;
&lt;p&gt;Cuando no puedes cambiar el interior, trabajas por fuera. Hay tres costuras que no requieren editar el código legacy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;El borde.&lt;/strong&gt; Pon una fachada, como un reverse proxy o un API gateway, delante de la aplicación legacy. Al principio reenvía todo. Luego, ruta por ruta, envía peticiones específicas al servicio nuevo. Los usuarios ven las mismas URLs; la fachada decide quién responde.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Los datos.&lt;/strong&gt; Si el servicio nuevo necesita datos que son de la base de datos legacy, no la consultes directamente desde el servicio nuevo. Captura los cambios con &lt;em&gt;change data capture&lt;/em&gt; o una exportación estilo outbox y aliméntalos al servicio nuevo, para que el esquema legacy siga siendo un detalle de implementación.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La capa de traducción.&lt;/strong&gt; El sistema legacy habla su propio dialecto: códigos de estado raros, un registro de cliente con cuarenta columnas, fechas guardadas como texto. Una &lt;strong&gt;capa anticorrupción&lt;/strong&gt; (&lt;em&gt;anti-corruption layer&lt;/em&gt;, un término de Domain-Driven Design) traduce entre ese dialecto y el modelo limpio del servicio nuevo. Sin ella, el diseño legacy se filtra a cada servicio nuevo y acabas reconstruyendo el sistema viejo en piezas más pequeñas.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# Reglas ilustrativas de gateway: la fachada es dueña de la decisión de enrutamiento&lt;br&gt;
routes:

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;code&gt;&lt;br&gt;
&lt;li&gt;path: /api/payments/**
target: payments-service   # nuevo&lt;/li&gt;
&lt;br&gt;
&lt;/code&gt;&lt;li&gt;&lt;code&gt;path: /**&lt;br&gt;
target: legacy-app         # todo lo demás, sin cambios&lt;br&gt;
&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La fachada también le da a la tienda su red de seguridad. Si el servicio nuevo de pagos se comporta mal, volver atrás es un cambio de enrutamiento, no un despliegue.&lt;/p&gt;
&lt;p&gt;El primer servicio ya corre detrás de la fachada. La siguiente pregunta es la que le importa al negocio: ¿es tan bueno como lo que reemplazó?&lt;/p&gt;
&lt;h2 id="superar-a-un-sistema-con-doce-a%C3%B1os-de-pruebas"&gt;Superar a un sistema con doce años de pruebas&lt;/h2&gt;
&lt;p&gt;Los sistemas legacy tienen una ventaja fácil de subestimar: los ha golpeado cada pedido extraño, cada tormenta de reintentos, cada reporte de fin de mes. El servicio nuevo, en comparación, no ha sido probado en el mundo real. "Código más limpio" no es un argumento que sobreviva al primer incidente.&lt;/p&gt;
&lt;p&gt;Por eso la tienda trata la paridad como un requisito con evidencia, en cuatro pasos:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Medir primero la línea base del legacy.&lt;/strong&gt; Percentiles de latencia, tasa de errores, throughput en el pico y el costo de operarlo. No puedes afirmar que igualas algo que nunca mediste, y un monolito viejo en una red local suele ser más rápido por petición que un sistema distribuido con saltos de red.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Definir "al menos igual de bueno" por capacidad.&lt;/strong&gt; En pagos importan más la corrección y la idempotencia que la velocidad pura. En la búsqueda de productos importa más la latencia. Escríbelo como objetivos de nivel de servicio (SLO) antes de construir.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correr en modo sombra.&lt;/strong&gt; La fachada envía cada petición real al sistema legacy, que responde al usuario, y una copia al servicio nuevo, cuya respuesta solo se registra. Compara las dos respuestas. Las diferencias son errores del servicio nuevo o comportamiento no documentado del viejo, y ambos conviene encontrarlos antes de que afecten a un cliente.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mover el tráfico gradualmente.&lt;/strong&gt; Usa feature flags o enrutamiento ponderado: una parte pequeña del tráfico, luego más, vigilando los SLO en cada paso. El camino legacy se mantiene vivo hasta que el nuevo haya aguantado un ciclo de negocio completo, incluidos los momentos incómodos como el cierre de mes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;El modo sombra tiene costos que conviene decir. Las peticiones duplicadas no deben duplicar efectos secundarios, así que una llamada de pago en sombra necesita un modo "dry run" o un sandbox. Y necesitas observabilidad real en ambos lados antes de que nada de esto funcione: trazas, métricas y logs que puedas correlacionar.&lt;/p&gt;
&lt;h2 id="lo-que-esto-te-cuesta"&gt;Lo que esto te cuesta&lt;/h2&gt;
&lt;p&gt;Una migración así es más lenta e incómoda que una reescritura en el papel. Sé honesto con la factura:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dos sistemas a la vez.&lt;/strong&gt; Durante un buen tiempo operas, parchas y aseguras ambos. El sistema legacy no se abarata mientras lo reemplazas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Una fachada de apariencia permanente.&lt;/strong&gt; El gateway y la capa anticorrupción son componentes extra que hay que operar. Algunos sobrevivirán a la migración.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La complejidad de los sistemas distribuidos&lt;/strong&gt; llega con el primer servicio, como se describe en &lt;a href="https://blog.ifirestone.dev/arquitectura-de-microservicios/" rel="noreferrer noopener"&gt;Arquitectura Microservicios&lt;/a&gt; : fallos de red, versionado, monitoreo entre servicios.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No todo debe moverse.&lt;/strong&gt; Algunos módulos son estables, cambian poco y no tienen problemas de escala. Dejarlos en el núcleo legacy, detrás de la fachada, puede ser el estado final correcto.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Depende del sistema, pero un resultado común es un núcleo legacy más pequeño que se queda, rodeado de un puñado de servicios que se ganaron su independencia.&lt;/p&gt;
&lt;h2 id="conclusi%C3%B3n"&gt;Conclusión&lt;/h2&gt;
&lt;p&gt;La tienda del principio creía que su problema era la tecnología vieja. Los problemas más difíciles resultaron ser el conocimiento que faltaba, el acceso restringido y una vara de prueba muy alta. Cada uno tiene una respuesta práctica: aprender el comportamiento real desde producción, trabajar por el borde, los datos y una capa de traducción en lugar de editar lo intocable, y tratar la paridad con el sistema legacy como algo que se mide, no que se supone.&lt;/p&gt;
&lt;p&gt;Si enfrentas una migración así, un primer paso concreto es pequeño: pon un proxy delante de la aplicación legacy, reenvía todo y empieza a leer los logs. En una semana aprenderás más sobre tu sistema que lo que el wiki te ha dicho en años.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>microservices</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Construir un SaaS multi-tenant en Laravel: la serie que me habría gustado leer primero: 01 de 08</title>
      <dc:creator>Ivan Firestone</dc:creator>
      <pubDate>Fri, 30 Oct 2026 13:00:07 +0000</pubDate>
      <link>https://dev.to/ifirestone/construir-un-saas-multi-tenant-en-laravel-la-serie-que-me-habria-gustado-leer-primero-01-de-08-1k1f</link>
      <guid>https://dev.to/ifirestone/construir-un-saas-multi-tenant-en-laravel-la-serie-que-me-habria-gustado-leer-primero-01-de-08-1k1f</guid>
      <description>&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%2Fimages.unsplash.com%2Fphoto-1559526324-593bc073d938%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDEzfHxzYWFzfGVufDB8fHx8MTc5Mjc4NjYzMHww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1559526324-593bc073d938%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDEzfHxzYWFzfGVufDB8fHx8MTc5Mjc4NjYzMHww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="Construir un SaaS multi-tenant en Laravel: la serie que me habría gustado leer primero: 01 de 08" width="2000" height="1333"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;blockquote&gt;Por qué escribo una serie sobre diseñar y entregar un SaaS multi-tenant en Laravel: qué haría distinto desde el primer día, qué cubre la serie, a quién va dirigida y cómo leerla.&lt;/blockquote&gt;
&lt;p&gt;Este es el artículo que habría querido leer antes de empezar a construir un sistema multi-tenant. Me habría ahorrado tiempo, mucho código reescrito y muchas horas migrando sistemas que ya estaban en marcha.&lt;/p&gt;
&lt;p&gt;La mayoría de las decisiones caras en un SaaS multi-tenant son baratas el día uno y costosas el día cuatrocientos. Dónde vive el tenant en el schema, cómo una petición encuentra su empresa, qué sabe un job en segundo plano sobre para quién trabaja: cada una se puede resolver bien en una tarde al principio, o arreglar después con una migración, una reescritura y un deploy con los nervios de punta. Conozco el segundo camino porque lo he recorrido. Esta serie es lo que haría el primer día si empezara de nuevo, sabiendo lo que sé ahora.&lt;/p&gt;
&lt;p&gt;Aquí va uno de esos caminos. Empecé multi-tenant, pero no multiusuario. Añadir el multiusuario después no fue una funcionalidad. Fue una reescritura que tocó todo el sistema. Y las guardas que mantienen los datos de un tenant lejos de otro no estaban tan bien construidas como debían. Los dos problemas están en los cimientos, y por eso el primer artículo de la serie trata de quién pertenece a qué empresa y cómo lo descubre una petición.&lt;/p&gt;
&lt;h2 id="qu%C3%A9-es-esta-serie"&gt;Qué es esta serie&lt;/h2&gt;
&lt;p&gt;Es un conjunto de decisiones con sus razones adjuntas, escrito para desarrolladores que ya construyen con Laravel y quieren diseñar y entregar un SaaS multi-tenant sin aprender las partes difíciles por el camino lento. Cada artículo sigue el mismo producto ficticio, una app de facturación donde el tenant es una &lt;code&gt;Company&lt;/code&gt;, de modo que los ejemplos conectan de uno a otro.&lt;/p&gt;
&lt;p&gt;No es una guía de paquetes, y no es un framework. No encontrarás una librería que instalar que te resuelva el tenancy. Encontrarás las piezas que creo que vale la pena construir tú mismo, por qué, y qué cuesta cada una. Donde una decisión es cuestión de criterio, lo digo.&lt;/p&gt;
&lt;h2 id="qu%C3%A9-asume"&gt;Qué asume&lt;/h2&gt;
&lt;p&gt;Unas pocas elecciones están fijadas para que la serie pueda ir en profundidad y no en extensión:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Una aplicación Laravel y una base de datos&lt;/strong&gt;, con una columna &lt;code&gt;company_id&lt;/code&gt; en las tablas que pertenecen a un tenant. Si todavía decides entre un monolito modular y microservicios, esa es otra pregunta, y escribí sobre cómo abordarla en &lt;a href="https://blog.ifirestone.dev/monolito-modular-o-microservicios-un-marco-para-decidir-sin-dogmas/" rel="noreferrer noopener"&gt;Monolito modular o microservicios: un marco para decidir sin dogmas.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laravel 13 y PostgreSQL&lt;/strong&gt; en los ejemplos, aunque casi todo aplica a otras versiones y a MySQL. El único artículo que depende de PostgreSQL lo dice desde el principio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Un lector con algo de experiencia.&lt;/strong&gt; No explico qué es un middleware ni una cola.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lo que se deja fuera a propósito: una base de datos separada por tenant, el single sign-on y los detalles de cualquier proveedor de pagos concreto. Cada uno merece su propia serie, y ninguno es donde suele doler una primera versión.&lt;/p&gt;
&lt;h2 id="qu%C3%A9-encontrar%C3%A1s"&gt;Qué encontrarás&lt;/h2&gt;
&lt;p&gt;Estos son los artículos en el orden en que yo los leería. Cada uno se sostiene solo una vez que conoces el contexto del tenant del primero. Actualizo esta lista a medida que se publica la serie: un título con enlace ya está publicado, y un título sin enlace está por llegar.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SaaS multi-tenant en Laravel: una base de datos, un global scope y las fugas que no detecta.&lt;/strong&gt; El cimiento: cómo una petición encuentra su empresa, un scope que falla cerrado y lo que el scope todavía no ve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adiós a los controladores gordos en Laravel: las Actions como hogar de la lógica de negocio.&lt;/strong&gt; Dónde viven las reglas, para que un controlador, un comando y una API compartan una sola implementación.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data objects, Concerns y Events: el reparto de apoyo de una Action en Laravel.&lt;/strong&gt; Qué alimenta a una Action, qué comparte y qué anuncia, con el contexto de tenant llevado a los listeners en cola.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;De la idea al primer cliente de pago: una hoja de ruta de entrega para un SaaS multi-tenant en Laravel.&lt;/strong&gt; El orden del trabajo: muros antes que habitaciones, un slice temprano, límites de plan, webhooks de facturación y deploys seguros.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probar el aislamiento entre tenants en Laravel: tests de fuga, tests de guarda y lo que aún no detectan.&lt;/strong&gt; Cómo demostrar, en cada commit, que una empresa no puede ver a otra.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observabilidad por tenant en Laravel: logs, métricas y errores por empresa.&lt;/strong&gt; Cómo responder "¿qué le pasó a esta empresa?" sin ahogarte en datos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row-Level Security en PostgreSQL: un segundo muro para una app Laravel multi-tenant.&lt;/strong&gt; Una capa opcional a nivel de base de datos detrás del scope de la aplicación, y los casos en que vale su costo.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="c%C3%B3mo-leerla"&gt;Cómo leerla&lt;/h2&gt;
&lt;p&gt;Dos hábitos ayudarán. Primero, trata el código como un punto de partida. Los fragmentos son pequeños a propósito, y no los he ejecutado contra tu schema, tus policies ni tu configuración de deploy. Segundo, presta atención a las secciones de costos. Cada patrón de aquí tiene un precio, y la pregunta honesta no es "¿es esto una buena práctica?" sino "¿vale la pena para mi equipo, a mi tamaño, este año?".&lt;/p&gt;
&lt;p&gt;A veces la respuesta es que no. Una Action de paso, un Data object para dos argumentos o una política de base de datos para un prototipo son formas de gastar un esfuerzo que necesitabas en otro sitio.&lt;/p&gt;
&lt;h2 id="conclusi%C3%B3n"&gt;Conclusión&lt;/h2&gt;
&lt;p&gt;Escribo esta serie porque el consejo más útil que puedo darle a mi yo del pasado es también el que la mayoría de los equipos oye demasiado tarde: decide los cimientos pronto, hazlos comprobables y mantén aburrido todo lo demás. Si estás a punto de empezar un producto multi-tenant, o estás lo bastante al principio como para que los cimientos aún puedan cambiar, empieza por el contexto del tenant. Todo lo demás de la serie se apoya en él.&lt;/p&gt;


</description>
      <category>laravel</category>
      <category>architecture</category>
      <category>saas</category>
      <category>backend</category>
    </item>
    <item>
      <title>Arquitectura de microservicios</title>
      <dc:creator>Ivan Firestone</dc:creator>
      <pubDate>Wed, 28 Oct 2026 13:00:07 +0000</pubDate>
      <link>https://dev.to/ifirestone/arquitectura-de-microservicios-53j4</link>
      <guid>https://dev.to/ifirestone/arquitectura-de-microservicios-53j4</guid>
      <description>&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%2Fimages.unsplash.com%2Fphoto-1667372459510-55b5e2087cd0%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fG1pY3Jvc2VydmljZXN8ZW58MHx8fHwxNzkyNzgzMTQyfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1667372459510-55b5e2087cd0%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fG1pY3Jvc2VydmljZXN8ZW58MHx8fHwxNzkyNzgzMTQyfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="Arquitectura de microservicios" width="2000" height="1125"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Imagina una tienda en línea pequeña. Un solo código, una sola base de datos, un solo despliegue. Los clientes inician sesión, ven productos, pagan y reciben un correo. Funciona, y durante mucho tiempo nadie lo cuestiona.&lt;/p&gt;
&lt;p&gt;Luego la tienda crece y empiezan las preguntas. ¿Por qué un error en la página de productos nos obliga a volver a desplegar el código de pagos? ¿Por qué toda la aplicación se pone lenta cada vez que alguien sube un lote de fotos de productos? En algún momento, alguien dice la palabra: &lt;em&gt;microservicios&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Este artículo sigue a esa tienda. Veremos qué son los microservicios, las pautas que indican cuándo una funcionalidad merece convertirse en uno, qué se gana y cuánto cuesta. No entraremos a fondo en cómo construirlos; esa es una historia para otro día.&lt;/p&gt;
&lt;h2 id="%C2%BFqu%C3%A9-es-realmente-un-microservicio"&gt;¿Qué es realmente un microservicio?&lt;/h2&gt;
&lt;p&gt;Una arquitectura de microservicios divide una aplicación en servicios pequeños e independientes, cada uno responsable de una capacidad del sistema. En lugar de un solo programa que lo hace todo, la tienda pasa a ser un conjunto de programas que se comunican entre sí, normalmente a través de APIs.&lt;/p&gt;
&lt;p&gt;La palabra clave es &lt;em&gt;independiente&lt;/em&gt;. Cada servicio se puede desarrollar, desplegar y escalar por separado. Esa independencia es todo el punto, y también es lo que hace difícil la decisión: solo obtienes los beneficios si las piezas son realmente separables.&lt;/p&gt;
&lt;p&gt;Entonces, ¿cuáles lo son? Recorramos la tienda para averiguarlo.&lt;/p&gt;
&lt;h2 id="%C2%BFqu%C3%A9-funcionalidades-merecen-su-propio-servicio"&gt;¿Qué funcionalidades merecen su propio servicio?&lt;/h2&gt;
&lt;p&gt;No toda funcionalidad justifica un servicio propio. Estas son seis pautas, y cada una aparece a medida que la tienda crece.&lt;/p&gt;
&lt;h3 id="1-la-funcionalidad-puede-valerse-por-s%C3%AD-sola"&gt;1. La funcionalidad puede valerse por sí sola&lt;/h3&gt;
&lt;p&gt;El inicio de sesión y los pagos son los primeros candidatos. Ninguno necesita saber cómo se muestran los productos ni cómo se envían los pedidos. Reciben una petición, hacen un trabajo y devuelven una respuesta.&lt;/p&gt;
&lt;p&gt;Eso es &lt;em&gt;autonomía funcional&lt;/em&gt;: si una funcionalidad puede operar sin estar fuertemente acoplada al resto del sistema, es una buena candidata. La autenticación y la gestión de pagos son ejemplos clásicos.&lt;/p&gt;
&lt;h3 id="2-necesita-escalar-de-forma-distinta"&gt;2. Necesita escalar de forma distinta&lt;/h3&gt;
&lt;p&gt;Ahora la tienda agrega la subida de imágenes. Cada foto de producto se redimensiona y se comprime, y durante una importación grande del catálogo los servidores se calientan. Pero la página de pago no necesita más potencia que antes.&lt;/p&gt;
&lt;p&gt;Escalar toda la aplicación para atender una sola tarea pesada es un desperdicio. Cuando una funcionalidad tiene requisitos de escalado distintos al resto, como un servicio de procesamiento de imágenes que necesita mucho cómputo, un servicio separado te permite escalar solo lo que está bajo presión.&lt;/p&gt;
&lt;h3 id="3-otros-sistemas-tambi%C3%A9n-la-necesitan"&gt;3. Otros sistemas también la necesitan&lt;/h3&gt;
&lt;p&gt;La tienda lanza una segunda marca con su propio sitio web. Ambas necesitan enviar confirmaciones de pedido y restablecimientos de contraseña por correo. Copiar el código de correo al segundo proyecto funcionaría, hasta el día en que una copia recibe una corrección y la otra no.&lt;/p&gt;
&lt;p&gt;Si varios sistemas o aplicaciones usan la misma capacidad, convertirla en un servicio fomenta la reutilización y elimina la duplicación. Un servicio de notificaciones por correo es el ejemplo de siempre.&lt;/p&gt;
&lt;h3 id="4-cambia-a-otro-ritmo"&gt;4. Cambia a otro ritmo&lt;/h3&gt;
&lt;p&gt;El equipo añade recomendaciones de productos con machine learning. Los modelos se reentrenan y se ajustan constantemente, mientras que el flujo de pago cambia unas pocas veces al año. Atarlos al mismo ciclo de releases frena a uno y pone en riesgo al otro.&lt;/p&gt;
&lt;p&gt;Cuando una funcionalidad necesita un ciclo de desarrollo y despliegue distinto al del resto del sistema, debería ser su propio servicio. Un servicio de recomendaciones que necesita actualizaciones frecuentes encaja bien.&lt;/p&gt;
&lt;h3 id="5-corresponde-a-un-dominio-de-negocio-claro"&gt;5. Corresponde a un dominio de negocio claro&lt;/h3&gt;
&lt;p&gt;Mira cómo habla el negocio de sí mismo: catálogo, pedidos, pagos, inventario. En el Diseño Guiado por el Dominio (Domain-Driven Design, DDD), estos son &lt;em&gt;subdominios&lt;/em&gt;: áreas del negocio con su propio lenguaje y sus propias reglas.&lt;/p&gt;
&lt;p&gt;Las funcionalidades que corresponden a un subdominio bien definido son candidatas ideales. Un servicio de inventario en un sistema de comercio electrónico, por ejemplo, refleja cómo el negocio ya piensa.&lt;/p&gt;
&lt;h3 id="6-tiene-el-tama%C3%B1o-adecuado"&gt;6. Tiene el tamaño adecuado&lt;/h3&gt;
&lt;p&gt;Esta es la pauta que la gente olvida. Imagina dividir la tienda tan lejos que "dar formato a un precio" se vuelve un servicio. Ahora cada operación simple requiere una llamada de red, y el sistema es un laberinto.&lt;/p&gt;
&lt;p&gt;Un microservicio debe ser lo bastante pequeño para enfocarse en una tarea, pero lo bastante grande para no fragmentar el sistema en exceso. Si una funcionalidad es trivial, o depende mucho de otras, probablemente no merece ser un servicio.&lt;/p&gt;
&lt;h2 id="lo-que-gana-la-tienda"&gt;Lo que gana la tienda&lt;/h2&gt;
&lt;p&gt;Supongamos que la tienda sigue esas pautas y separa las piezas correctas. ¿Qué cambia?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Escalabilidad.&lt;/strong&gt; El servicio de imágenes escala durante una importación mientras todo lo demás se queda como está.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexibilidad tecnológica.&lt;/strong&gt; El servicio de recomendaciones puede usar el lenguaje y las herramientas más adecuados para machine learning, sin imponérselos al resto del sistema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desarrollo y despliegue independientes.&lt;/strong&gt; Distintos equipos pueden trabajar en distintos servicios al mismo tiempo, lo que acelera el desarrollo y la entrega continua.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resiliencia.&lt;/strong&gt; Como los servicios están desacoplados, la caída de uno no tumba necesariamente al resto. Si las recomendaciones se caen, los clientes todavía pueden pagar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reutilización.&lt;/strong&gt; Un servicio bien diseñado, como el de correo, se puede reutilizar en distintos proyectos y contextos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mantenimiento más fácil.&lt;/strong&gt; Corregir un error o actualizar un servicio no implica tocar toda la aplicación.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="lo-que-paga-la-tienda"&gt;Lo que paga la tienda&lt;/h2&gt;
&lt;p&gt;Si esa fuera toda la historia, todos los proyectos empezarían con microservicios. No lo es.&lt;/p&gt;
&lt;p&gt;Dividir un sistema convierte llamadas a función en llamadas de red, y un solo código en muchas piezas móviles. La tienda ahora necesita monitoreo avanzado para ver qué pasa entre servicios, un versionado cuidadoso para que un servicio no rompa a otro, y un plan para saber cómo se comunican los servicios cuando algo sale mal.&lt;/p&gt;
&lt;p&gt;Esos costos son reales, y caen sobre ti el día que divides. Por eso conviene analizar con cuidado antes de convertir una funcionalidad en un servicio nuevo.&lt;/p&gt;
&lt;h2 id="de-vuelta-a-la-tienda"&gt;De vuelta a la tienda&lt;/h2&gt;
&lt;p&gt;La tienda no necesitó microservicios el primer día. Los necesitó cuando funcionalidades específicas empezaron a tirar en direcciones distintas: otra carga, otro ritmo de releases, otros consumidores. Las seis pautas anteriores son una forma de notar ese momento, y de notar cuándo todavía no ha llegado.&lt;/p&gt;
&lt;p&gt;Usados con intención, los microservicios permiten que un equipo se concentre en la calidad, la escalabilidad y la agilidad. Usados por moda, solo añaden complejidad. Si todavía estás sopesando los pros y contras, &lt;a href="https://blog.ifirestone.dev/monolito-modular-o-microservicios-un-marco-para-decidir-sin-dogmas/" rel="noreferrer noopener"&gt;Monolito modular o microservicios: un marco para decidir sin dogmas&lt;/a&gt; profundiza en esa decisión.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>microservices</category>
      <category>scalability</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Malos hábitos en el desarrollo web que debemos evitar</title>
      <dc:creator>Ivan Firestone</dc:creator>
      <pubDate>Tue, 27 Oct 2026 13:00:06 +0000</pubDate>
      <link>https://dev.to/ifirestone/malos-habitos-en-el-desarrollo-web-que-debemos-evitar-2p78</link>
      <guid>https://dev.to/ifirestone/malos-habitos-en-el-desarrollo-web-que-debemos-evitar-2p78</guid>
      <description>&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%2Fimages.unsplash.com%2Fphoto-1590608897129-79da98d15969%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fEJhZCUyMEhhYml0cyUyMGluJTIwV2ViJTIwRGV2ZWxvcG1lbnQlMjBXZSUyMFNob3VsZCUyMEF2b2lkfGVufDB8fHx8MTc5Mjc4Mjc1M3ww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1590608897129-79da98d15969%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fEJhZCUyMEhhYml0cyUyMGluJTIwV2ViJTIwRGV2ZWxvcG1lbnQlMjBXZSUyMFNob3VsZCUyMEF2b2lkfGVufDB8fHx8MTc5Mjc4Mjc1M3ww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="Malos hábitos en el desarrollo web que debemos evitar" width="2000" height="1333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Imagina que acabas de lanzar un sitio web. En tu monitor de 27 pulgadas se ve genial. Luego un amigo lo abre en su teléfono y te manda una captura: el menú se desborda, los botones son diminutos y la mitad de la página exige scroll lateral.&lt;/p&gt;
&lt;p&gt;Nada en el código está "mal" de una forma que lance un error. Simplemente el sitio arrastra unos cuantos hábitos silenciosos que lo empeoran para las personas reales, y también para quien lo mantiene. Son fáciles de adquirir, porque cada uno ahorra un poco de tiempo hoy y lo cobra después.&lt;/p&gt;
&lt;p&gt;Estos son cinco de los más comunes. Para cada uno veremos cómo muerde y qué hacer en su lugar. Empezamos con el de aquella captura.&lt;/p&gt;
&lt;h2 id="1-no-pensar-en-el-m%C3%B3vil-desde-el-principio"&gt;1. No pensar en el móvil desde el principio&lt;/h2&gt;
&lt;p&gt;La mayoría de los usuarios llega a la web desde un teléfono. Tratar el móvil como un añadido, algo donde se encaja el diseño de escritorio al final, es un error crítico. Corregirlo tarde significa deshacer decisiones sobre las que se construyó toda la página.&lt;/p&gt;
&lt;p&gt;Un enfoque &lt;strong&gt;mobile-first&lt;/strong&gt; invierte el orden: primero asegúrate de que el sitio funcione y se vea bien en pantallas pequeñas, y luego escala a las grandes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Qué hacer:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prototipa primero el diseño móvil y revisa la experiencia en distintos tamaños de pantalla.&lt;/li&gt;
&lt;li&gt;Escribe los estilos base para pantallas pequeñas y usa media queries con &lt;code&gt;min-width&lt;/code&gt; para mejorar en las grandes.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/* Base: pantallas pequeñas */
.card-list {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;
}

/* Pantallas grandes: mejoras */
@media (min-width: 768px) {
  .card-list {
    grid-template-columns: repeat(3, 1fr);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cuando el diseño aguanta en todas las pantallas, lo siguiente que nota un visitante es si las páginas parecen pertenecer al mismo sitio.&lt;/p&gt;
&lt;h2 id="2-ser-inconsistente"&gt;2. Ser inconsistente&lt;/h2&gt;
&lt;p&gt;Imagina a un visitante navegando por tu sitio. En la página de inicio, el enlace activo es azul. En la de precios, es verde. En el blog, va subrayado y en rojo. Nada está roto, pero el visitante empieza a preguntarse si sigue en el mismo sitio.&lt;/p&gt;
&lt;p&gt;El diseño, los enlaces y los estilos inconsistentes confunden a los usuarios y hacen que un producto parezca poco profesional.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Qué hacer:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Define y sigue una guía de estilo que cubra colores, tipografía, botones y enlaces.&lt;/li&gt;
&lt;li&gt;Usa un sistema de diseño (Material Design, Bootstrap o uno propio) para mantener la coherencia visual y funcional.&lt;/li&gt;
&lt;li&gt;Centraliza los valores de diseño como tokens en lugar de repetirlos:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;:root {
  --color-link: #0b5fff;
  --color-link-active: #0a3fb3;
}

a { color: var(--color-link); }
a:active, a[aria-current="page"] { color: var(--color-link-active); }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cambias el token una vez y todas las páginas lo siguen. Justo lo contrario de lo que pasa con el siguiente hábito.&lt;/p&gt;
&lt;h2 id="3-escribir-urls-fijas-en-el-c%C3%B3digo"&gt;3. Escribir URLs fijas en el código&lt;/h2&gt;
&lt;p&gt;Digamos que el sitio tiene cien páginas, cada una enlazando a &lt;code&gt;https://www.mysite.com/...&lt;/code&gt;. Entonces cambia el dominio. O alguien necesita una copia de staging para probar un release. De pronto todos los enlaces apuntan al lugar equivocado, y la solución es un buscar y reemplazar en todo el proyecto.&lt;/p&gt;
&lt;p&gt;Las URLs fijas en el código fuente hacen que un sitio sea más difícil de mantener y de escalar. Se rompen cada vez que cambia el dominio o cuando trabajas con varios entornos (desarrollo, staging, producción).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Qué hacer:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prefiere rutas relativas en lugar de absolutas para los enlaces internos y los recursos.&lt;/li&gt;
&lt;li&gt;Usa variables de entorno para manejar las URLs que dependen del entorno, como la URL base de una API.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// Mal
fetch("&amp;lt;https://api.mysite.com/users&amp;gt;");

// Mejor
fetch(`${process.env.API_BASE_URL}/users`);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Mal --&amp;gt;
&amp;lt;a href="&amp;lt;https://www.mysite.com/about&amp;gt;"&amp;gt;Acerca de&amp;lt;/a&amp;gt;

&amp;lt;!-- Mejor --&amp;gt;
&amp;lt;a href="/about"&amp;gt;Acerca de&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hasta aquí vimos hábitos que perjudican a quienes mantienen el sitio. El siguiente perjudica a quienes no pueden ver la página.&lt;/p&gt;
&lt;h2 id="4-omitir-el-atributo-alt-en-las-im%C3%A1genes"&gt;4. Omitir el atributo &lt;code&gt;alt&lt;/code&gt; en las imágenes&lt;/h2&gt;
&lt;p&gt;Piensa en alguien que usa un lector de pantalla y llega a la página de tu producto. Donde debería haber una foto del producto, el lector dice solo "imagen" o, peor, lee en voz alta un nombre de archivo como &lt;code&gt;IMG_4821.jpg&lt;/code&gt;. Lo mismo le pasa a cualquiera cuando una imagen no carga por una mala conexión.&lt;/p&gt;
&lt;p&gt;El atributo &lt;code&gt;alt&lt;/code&gt; describe una imagen a los usuarios con discapacidad visual y a todos cuando la imagen no carga. Omitirlo hace la página más difícil de usar, y además perjudica el SEO.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Qué hacer:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Da a cada imagen con significado una descripción clara y concisa.&lt;/li&gt;
&lt;li&gt;Si una imagen es puramente decorativa, usa un &lt;code&gt;alt=""&lt;/code&gt; vacío para que la tecnología de asistencia la salte en lugar de leer un nombre de archivo.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Informativa --&amp;gt;
&amp;lt;img src="chart.png" alt="Gráfico de barras que muestra el tráfico móvil creciendo 40% interanual"&amp;gt;

&amp;lt;!-- Decorativa --&amp;gt;
&amp;lt;img src="divider.svg" alt=""&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Los lectores de pantalla dependen tanto de la estructura de la página como de sus imágenes, lo que nos lleva al último hábito.&lt;/p&gt;
&lt;h2 id="5-usar-demasiados-divs"&gt;5. Usar demasiados &lt;code&gt;div&lt;/code&gt;s&lt;/h2&gt;
&lt;p&gt;Abre el HTML de una página construida así y encontrarás &lt;code&gt;div&lt;/code&gt; dentro de &lt;code&gt;div&lt;/code&gt; dentro de &lt;code&gt;div&lt;/code&gt;, cada uno con un nombre de clase que intenta decir qué es en realidad el elemento. Para una persona es tedioso. Para un lector de pantalla o un motor de búsqueda, es una página sin puntos de referencia para orientarse.&lt;/p&gt;
&lt;p&gt;Recurrir a &lt;code&gt;div&lt;/code&gt; cuando existe un elemento semántico específico hace el código más difícil de leer y perjudica la accesibilidad y el SEO. Los lectores de pantalla y los motores de búsqueda dependen de elementos como &lt;code&gt;header&lt;/code&gt;, &lt;code&gt;nav&lt;/code&gt;, &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;section&lt;/code&gt;, &lt;code&gt;article&lt;/code&gt; y &lt;code&gt;footer&lt;/code&gt; para entender la estructura de la página.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Qué hacer:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Usa elementos semánticos para estructurar tu HTML.&lt;/li&gt;
&lt;li&gt;Deja &lt;code&gt;div&lt;/code&gt; para agrupaciones genéricas, cuando ningún elemento mejor encaje.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Sopa de divs --&amp;gt;
&amp;lt;div class="header"&amp;gt;...&amp;lt;/div&amp;gt;
&amp;lt;div class="main"&amp;gt;
  &amp;lt;div class="article"&amp;gt;...&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;div class="footer"&amp;gt;...&amp;lt;/div&amp;gt;

&amp;lt;!-- Semántico --&amp;gt;
&amp;lt;header&amp;gt;...&amp;lt;/header&amp;gt;
&amp;lt;main&amp;gt;
  &amp;lt;article&amp;gt;...&amp;lt;/article&amp;gt;
&amp;lt;/main&amp;gt;
&amp;lt;footer&amp;gt;...&amp;lt;/footer&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="de-vuelta-a-aquella-captura"&gt;De vuelta a aquella captura&lt;/h2&gt;
&lt;p&gt;Vuelve a la captura de tu amigo, la del principio. Detrás del menú desbordado puede haber un diseño sin enfoque mobile-first, estilos inconsistentes, una URL fija, una imagen sin descripción y una página hecha solo de &lt;code&gt;div&lt;/code&gt;s. Ninguno fue una decisión grande, pero juntos forman un sitio más difícil de usar y más difícil de mantener.&lt;/p&gt;
&lt;p&gt;Evitar estos hábitos mejora más que el código: mejora la experiencia de usuario, la accesibilidad y la eficiencia del desarrollo. Incorporarlos desde el inicio hace un sitio más fácil de mantener y más robusto.&lt;/p&gt;
&lt;p&gt;Elige un proyecto, revísalo contra esta lista y corrige lo que encuentres.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>spanish</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A Local Development Toolbox with Docker Compose: Databases, Cloud Emulators and Mocks</title>
      <dc:creator>Ivan Firestone</dc:creator>
      <pubDate>Mon, 26 Oct 2026 12:00:05 +0000</pubDate>
      <link>https://dev.to/ifirestone/a-local-development-toolbox-with-docker-compose-databases-cloud-emulators-and-mocks-32db</link>
      <guid>https://dev.to/ifirestone/a-local-development-toolbox-with-docker-compose-databases-cloud-emulators-and-mocks-32db</guid>
      <description>&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%2Fimages.unsplash.com%2Fphoto-1646627927863-19874c27316b%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fGRvY2tlcnxlbnwwfHx8fDE3OTI3MDcwNDZ8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1646627927863-19874c27316b%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fGRvY2tlcnxlbnwwfHx8fDE3OTI3MDcwNDZ8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="A Local Development Toolbox with Docker Compose: Databases, Cloud Emulators and Mocks" width="2000" height="1250"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;blockquote&gt; How to build a reproducible local environment with Docker Compose for databases, MinIO, AWS/Azure/GCP emulators and WireMock, and what trade-offs it has.&lt;/blockquote&gt;
&lt;p&gt;Testing integrations against real services is slow, costly and sometimes dangerous. I built a repository, &lt;a href="https://github.com/ifirestone/dev-tools" rel="noopener noreferrer"&gt;&lt;code&gt;dev-tools&lt;/code&gt;&lt;/a&gt;, that gathers in one place everything needed to set up a local environment and &lt;strong&gt;test without depending on real services&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Local databases and storage with Docker Compose.&lt;/li&gt;
&lt;li&gt;AWS, Azure and GCP emulators.&lt;/li&gt;
&lt;li&gt;A mock server with WireMock.&lt;/li&gt;
&lt;li&gt;Sample data in JSON.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This article explains the idea, how to use it and, above all, &lt;strong&gt;its limits&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;A typical project depends on a database, object storage, sometimes cloud services and third-party APIs. Without a consistent local environment, familiar things happen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;"It works on my machine", because each developer installed different versions.&lt;/li&gt;
&lt;li&gt;Tests that depend on someone else's sandbox that goes down or has usage limits.&lt;/li&gt;
&lt;li&gt;Real credentials on development machines, an unnecessary risk.&lt;/li&gt;
&lt;li&gt;Slow onboarding: days to get the environment working.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The solution is to declare the environment as code and bring it up with one command.&lt;/p&gt;
&lt;h2 id="repository-structure"&gt;Repository Structure&lt;/h2&gt;
&lt;p&gt;Each folder is independent: you only use the one you need.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dev-tools/&lt;br&gt;
├── dev-infra/    # Local databases and storage&lt;br&gt;
├── floci/        # AWS / Azure / GCP cloud emulators&lt;br&gt;
├── wiremock/     # Mock server + Postman and Bruno collections&lt;br&gt;
├── demo/         # Sample data in JSON&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="dev-infra-databases-and-storage"&gt;
&lt;code&gt;dev-infra&lt;/code&gt;: Databases and Storage&lt;/h3&gt;
&lt;p&gt;A Compose file with MinIO, PostgreSQL, MySQL, Redis, Oracle, MongoDB and SQL Server, with a development user created in each service. Credentials live in a local &lt;code&gt;.env&lt;/code&gt; that isn't versioned; only the &lt;code&gt;.env.example&lt;/code&gt; template is published.&lt;/p&gt;
&lt;h3 id="floci-cloud-emulators"&gt;
&lt;code&gt;floci&lt;/code&gt;: Cloud Emulators&lt;/h3&gt;
&lt;p&gt;Local emulators for AWS, Azure and GCP with a unified web console, using the images from the open-source Floci project. They let you develop against the S3, DynamoDB, Lambda and other service APIs without a real account or costs.&lt;/p&gt;
&lt;h3 id="wiremock-third-party-api-mocks"&gt;
&lt;code&gt;wiremock&lt;/code&gt;: Third-Party API Mocks&lt;/h3&gt;
&lt;p&gt;A &lt;a href="https://wiremock.org/" rel="noopener noreferrer"&gt;WireMock&lt;/a&gt; server with an admin interface, sample stubs and Postman and Bruno collections. It's used to simulate external APIs: successful responses, errors, latencies and edge cases that are hard to trigger on the real service.&lt;/p&gt;
&lt;h3 id="demo-sample-data"&gt;
&lt;code&gt;demo&lt;/code&gt;: Sample Data&lt;/h3&gt;
&lt;p&gt;Bilingual JSON catalogs (countries, banks, professions…) to use in seeds, mocks and tests.&lt;/p&gt;
&lt;h2 id="how-to-use-it"&gt;How to Use It&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git clone &lt;a href="https://github.com/ifirestone/dev-tools.git" rel="noopener noreferrer"&gt;https://github.com/ifirestone/dev-tools.git&lt;/a&gt;&lt;br&gt;
cd dev-tools

&lt;h1&gt;
  
  
  Local credentials (not versioned)
&lt;/h1&gt;

&lt;p&gt;cp dev-infra/.env.example dev-infra/.env&lt;/p&gt;

&lt;h1&gt;
  
  
  Each stack is independent
&lt;/h1&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;docker compose -f dev-infra/docker-compose.yml up -d&lt;br&gt;&lt;br&gt;
docker compose -f floci/docker-compose.yml up -d&lt;br&gt;&lt;br&gt;
docker compose -f wiremock/docker-compose.yml up -d&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;The three stacks don't share ports, so they can run at the same time. To shut one down:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose -f &amp;lt;folder&amp;gt;/docker-compose.yml down      # keeps data&lt;br&gt;&lt;br&gt;
docker compose -f &amp;lt;folder&amp;gt;/docker-compose.yml down -v   # also deletes data&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An example with the AWS emulator:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export AWS_ACCESS_KEY_ID=test&lt;br&gt;&lt;br&gt;
export AWS_SECRET_ACCESS_KEY=test&lt;br&gt;&lt;br&gt;
export AWS_DEFAULT_REGION=us-east-1

&lt;/code&gt;&lt;p&gt;&lt;code&gt;aws --endpoint-url &lt;a href="http://localhost:4566" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="http://localhost:4566" rel="noopener noreferrer"&gt;http://localhost:4566&lt;/a&gt; s3 ls&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;Your application code only changes the &lt;em&gt;endpoint&lt;/em&gt;. The rest of the client is the same one you'd use in the real cloud.&lt;/p&gt;
&lt;h2 id="why-mocks-in-addition-to-emulators"&gt;Why Mocks in Addition to Emulators&lt;/h2&gt;
&lt;p&gt;Emulators cover services with a public, stable API. For specific third-party APIs (a payment provider, a corporate platform) there is no emulator, and that's where WireMock helps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Determinism:&lt;/strong&gt; the response is always the same, so tests aren't brittle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error cases on demand:&lt;/strong&gt; a 500, a timeout or a malformed response can be simulated in seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel work:&lt;/strong&gt; you can develop the integration before having access to the provider's sandbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit contract:&lt;/strong&gt; the stubs document how you expect the third party to respond.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="trade-offs-and-limits-what-you-should-know"&gt;Trade-offs and Limits (What You Should Know)&lt;/h2&gt;
&lt;p&gt;A toolbox like this has costs, and they're worth stating:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Partial fidelity.&lt;/strong&gt; An emulator doesn't replicate 100% of the real cloud's behavior: permissions, quota limits and edge cases may differ. Use it for development and quick tests, &lt;strong&gt;not as a substitute for testing against the real service&lt;/strong&gt; before production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource consumption.&lt;/strong&gt; Oracle and SQL Server use a lot of memory. If your machine is tight, bring up only the services you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local development only.&lt;/strong&gt; None of this is meant for production or for exposure on a shared network. The sample credentials are public.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker access.&lt;/strong&gt; The emulators mount &lt;code&gt;/var/run/docker.sock&lt;/code&gt; to support services such as serverless functions, which gives them control over Docker on your machine. Remove that mount in environments where you don't need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images tagged &lt;code&gt;latest&lt;/code&gt;.&lt;/strong&gt; Several services use that tag. If you need reproducibility, pin versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple Silicon.&lt;/strong&gt; Some images only exist for x86_64 and run emulated; you'll see a platform warning, not an error.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-i-would-do-differently-with-more-time"&gt;What I Would Do Differently With More Time&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Pin the versions of all images by default.&lt;/li&gt;
&lt;li&gt;Add Compose &lt;em&gt;profiles&lt;/em&gt; to bring up typical combinations (for example, "just PostgreSQL + MinIO").&lt;/li&gt;
&lt;li&gt;Automate health checks to know when each service is ready.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A local environment declared as code speeds up onboarding and eliminates "it works on my machine".&lt;/li&gt;
&lt;li&gt;Emulators and mocks let you test integrations without depending on third parties or exposing real credentials.&lt;/li&gt;
&lt;li&gt;Use them for development, with clarity about their fidelity and security limits.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The repository is open source (MIT license). If you use it and a service is missing, contributions are welcome.&lt;/p&gt;


</description>
      <category>devops</category>
      <category>docker</category>
      <category>testing</category>
      <category>tools</category>
    </item>
    <item>
      <title>Skills de IA para analizar logs: cómo estructuré un diagnóstico reproducible</title>
      <dc:creator>Ivan Firestone</dc:creator>
      <pubDate>Sun, 25 Oct 2026 12:00:06 +0000</pubDate>
      <link>https://dev.to/ifirestone/skills-de-ia-para-analizar-logs-como-estructure-un-diagnostico-reproducible-10jd</link>
      <guid>https://dev.to/ifirestone/skills-de-ia-para-analizar-logs-como-estructure-un-diagnostico-reproducible-10jd</guid>
      <description>&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%2Fimages.unsplash.com%2Fphoto-1694903089438-bf28d4697d9a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDJ8fGFpJTIwc2tpbGxzfGVufDB8fHx8MTc5Mjc3OTc4NXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1694903089438-bf28d4697d9a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDJ8fGFpJTIwc2tpbGxzfGVufDB8fHx8MTc5Mjc3OTc4NXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="Skills de IA para analizar logs: cómo estructuré un diagnóstico reproducible" width="2000" height="1125"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;blockquote&gt;Cómo diseñé un conjunto de skills y agentes para asistentes de IA que detectan la tecnología de un log, piden el contexto que falta y entregan un diagnóstico estandarizado. Diseño, instalación y límites.&lt;/blockquote&gt;
&lt;h2 id="resumen"&gt;Resumen&lt;/h2&gt;
&lt;p&gt;Analizar logs con un asistente de IA funciona mejor cuando el asistente sigue un &lt;strong&gt;procedimiento&lt;/strong&gt; en lugar de improvisar. Reuní mis instrucciones en &lt;a href="https://github.com/ifirestone/ia-tools" rel="noopener noreferrer"&gt;&lt;code&gt;ia-tools&lt;/code&gt;&lt;/a&gt;: una colección de &lt;em&gt;skills&lt;/em&gt; y agentes, en español, para Claude Code, Codex y otros entornos compatibles con el estándar abierto Agent Skills.&lt;/p&gt;
&lt;p&gt;Este artículo explica cómo están diseñados, por qué los dividí así y qué límites tienen.&lt;/p&gt;
&lt;h2 id="el-problema"&gt;El problema&lt;/h2&gt;
&lt;p&gt;Pegar un stack trace en un chat y preguntar "¿qué está fallando?" da resultados dispares:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;El asistente supone la tecnología y la versión sin preguntar.&lt;/li&gt;
&lt;li&gt;Cada respuesta tiene un formato distinto, difícil de comparar o de pasar a un ticket.&lt;/li&gt;
&lt;li&gt;Los datos sensibles (tokens, credenciales, datos personales) se cuelan sin que nadie los señale.&lt;/li&gt;
&lt;li&gt;El diagnóstico ocurre sin conocer el entorno (producción o desarrollo, rol del servidor, versión).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Un buen diagnóstico siempre sigue los mismos pasos. Y si los pasos son siempre los mismos, se pueden escribir una sola vez.&lt;/p&gt;
&lt;h2 id="qu%C3%A9-es-una-skill"&gt;Qué es una skill&lt;/h2&gt;
&lt;p&gt;Una skill es una carpeta con un archivo &lt;code&gt;SKILL.md&lt;/code&gt;: instrucciones que el modelo carga automáticamente cuando la conversación coincide con su &lt;code&gt;description&lt;/code&gt;, o que invocas a mano con &lt;code&gt;/nombre&lt;/code&gt;. El material de referencia largo vive en &lt;code&gt;references/&lt;/code&gt; y se carga solo cuando hace falta.&lt;/p&gt;
&lt;p&gt;Esa carga bajo demanda es la decisión de diseño más importante: &lt;strong&gt;no meter todo en el contexto&lt;/strong&gt;. Una sola skill gigante con reglas para veinte tecnologías consumiría contexto y diluiría las instrucciones.&lt;/p&gt;
&lt;h2 id="c%C3%B3mo-est%C3%A1-organizado"&gt;Cómo está organizado&lt;/h2&gt;
&lt;p&gt;Una skill principal y varias específicas por tecnología:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;log-analyzer&lt;/code&gt;:&lt;/strong&gt; detecta la tecnología del log a partir de sus patrones, pide el contexto que no puede inferir y produce un diagnóstico estandarizado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills por familia técnica:&lt;/strong&gt; nube (Azure, AWS, GCP), bases de datos (PostgreSQL, Oracle, SQL Server), Docker, .NET, Kafka, Kubernetes y OpenShift, Laravel, PHP, Spring Boot, Quarkus, servidores web, Windows Event Log, MinIO, IBM API Connect y alertas de SRE.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La principal decide qué referencia cargar. Así el asistente trae al contexto solo lo relevante para ese log.&lt;/p&gt;
&lt;h2 id="el-flujo-del-diagn%C3%B3stico"&gt;El flujo del diagnóstico&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detectar la tecnología&lt;/strong&gt; a partir de los patrones del propio log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preguntar lo que no puede inferir:&lt;/strong&gt; entorno, versión, rol del servidor. Diagnosticar sin ese contexto lleva a conclusiones seguras de sí mismas y equivocadas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analizar&lt;/strong&gt; con las referencias específicas de esa tecnología.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entregar un reporte con estructura fija&lt;/strong&gt;, para que sea comparable entre incidentes y fácil de adjuntar a un ticket.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="una-decisi%C3%B3n-deliberada-se%C3%B1alar-no-ocultar"&gt;Una decisión deliberada: señalar, no ocultar&lt;/h2&gt;
&lt;p&gt;Las skills &lt;strong&gt;no censuran&lt;/strong&gt; las credenciales, los tokens ni los datos personales que aparecen en el log. Los &lt;strong&gt;señalan en rojo&lt;/strong&gt; en el reporte.&lt;/p&gt;
&lt;p&gt;La razón es práctica: si el log contiene un secreto, lo que necesitas es &lt;strong&gt;rotarlo&lt;/strong&gt;, y para eso tienes que saber que está ahí. Ocultarlo en silencio te deja con una falsa sensación de seguridad. El costo es que el reporte puede contener datos sensibles, así que debes revisarlo antes de compartirlo.&lt;/p&gt;
&lt;p&gt;Una regla complementaria: &lt;strong&gt;no pegues logs de producción en herramientas externas sin la autorización de tu organización.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="agentes-otra-pieza-distinta"&gt;Agentes: otra pieza distinta&lt;/h2&gt;
&lt;p&gt;Además de las skills, el repositorio incluye cuatro agentes para Claude Code (roles independientes con instrucciones propias):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redacción de comunicaciones corporativas.&lt;/li&gt;
&lt;li&gt;Conversión de notas o transcripciones en minutas de reunión.&lt;/li&gt;
&lt;li&gt;Extracción de decisiones, tareas y riesgos accionables.&lt;/li&gt;
&lt;li&gt;Traducción, corrección y ajuste de tono.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Son plantillas genéricas escritas para equipos hispanohablantes; la idea es que las adaptes a tu organización.&lt;/p&gt;
&lt;h2 id="instalaci%C3%B3n"&gt;Instalación&lt;/h2&gt;
&lt;p&gt;En Claude Code, como plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/plugin install ia-tools --marketplace ifirestone/ia-tools&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Para Codex y otros entornos compatibles, el repositorio incluye un script que enlaza cada skill en tu directorio de skills:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./scripts/link-skills.sh&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Como son enlaces simbólicos al repositorio, un &lt;code&gt;git pull&lt;/code&gt; mantiene todo al día. Ten en cuenta que el script reemplaza las carpetas existentes con el mismo nombre de skill, así que revísalo antes si ya tienes skills propias.&lt;/p&gt;
&lt;p&gt;ChatGPT no tiene un mecanismo nativo para cargar skills: puedes pegar el contenido de un &lt;code&gt;SKILL.md&lt;/code&gt; como instrucciones de un Project o de un GPT personalizado y subir las referencias como archivos de conocimiento.&lt;/p&gt;
&lt;h2 id="l%C3%ADmites-que-conviene-tener-claros"&gt;Límites que conviene tener claros&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Un diagnóstico es una hipótesis, no un veredicto.&lt;/strong&gt; El asistente puede equivocarse, sobre todo con logs parciales o sin contexto. Verifica antes de actuar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La calidad depende del log que le des.&lt;/strong&gt; Un fragmento sin la línea del error raíz produce conclusiones débiles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mantenimiento.&lt;/strong&gt; Las tecnologías cambian de versión y de mensajes de error; las referencias hay que mantenerlas al día.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensibilidad de los datos.&lt;/strong&gt; Señalar no es proteger. La responsabilidad de no exponer información sigue siendo tuya.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="lo-que-aprend%C3%AD-al-dise%C3%B1arlo"&gt;Lo que aprendí al diseñarlo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Un procedimiento fijo vale más que un prompt ingenioso.&lt;/strong&gt; La consistencia es lo que vuelve útil el diagnóstico.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preguntar antes de diagnosticar&lt;/strong&gt; evita la mayoría de las respuestas falsamente seguras.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dividir por tecnología&lt;/strong&gt; mantiene el contexto pequeño y las instrucciones precisas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escribir en el idioma del equipo&lt;/strong&gt; reduce la fricción cuando el reporte lo lee gente de operaciones o de soporte.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusi%C3%B3n"&gt;Conclusión&lt;/h2&gt;
&lt;p&gt;Las skills convierten una buena práctica de diagnóstico en algo repetible y compartible. No reemplazan el criterio del ingeniero; ahorran el trabajo mecánico del primer análisis.&lt;/p&gt;
&lt;p&gt;El repositorio es de código abierto (MIT). Si agregas una skill para otra tecnología, la guía de contribución explica la estructura.&lt;/p&gt;


</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Ocho claves de CA, una de ellas el número 4: lo que enseña el incidente de eMudhra sobre los trust stores</title>
      <dc:creator>Ivan Firestone</dc:creator>
      <pubDate>Sun, 25 Oct 2026 02:57:57 +0000</pubDate>
      <link>https://dev.to/ifirestone/ocho-claves-de-ca-una-de-ellas-el-numero-4-lo-que-ensena-el-incidente-de-emudhra-sobre-los-trust-5gbm</link>
      <guid>https://dev.to/ifirestone/ocho-claves-de-ca-una-de-ellas-el-numero-4-lo-que-ensena-el-incidente-de-emudhra-sobre-los-trust-5gbm</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwmtomyhto5fl69bsw6ay.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwmtomyhto5fl69bsw6ay.webp" alt="Ocho claves de CA, una de ellas el número 4: lo que enseña el incidente de eMudhra sobre los trust stores" width="690" height="388"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;p&gt;La clave privada de una autoridad certificadora debería ser un número tan grande que adivinarlo llevaría más tiempo que la vida del sol. El 17 de octubre de 2026, un investigador demostró que una de las de eMudhra era el número 4.&lt;/p&gt;
&lt;p&gt;No un 4 seguido de unos cientos de dígitos. Solo un 4.&lt;/p&gt;
&lt;p&gt;Este artículo trata de lo que eso significa para el código que ejecutas, no para la pestaña del navegador en la que lees esto. Dejo fuera la discusión sobre si eMudhra merece un lugar en los programas de certificados raíz, porque eso les corresponde a quienes gestionan esos programas. Lo que quiero cubrir es la distancia entre "Chrome lo bloqueó" y "mi servicio lo bloquea", porque en esa distancia vivimos casi todos.&lt;/p&gt;
&lt;h2 id="ocho-claves-que-se-cuentan-con-los-dedos"&gt;Ocho claves que se cuentan con los dedos&lt;/h2&gt;
&lt;p&gt;Las claves afectadas pertenecían a ocho certificados intermedios, generados en una ceremonia de claves (key ceremony, el acto formal en que se genera y custodia la clave de una CA) en abril de 2025. Seis eran para TLS y dos para firma de código. Las claves privadas resultaron ser enteros pequeños: 4, 56, 66, 115, 155, 156, 230 y 237, según&amp;nbsp;&lt;a href="https://www.theregister.com/security/2026/10/22/disgraced-indian-cert-issuer-leaves-the-cert-issuing-business/5304848" rel="noopener noreferrer"&gt;The Register&lt;/a&gt;&amp;nbsp;(en inglés). Cualquiera podía recuperarlas a partir de los certificados públicos en menos de un segundo.&lt;/p&gt;
&lt;p&gt;Un repaso rápido de por qué importa. Una CA intermedia es la que firma los certificados que tus servidores presentan en la práctica. Quien tenga su clave privada puede emitir un certificado para cualquier dominio, y todo cliente que confíe en esa cadena lo aceptará. Piensa en un sello de lacre cuyo dibujo cualquiera puede redibujar a partir de una foto del sello.&lt;/p&gt;
&lt;p&gt;Aquí no se robó nada.&amp;nbsp;&lt;a href="https://www.itnews.com.au/news/cloudflare-warns-fixing-botched-emudhra-certs-goes-beyond-browsers-629804" rel="noopener noreferrer"&gt;iTnews&lt;/a&gt;&amp;nbsp;(en inglés) lo dijo bien: no hizo falta robar nada para crear el fallo. eMudhra afirma que un error en su ceremonia de generación de claves hizo que un paso de wrap/unwrap produjera un segundo juego de claves débiles junto a las fuertes que pretendía, y que las débiles fueron las que se enviaron a los programas de certificados raíz de los navegadores. También dice que las CA afectadas no emitieron certificados de entidad final, que las revocó en 14 horas desde que descubrió el problema y que su infraestructura y sus datos no se vieron comprometidos. &lt;strong&gt;&lt;em&gt;No puedo verificar esas afirmaciones por mi cuenta.&lt;/em&gt;&lt;/strong&gt; Lo público son los certificados, y si alguien usó las claves en los aproximadamente 18 meses previos al reporte es, hasta donde sé, algo que se desconoce.&lt;/p&gt;
&lt;p&gt;Así que esto no fue un hackeo. Fue un mal número a la vista de todos durante año y medio. Lo que lleva a la siguiente pregunta: quién se enteró y con qué rapidez reaccionó el mundo.&lt;/p&gt;
&lt;h2 id="cinco-d%C3%ADas-cuatro-fabricantes"&gt;Cinco días, cuatro fabricantes&lt;/h2&gt;
&lt;p&gt;El reporte llegó a los programas de certificados raíz de los navegadores el 17 de octubre. Chrome bloqueó los certificados ese mismo día mediante su mecanismo CRLSet, y Firefox hizo lo mismo con OneCRL. Microsoft siguió el 20 de octubre.  iTnews afirma que los dispositivos Apple estaban protegidos desde el 19 de octubre, mientras que el aviso que cito más abajo indica el 22. No pude conciliar ambas fechas.&lt;/p&gt;
&lt;p&gt;El reloj de Cloudflare empezó más tarde. Su&amp;nbsp;&lt;a href="https://blog.cloudflare.com/emudhra-certificate-compromise/" rel="noopener noreferrer"&gt;post del incidente&lt;/a&gt;&amp;nbsp;(en inglés) dice que se enteró hacia las 17:00 UTC del 19 de octubre. A partir de ahí el trabajo siguió un orden claro:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Primero, los servicios que usan certificados públicos para autenticar endpoints remotos: proxies que se conectan a orígenes y aplicaciones que llaman a APIs externas.&lt;/li&gt;
&lt;li&gt;Después, los trust stores compartidos y las imágenes base, para que todo lo que hereda confianza de ellos recibiera la corrección, seguido de los reinicios y despliegues de imágenes necesarios para activarla.&lt;/li&gt;
&lt;li&gt;Las aplicaciones con trust stores propios o personalizados se evaluaron aparte.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La cronología publicada: actualizaciones del cliente WARP a las 02:00 UTC del 20 de octubre, la actualización de cloudflared a las 09:15, y las mitigaciones de CDN y de Zero Trust del lado del servidor desplegadas por completo a las 19:20. Cloudflare reportó que no hubo explotación activa en sus servicios. También subrayó que es un problema de autenticación de certificados, no una debilidad en los algoritmos de cifrado TLS.&lt;/p&gt;
&lt;p&gt;Luego vino la frase que más importa para el resto de nosotros: algunos clientes tienen que actuar sobre software o configuraciones de confianza que operan ellos mismos, sobre todo en los servidores de origen. ¿Por qué un borde ya parcheado dejaría algo por hacer?&lt;/p&gt;
&lt;h2 id="el-navegador-tiene-un-interruptor-tu-servicio-no"&gt;El navegador tiene un interruptor. Tu servicio no.&lt;/h2&gt;
&lt;p&gt;Los fabricantes de navegadores lo resolvieron "con solo accionar un interruptor", como lo expresó Cloudflare (leyendo entre lineas), porque los navegadores tienen canales de revocación rápidos como los CRLSets, que empujan un bloqueo a todos los usuarios sin ciclo de actualización. Además, tienen una aplicación que arreglar, no cientos.&lt;/p&gt;
&lt;p&gt;Todo lo demás (tu cliente HTTP, tu runtime, tu imagen de contenedor, el bundle de CA que trae una herramienta) tiene su propia idea de en quién confiar. iTnews lo probó en macOS: &lt;strong&gt;&lt;em&gt;curl&lt;/em&gt;&lt;/strong&gt; siguió aceptando certificados de la cadena comprometida después del bloqueo de Apple, porque curl comprueba contra su propio archivo en&amp;nbsp;&lt;code&gt;/etc/ssl/cert.pem&lt;/code&gt;&amp;nbsp;sin consultar información de revocación. El aviso de Jackson dice que ese mismo archivo alimenta a git, python3, perl, ruby y openssl, los que vienen incluidos en macOS. Dennis Jackson, de Mozilla, quien publica el aviso enlazado más abajo, sugirió quitar dos raíces de ese archivo como solución provisional: la emSign ECC Root CA - G3 de eMudhra y la raíz AAA Certificate Services. Sectigo había hecho la firma cruzada de algunos de los intermedios débiles. Mozilla está retirando ahora la confianza en la raíz emSign por completo, y Chrome se ha comprometido a un camino similar.&lt;/p&gt;
&lt;p&gt;Una advertencia sobre esa solución provisional, y esta parte es mi criterio, no el de Jackson. Quitar una raíz muy usada de un bundle compartido no sale gratis. Pruébalo contra los servicios que llamas antes de desplegarlo. El propio aviso trata estos pasos como provisionales y los da por plataforma: macOS, Linux, Windows, Android y los BSD. Califica el enfoque de Linux, dejar de confiar en las raíces mediante la lista de bloqueo de la distribución, como solo para expertos, y advierte que desactivar la raíz AAA Certificate Services en Android puede romper sitios que aún encadenan a ella.&lt;/p&gt;
&lt;p&gt;Hay otra razón por la que los navegadores lo tuvieron más fácil, y está en el mismo aviso. Ninguno de los intermedios débiles tiene una entrada correspondiente en los logs de Certificate Transparency (CT), así que los navegadores que exigen CT no podrían haber sido atacados con certificados de estas claves. Los clientes que no exigen CT, entre ellos muchos clientes HTTP de backend, no tuvieron ese colchón. El 22 de octubre Jackson añadió a su verificador dos hosts de prueba "No CT", para ver si un cliente acepta certificados que nunca se registraron.&lt;/p&gt;
&lt;p&gt;Imagina un job de backend que llama a la API de un socio desde una imagen de contenedor construida hace seis meses. Abres el sitio del socio en tu navegador, ves el candado y concluyes que todo está bien. El contenedor nunca se enteró del 17 de octubre. Esa es la distancia.&lt;/p&gt;
&lt;h2 id="qu%C3%A9-revisar-el-lunes"&gt;Qué revisar el lunes&lt;/h2&gt;
&lt;p&gt;Empieza por averiguar quién verifica certificados en tu stack. El consejo de Cloudflare para desarrolladores es mirar por debajo del SDK: un cliente de API puede delegar la verificación en una librería HTTP, en el runtime del lenguaje, en el sistema operativo o en una configuración de confianza propia, así que actualizar solo el SDK puede no cambiar nada.&lt;/p&gt;
&lt;p&gt;Luego ve lugar por lugar:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Imágenes base de contenedores.&lt;/strong&gt;&amp;nbsp;Reconstruye desde una base nueva y vuelve a desplegar. Una imagen nueva en disco no hace nada por un contenedor que sigue ejecutando la anterior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust stores del sistema.&lt;/strong&gt;&amp;nbsp;En imágenes basadas en Debian puedes al menos ver si las raíces emSign siguen instaladas. Tómalo como punto de partida, no como veredicto, porque la corrección puede estar en un store o verificador actualizado y no en la ausencia de la raíz.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Archivos de CA propios de cada herramienta.&lt;/strong&gt;&amp;nbsp;Herramientas como curl que traen su propio bundle necesitan su propia actualización.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuración de mTLS.&lt;/strong&gt;&amp;nbsp;Revisa en qué CA confías para verificar certificados de cliente. Confía en las que de verdad usas para emitir, no en un bundle público completo, si tus integraciones lo permiten. Te cuesta flexibilidad cuando un socio cambia de CA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificados de origen.&lt;/strong&gt;&amp;nbsp;Si usas los modos Full (strict) o Strict de Cloudflare, revisa tus certificados de origen personalizados. Cloudflare dice que Automatic SSL/TLS te avisará si sigues usando los que ya no son de confianza.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# Imagen basada en Debian/Ubuntu: ¿siguen instaladas las raíces emSign?&lt;br&gt;
ls /etc/ssl/certs | grep -i emsign

&lt;h1&gt;
  
  
  Reconstruye desde una imagen base nueva y vuelve a desplegar
&lt;/h1&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;docker build --pull --no-cache -t myapp:fixed .&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;Cuando apliques el parche, puede que empieces a ver errores de certificado en tus paneles. La advertencia de Cloudflare aquí es directa y la comparto: no los resuelvas desactivando la verificación.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Tentador a las 2 a. m. cuando una llamada empieza a fallar. No lo hagas.&lt;br&gt;&lt;br&gt;
requests.get(url, verify=False)&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esa línea elimina justo la autenticación que la actualización debía restaurar. Averigua qué cadena presenta en realidad la llamada que falla y corrige el trust store.&lt;/p&gt;
&lt;p&gt;Por último, prueba por el camino que tu aplicación usa de verdad. El&amp;nbsp;&lt;a href="https://emudhra.we-have-a-problem.com/" rel="noopener noreferrer"&gt;sitio de pruebas&lt;/a&gt;&amp;nbsp;de Jackson cubre navegadores y clientes que no son navegadores. Lee con cuidado un fallo, porque la página advierte que un error de red se ve igual que un rechazo de certificado. El aviso que hay detrás también sigue las versiones corregidas en distribuciones de Linux, bundles de CA como certifi, runtimes de lenguajes (entre ellos Node.js, Python y Go) y herramientas como Docker. Algunas son dependencias indirectas, así que revisa tu árbol de dependencias. Trátala como tu tabla de consulta, y trata sus entradas pendientes como abiertas: cuando la leí el 24 de octubre, algunas seguían marcadas como pendientes, por ejemplo RHEL 7 y Bottlerocket. Cloudflare es explícito en que una prueba exitosa en el navegador no demuestra que un servicio en segundo plano esté a salvo, y una prueba con curl tampoco, si la aplicación usa otro verificador.&lt;/p&gt;
&lt;p&gt;Si usas Tunnel y WARP como describí en&amp;nbsp;&lt;a href="https://blog.ifirestone.dev/exponer-entornos-de-desarrollo-con-cloudflare-tunnel-y-warp-sin-abrir-puertos/" rel="noopener noreferrer"&gt;Exponer entornos de desarrollo con Cloudflare Tunnel y WARP sin abrir puertos&lt;/a&gt;, actualiza cloudflared a 2026.10.2 y aplica las actualizaciones correspondientes del cliente WARP. El post de Cloudflare lista las versiones corregidas por plataforma.&lt;/p&gt;
&lt;h2 id="lo-que-cost%C3%B3-y-lo-que-deja"&gt;Lo que costó y lo que deja&lt;/h2&gt;
&lt;p&gt;El 22 de octubre, eMudhra anunció que sale del negocio global de certificados SSL/TLS. Su&amp;nbsp;&lt;a href="https://emudhra.com/en/press-release/emudhra-to-exit-global-ssl-tls-certificate-business" rel="noopener noreferrer"&gt;comunicado&lt;/a&gt;, citado por The Register, dice que ese negocio es cerca del 1 por ciento de sus ingresos globales y que el escrutinio fue desproporcionado respecto a su peso en la empresa. Estima que unos 30.000 certificados necesitan migrarse y dice que los servicios de revocación y de estado seguirán funcionando hasta que todos los certificados emitidos hayan expirado o se hayan reemplazado.&lt;/p&gt;
&lt;p&gt;También dice que la infraestructura afectada pasó dos años de auditorías independientes y estaba en las listas de confianza de los navegadores desde mediados de 2025. Si eso es exacto, es la parte incómoda. Las auditorías no detectaron una clave privada que era el número 4.&lt;/p&gt;
&lt;p&gt;No creo que la lección sea que una autoridad fue descuidada. La lección es que la mayor parte de nuestro software trata un trust store como una lista estática de raíces, cuando el modelo de amenazas exige que se comporte como un feed de revocación. Cloudflare señala proyectos como&amp;nbsp;&lt;a href="https://discourse.ubuntu.com/t/an-update-on-upki/77063" rel="noopener noreferrer"&gt;upki&lt;/a&gt;, de Canonical, que tratan el trust store como algo que debe actualizarse rápido con información de revocación y de certificate transparency. Es una dirección, no algo que puedas instalar el lunes.&lt;/p&gt;
&lt;p&gt;Lo que sí puedes hacer el lunes es abrir el archivo. En algún punto de tu stack hay una lista de en quién confías, y nadie la ha mirado desde el día en que se construyó la imagen. El número 4 estuvo a la vista de todos unos 18 meses. La lista probablemente es más vieja.&lt;/p&gt;
&lt;p&gt;La misma semana tuvo un fallo muy distinto en el otro extremo de la escala: un parche que se saltó versiones que la gente seguía ejecutando. Escribí sobre su costo en&amp;nbsp;&lt;a href="https://blog.ifirestone.dev/cuando-el-parche-se-salta-tu-version-lo-que-costo-php2shell-a-los-gobiernos-y-lo-que-anade-el-eol/" rel="noopener noreferrer"&gt;Cuando el parche se salta tu versión: lo que costó PHP2Shell a los gobiernos y lo que añade el EOL&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="fuentes"&gt;Fuentes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://blog.cloudflare.com/emudhra-certificate-compromise/" rel="noopener noreferrer"&gt;8 eMudhra certificates are compromised. Here's what you need to know&lt;/a&gt;&amp;nbsp;(Cloudflare Blog, en inglés)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.theregister.com/security/2026/10/22/disgraced-indian-cert-issuer-leaves-the-cert-issuing-business/5304848" rel="noopener noreferrer"&gt;Disgraced Indian cert issuer leaves the cert issuing business&lt;/a&gt;&amp;nbsp;(The Register, en inglés)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.itnews.com.au/news/cloudflare-warns-fixing-botched-emudhra-certs-goes-beyond-browsers-629804" rel="noopener noreferrer"&gt;Cloudflare warns fixing botched eMudhra certs goes beyond browsers&lt;/a&gt;&amp;nbsp;(iTnews, en inglés)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://emudhra.we-have-a-problem.com/" rel="noopener noreferrer"&gt;Advisory and checker for the compromised eMudhra intermediate CAs&lt;/a&gt;&amp;nbsp;(Dennis Jackson,&amp;nbsp;&lt;a href="http://we-have-a-problem.com/" rel="noopener noreferrer"&gt;we-have-a-problem.com&lt;/a&gt;, en inglés)&lt;/li&gt;
&lt;/ul&gt;


</description>
    </item>
    <item>
      <title>AI Skills for Log Analysis: How I Structured a Reproducible Diagnosis</title>
      <dc:creator>Ivan Firestone</dc:creator>
      <pubDate>Sat, 24 Oct 2026 13:00:07 +0000</pubDate>
      <link>https://dev.to/ifirestone/ai-skills-for-log-analysis-how-i-structured-a-reproducible-diagnosis-21o0</link>
      <guid>https://dev.to/ifirestone/ai-skills-for-log-analysis-how-i-structured-a-reproducible-diagnosis-21o0</guid>
      <description>&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%2Fimages.unsplash.com%2Fphoto-1694903089438-bf28d4697d9a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDJ8fGFpJTIwc2tpbGxzfGVufDB8fHx8MTc5Mjc3OTc4NXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1694903089438-bf28d4697d9a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDJ8fGFpJTIwc2tpbGxzfGVufDB8fHx8MTc5Mjc3OTc4NXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="AI Skills for Log Analysis: How I Structured a Reproducible Diagnosis" width="2000" height="1125"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Analyzing logs with an AI assistant works better when the assistant follows a &lt;strong&gt;procedure&lt;/strong&gt; rather than improvising. I gathered my instructions in &lt;a href="https://github.com/ifirestone/ia-tools" rel="noopener noreferrer"&gt;&lt;code&gt;ia-tools&lt;/code&gt;&lt;/a&gt;: a collection of &lt;em&gt;skills&lt;/em&gt; and agents, in Spanish, for Claude Code, Codex and other harnesses compatible with the open Agent Skills standard.&lt;/p&gt;
&lt;p&gt;This article explains how they're designed, why I split them this way and what limits they have.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Pasting a stack trace into a chat and asking "what's failing?" gives uneven results:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The assistant assumes technology and version without asking.&lt;/li&gt;
&lt;li&gt;Each answer has a different format, hard to compare or move into a ticket.&lt;/li&gt;
&lt;li&gt;Sensitive data (tokens, credentials, personal data) slips through without anyone flagging it.&lt;/li&gt;
&lt;li&gt;Diagnosis happens without knowing the environment (production or development, server role, version).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A useful diagnosis always follows the same steps. If the steps are always the same, they can be written once.&lt;/p&gt;
&lt;h2 id="what-a-skill-is"&gt;What a Skill Is&lt;/h2&gt;
&lt;p&gt;A skill is a folder with a &lt;code&gt;SKILL.md&lt;/code&gt; file: instructions the model loads automatically when the conversation matches its &lt;code&gt;description&lt;/code&gt;, or that you invoke manually with &lt;code&gt;/name&lt;/code&gt;. Long reference material lives in &lt;code&gt;references/&lt;/code&gt; and is loaded only when needed.&lt;/p&gt;
&lt;p&gt;That on-demand loading is the most important design decision: &lt;strong&gt;don't put everything in the context&lt;/strong&gt;. A single giant skill with rules for twenty technologies would consume context and dilute the instructions.&lt;/p&gt;
&lt;h2 id="how-its-organized"&gt;How It's Organized&lt;/h2&gt;
&lt;p&gt;One main skill and several technology-specific ones:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;log-analyzer&lt;/code&gt;:&lt;/strong&gt; detects the log's technology from its patterns, requests the context it can't infer and produces a standardized diagnosis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills by technical family:&lt;/strong&gt; cloud (Azure, AWS, GCP), databases (PostgreSQL, Oracle, SQL Server), Docker, .NET, Kafka, Kubernetes and OpenShift, Laravel, PHP, Spring Boot, Quarkus, web servers, Windows Event Log, MinIO, IBM API Connect and SRE alerts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The main one decides which reference to load. That way the assistant brings into context only what's relevant for that log.&lt;/p&gt;
&lt;h2 id="the-diagnosis-flow"&gt;The Diagnosis Flow&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detect the technology&lt;/strong&gt; from the log's own patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask for what it can't infer:&lt;/strong&gt; environment, version, server role. Diagnosing without that context leads to confident and wrong conclusions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analyze&lt;/strong&gt; using the references specific to that technology.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deliver a report with a fixed structure,&lt;/strong&gt; so it's comparable across incidents and easy to attach to a ticket.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="a-deliberate-decision-flag-dont-hide"&gt;A Deliberate Decision: Flag, Don't Hide&lt;/h2&gt;
&lt;p&gt;The skills &lt;strong&gt;do not censor&lt;/strong&gt; credentials, tokens or personal data that appear in the log. They &lt;strong&gt;flag them in red&lt;/strong&gt; in the report.&lt;/p&gt;
&lt;p&gt;The reason is practical: if the log contains a secret, what you need is to &lt;strong&gt;rotate&lt;/strong&gt; it, and for that you have to know it's there. Silently hiding it leaves you with a false sense of security. The cost is that the report may contain sensitive data, so you must review it before sharing.&lt;/p&gt;
&lt;p&gt;A complementary rule: &lt;strong&gt;don't paste production logs into external tools without your organization's authorization.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="agents-a-different-piece"&gt;Agents: A Different Piece&lt;/h2&gt;
&lt;p&gt;In addition to the skills, the repository includes four agents for Claude Code (independent roles with their own instructions):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Drafting corporate communications.&lt;/li&gt;
&lt;li&gt;Turning notes or transcripts into meeting minutes.&lt;/li&gt;
&lt;li&gt;Extracting actionable decisions, tasks and risks.&lt;/li&gt;
&lt;li&gt;Translation, proofreading and tone adjustment.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They are generic templates written for Spanish-speaking teams; the idea is that you adapt them to your organization.&lt;/p&gt;
&lt;h2 id="installation"&gt;Installation&lt;/h2&gt;
&lt;p&gt;In Claude Code, as a plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/plugin install ia-tools --marketplace ifirestone/ia-tools&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For Codex and other compatible harnesses, the repository includes a script that links each skill into your skills directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./scripts/link-skills.sh&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since they are symbolic links to the repository, a &lt;code&gt;git pull&lt;/code&gt; keeps everything up to date. Keep in mind that the script replaces existing folders with the same skill name, so review it first if you have skills of your own.&lt;/p&gt;
&lt;p&gt;ChatGPT has no native mechanism for loading skills: you can paste the content of a &lt;code&gt;SKILL.md&lt;/code&gt; as the instructions of a Project or custom GPT and upload the references as knowledge files.&lt;/p&gt;
&lt;h2 id="limits-to-keep-clear"&gt;Limits to Keep Clear&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A diagnosis is a hypothesis, not a verdict.&lt;/strong&gt; The assistant can be wrong, especially with partial logs or without context. Verify before acting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality depends on the log you give it.&lt;/strong&gt; A fragment without the root error line produces weak conclusions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance.&lt;/strong&gt; Technologies change versions and error messages; the references must be kept up to date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data sensitivity.&lt;/strong&gt; Flagging is not protecting. The responsibility of not exposing information is still yours.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-i-learned-designing-it"&gt;What I Learned Designing It&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A fixed procedure is worth more than a clever prompt.&lt;/strong&gt; Consistency is what makes the diagnosis useful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asking before diagnosing&lt;/strong&gt; prevents most falsely confident answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Splitting by technology&lt;/strong&gt; keeps the context small and the instructions precise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing in the team's language&lt;/strong&gt; reduces friction when the report is read by operations or support people.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Skills turn a good diagnostic practice into something repeatable and shareable. They don't replace the engineer's judgment; they save the mechanical work of the first analysis.&lt;/p&gt;
&lt;p&gt;The repository is open source (MIT). If you add a skill for another technology, the contribution guide explains the structure.&lt;/p&gt;


</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Laravel con Docker Compose: cinco errores de infraestructura que vale la pena evitar</title>
      <dc:creator>Ivan Firestone</dc:creator>
      <pubDate>Sat, 24 Oct 2026 12:00:06 +0000</pubDate>
      <link>https://dev.to/ifirestone/laravel-con-docker-compose-cinco-errores-de-infraestructura-que-vale-la-pena-evitar-1mef</link>
      <guid>https://dev.to/ifirestone/laravel-con-docker-compose-cinco-errores-de-infraestructura-que-vale-la-pena-evitar-1mef</guid>
      <description>&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%2Fimages.unsplash.com%2Fphoto-1461749280684-dccba630e2f6%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDQ5fHx0ZWNobm9sb2d5fGVufDB8fHx8MTc5Mjc4MDQ4Nnww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1461749280684-dccba630e2f6%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDQ5fHx0ZWNobm9sb2d5fGVufDB8fHx8MTc5Mjc4MDQ4Nnww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="Laravel con Docker Compose: cinco errores de infraestructura que vale la pena evitar" width="2000" height="1335"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;blockquote&gt;Nombres de servicio, healthchecks de Postgres, un scheduler sin tabla de caché, colas y migraciones al desplegar: los errores que aparecen al llevar Laravel a Docker Compose y cómo evitarlos.&lt;/blockquote&gt;
&lt;h2 id="resumen"&gt;Resumen&lt;/h2&gt;
&lt;p&gt;Llevar una aplicación Laravel a Docker Compose parece trivial hasta que el primer despliegue falla por razones que no están en tu código. Estos son cinco errores de infraestructura que encontré depurando proyectos reales, con el síntoma, la causa y la solución.&lt;/p&gt;
&lt;h2 id="1-el-nombre-del-servicio-no-coincide-con-dbhost"&gt;1. El nombre del servicio no coincide con &lt;code&gt;DB_HOST&lt;/code&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Síntoma:&lt;/strong&gt; la aplicación no puede conectarse a la base de datos: &lt;code&gt;getaddrinfo failed&lt;/code&gt;, &lt;code&gt;Connection refused&lt;/code&gt; o &lt;code&gt;Name or service not known&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Causa:&lt;/strong&gt; en una red de Compose, los contenedores se resuelven por el &lt;strong&gt;nombre del servicio&lt;/strong&gt;. Si tu servicio se llama &lt;code&gt;postgres&lt;/code&gt; en &lt;code&gt;docker-compose.yml&lt;/code&gt; pero el &lt;code&gt;.env&lt;/code&gt; dice &lt;code&gt;DB_HOST=db&lt;/code&gt;, no hay nada que resolver. Pasa mucho al renombrar servicios o al copiar un &lt;code&gt;.env&lt;/code&gt; de otro proyecto.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solución:&lt;/strong&gt; que el valor de &lt;code&gt;DB_HOST&lt;/code&gt; sea exactamente el nombre del servicio.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:&lt;br&gt;
  postgres:&lt;br&gt;
    image: postgres:16&lt;br&gt;
  app:&lt;br&gt;
    environment:&lt;br&gt;
      DB_HOST: postgres # el nombre del servicio, no "localhost"&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Un clásico relacionado: usar &lt;code&gt;localhost&lt;/code&gt; dentro del contenedor. Dentro de un contenedor, &lt;code&gt;localhost&lt;/code&gt; es ese mismo contenedor.&lt;/p&gt;
&lt;h2 id="2-healthcheck-de-postgres-con-credenciales-distintas-a-las-reales"&gt;2. Healthcheck de Postgres con credenciales distintas a las reales&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Síntoma:&lt;/strong&gt; el contenedor de la base de datos queda &lt;code&gt;unhealthy&lt;/code&gt; y los servicios que dependen de él nunca arrancan. En los logs de Postgres: &lt;code&gt;FATAL: role "postgres" does not exist&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Causa:&lt;/strong&gt; el healthcheck usa un usuario o una base de datos distintos de los configurados en las variables de entorno. Si defines &lt;code&gt;POSTGRES_USER=laravel&lt;/code&gt; y tu healthcheck ejecuta &lt;code&gt;pg_isready -U postgres&lt;/code&gt;, estás comprobando contra un rol que no existe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solución:&lt;/strong&gt; que el healthcheck use las mismas variables, y escapar el &lt;code&gt;$&lt;/code&gt; para que lo resuelva el contenedor y no Compose:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:&lt;br&gt;
  postgres:&lt;br&gt;
    image: postgres:16&lt;br&gt;
    environment:&lt;br&gt;
      POSTGRES_USER: ${DB_USERNAME}&lt;br&gt;
      POSTGRES_PASSWORD: ${DB_PASSWORD}&lt;br&gt;
      POSTGRES_DB: ${DB_DATABASE}&lt;br&gt;
    healthcheck:&lt;br&gt;
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]&lt;br&gt;
      interval: 5s&lt;br&gt;
      timeout: 3s&lt;br&gt;
      retries: 10

&lt;/code&gt;&lt;p&gt;&lt;code&gt;app:&lt;br&gt;&lt;br&gt;
    depends_on:&lt;br&gt;&lt;br&gt;
      postgres:&lt;br&gt;&lt;br&gt;
        condition: service_healthy&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;Con &lt;code&gt;depends_on&lt;/code&gt; y &lt;code&gt;service_healthy&lt;/code&gt;, la app espera hasta que la base de datos realmente acepta conexiones, no solo hasta que el contenedor existe.&lt;/p&gt;
&lt;h2 id="3-el-scheduler-falla-porque-la-tabla-de-cach%C3%A9-no-existe"&gt;3. El scheduler falla porque la tabla de caché no existe&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Síntoma:&lt;/strong&gt; el scheduler lanza errores relacionados con la tabla &lt;code&gt;cache&lt;/code&gt; o &lt;code&gt;cache_locks&lt;/code&gt;, sobre todo cuando usas tareas con &lt;code&gt;withoutOverlapping()&lt;/code&gt; o &lt;code&gt;onOneServer()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Causa:&lt;/strong&gt; con &lt;code&gt;CACHE_STORE=database&lt;/code&gt;, Laravel guarda la caché y los locks en tablas. Si esas tablas no existen (la migración no se ejecutó o la base de datos es nueva), el scheduler falla al intentar adquirir un lock.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solución:&lt;/strong&gt; asegúrate de que la tabla de caché esté migrada. En Laravel 11 o superior:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;php artisan make:cache-table&lt;br&gt;&lt;br&gt;
php artisan migrate&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(En versiones anteriores el comando era &lt;code&gt;cache:table&lt;/code&gt;.) Y ejecuta el scheduler como un proceso propio:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;php artisan schedule:work&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="4-todos-los-procesos-en-un-solo-contenedor"&gt;4. Todos los procesos en un solo contenedor&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Síntoma:&lt;/strong&gt; en desarrollo todo funciona, y en producción las colas se detienen en silencio, los reinicios son impredecibles y no puedes escalar solo los &lt;em&gt;workers&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Causa:&lt;/strong&gt; en desarrollo es normal usar &lt;code&gt;concurrently&lt;/code&gt; para levantar el servidor web, la cola y el scheduler en una sola terminal. Llevar ese patrón a producción mete varios procesos en un contenedor, y Docker solo vigila el proceso principal: si la cola muere, nadie se entera.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solución:&lt;/strong&gt; un proceso por contenedor, con la misma imagen y un comando distinto:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:&lt;br&gt;&lt;br&gt;
  app:&lt;br&gt;&lt;br&gt;
    image: myapp:latest&lt;br&gt;&lt;br&gt;
    command: php-fpm

&lt;p&gt;queue:&lt;br&gt;
    image: myapp:latest&lt;br&gt;
    command: php artisan queue:work --tries=3 --timeout=90&lt;br&gt;
    restart: unless-stopped&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;scheduler:&lt;br&gt;&lt;br&gt;
    image: myapp:latest&lt;br&gt;&lt;br&gt;
    command: php artisan schedule:work&lt;br&gt;&lt;br&gt;
    restart: unless-stopped&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;Ventajas: reinicios independientes, escalado de workers por separado, logs separados y señales de parada manejadas por Docker.&lt;/p&gt;
&lt;h2 id="5-las-migraciones-se-ejecutan-en-cada-arranque-del-contenedor"&gt;5. Las migraciones se ejecutan en cada arranque del contenedor&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Síntoma:&lt;/strong&gt; al desplegar con varias réplicas aparecen errores intermitentes de migración, bloqueos en la base de datos o tablas creadas a medias.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Causa:&lt;/strong&gt; poner &lt;code&gt;php artisan migrate --force&lt;/code&gt; en el &lt;code&gt;entrypoint&lt;/code&gt; hace que &lt;strong&gt;cada&lt;/strong&gt; contenedor intente migrar al iniciar. Con varias réplicas, compiten entre sí.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solución:&lt;/strong&gt; las migraciones son un paso del despliegue, no del arranque de la aplicación. Ejecútalas &lt;strong&gt;una sola vez&lt;/strong&gt;, como tarea previa en el pipeline de CI/CD o como un servicio de una sola ejecución:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:&lt;br&gt;&lt;br&gt;
  migrate:&lt;br&gt;&lt;br&gt;
    image: myapp:latest&lt;br&gt;&lt;br&gt;
    command: php artisan migrate --force&lt;br&gt;&lt;br&gt;
    restart: "no"&lt;br&gt;&lt;br&gt;
    depends_on:&lt;br&gt;&lt;br&gt;
      postgres:&lt;br&gt;&lt;br&gt;
        condition: service_healthy&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y haz que &lt;code&gt;app&lt;/code&gt;, &lt;code&gt;queue&lt;/code&gt; y &lt;code&gt;scheduler&lt;/code&gt; arranquen después de que la migración termine con éxito.&lt;/p&gt;
&lt;h2 id="lista-de-verificaci%C3%B3n-r%C3%A1pida"&gt;Lista de verificación rápida&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;DB_HOST&lt;/code&gt; coincide con el nombre del servicio, nunca &lt;code&gt;localhost&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] El healthcheck de la base de datos usa las mismas credenciales que el servicio.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;depends_on&lt;/code&gt; usa &lt;code&gt;condition: service_healthy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] La tabla de caché existe si usas &lt;code&gt;CACHE_STORE=database&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Un proceso por contenedor: web, cola y scheduler separados.&lt;/li&gt;
&lt;li&gt;[ ] Las migraciones se ejecutan una vez por despliegue, no en cada arranque.&lt;/li&gt;
&lt;li&gt;[ ] Las imágenes tienen una versión fija, no &lt;code&gt;latest&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusi%C3%B3n"&gt;Conclusión&lt;/h2&gt;
&lt;p&gt;Casi todos estos errores tienen el mismo origen: asumir que la infraestructura se comporta como tu máquina local. Compose tiene reglas claras (resolución por nombre, orden de arranque, un proceso por contenedor). Conocerlas ahorra horas de depuración.&lt;/p&gt;
&lt;p&gt;Si tienes un error recurrente en tu propio despliegue, compártelo en los comentarios: seguro alguien más se lo ha encontrado también.&lt;/p&gt;


</description>
      <category>backend</category>
      <category>devops</category>
      <category>docker</category>
      <category>laravel</category>
    </item>
    <item>
      <title>Exponer entornos de desarrollo con Cloudflare Tunnel y WARP sin abrir puertos</title>
      <dc:creator>Ivan Firestone</dc:creator>
      <pubDate>Fri, 23 Oct 2026 17:18:17 +0000</pubDate>
      <link>https://dev.to/ifirestone/exponer-entornos-de-desarrollo-con-cloudflare-tunnel-y-warp-sin-abrir-puertos-2h5e</link>
      <guid>https://dev.to/ifirestone/exponer-entornos-de-desarrollo-con-cloudflare-tunnel-y-warp-sin-abrir-puertos-2h5e</guid>
      <description>&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%2Fimages.unsplash.com%2Fphoto-1614064641938-3bbee52942c7%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDR8fHNlY3VyaXR5fGVufDB8fHx8MTc5MjYzODk0MHww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1614064641938-3bbee52942c7%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDR8fHNlY3VyaXR5fGVufDB8fHx8MTc5MjYzODk0MHww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="Exponer entornos de desarrollo con Cloudflare Tunnel y WARP sin abrir puertos" width="2000" height="1334"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;p&gt;Necesitaba un entorno de desarrollo con &lt;strong&gt;despliegues frecuentes&lt;/strong&gt;, URLs públicas con HTTPS y acceso cómodo a las bases de datos para depurar. Lo que no quería era un servidor en Internet con los puertos 80 y 443 abiertos, esperando a que alguien lo escanee.&lt;/p&gt;
&lt;p&gt;Esos requisitos tiran en direcciones opuestas: mientras más fácil el acceso, mayor la exposición. Esta es la historia de la combinación que me funcionó:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Tunnel&lt;/strong&gt; para publicar las apps sin abrir los puertos 80 y 443.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traefik&lt;/strong&gt; como enrutador interno, con labels por aplicación.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portainer&lt;/strong&gt; para administrar los contenedores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare One WARP&lt;/strong&gt; para llegar a la red privada de Docker solo desde mi máquina.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Voy a explicar el diseño, el detalle que rompe Laravel detrás del proxy y, sobre todo, los riesgos.&lt;/p&gt;
&lt;h2 id="la-idea-que-el-servidor-llame-hacia-afuera"&gt;La idea: que el servidor llame hacia afuera&lt;/h2&gt;
&lt;p&gt;La forma habitual de publicar una app es abrir un puerto y dejar que el mundo se conecte. El túnel invierte eso:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Internet → Cloudflare (TLS) → túnel (conexión saliente) → Traefik → contenedores&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La clave: el conector &lt;code&gt;cloudflared&lt;/code&gt; &lt;strong&gt;abre la conexión hacia Cloudflare&lt;/strong&gt;, y no al revés. El servidor no necesita puertos de entrada abiertos. Cloudflare termina el TLS, y dentro del servidor el tráfico va por HTTP hacia Traefik.&lt;/p&gt;
&lt;p&gt;Eso resuelve la exposición y los certificados de un solo golpe. Pero un túnel solo lleva el tráfico hasta la puerta; algo tiene que decidir qué contenedor responde.&lt;/p&gt;
&lt;h2 id="elegir-el-enrutador"&gt;Elegir el enrutador&lt;/h2&gt;
&lt;p&gt;Despliego con mucha frecuencia. Con Traefik, cada aplicación declara su enrutamiento con labels en su propio &lt;code&gt;docker-compose&lt;/code&gt;, y no hay configuración central que tocar por cada proyecto nuevo.&lt;/p&gt;
&lt;p&gt;Antes evalué Dokploy para desarrollo. Es cómodo, pero me pareció pesado en recursos para un servidor pequeño. El camino que elegí (Portainer + Traefik + cloudflared) es más liviano, al precio de &lt;strong&gt;escribir a mano los labels de Traefik&lt;/strong&gt;. Es un intercambio entre comodidad y consumo.&lt;/p&gt;
&lt;h2 id="armar-las-piezas"&gt;Armar las piezas&lt;/h2&gt;
&lt;p&gt;La base es una red compartida, Traefik sin HTTPS (de eso se encarga Cloudflare) y &lt;code&gt;cloudflared&lt;/code&gt; con el token del túnel:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:&lt;br&gt;
  traefik:&lt;br&gt;
    image: traefik:v3&lt;br&gt;
    command:&lt;br&gt;
      - --providers.docker=true&lt;br&gt;
      - --providers.docker.exposedbydefault=false&lt;br&gt;
      - --entrypoints.web.address=:80&lt;br&gt;
    volumes:&lt;br&gt;
      - /var/run/docker.sock:/var/run/docker.sock:ro&lt;br&gt;
    networks: [edge]

&lt;p&gt;cloudflared:&lt;br&gt;
    image: cloudflare/cloudflared:latest&lt;br&gt;
    command: tunnel --no-autoupdate run&lt;br&gt;
    environment:&lt;br&gt;
      - TUNNEL_TOKEN=${TUNNEL_TOKEN}&lt;br&gt;
    networks: [edge]&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;networks:&lt;br&gt;&lt;br&gt;
  edge:&lt;br&gt;&lt;br&gt;
    name: edge&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;Fíjate en que &lt;strong&gt;ningún servicio publica puertos&lt;/strong&gt; (&lt;code&gt;ports:&lt;/code&gt;). Es intencional y es la mejor defensa: si no se publica ningún puerto, no hay nada que puedas exponer por accidente.&lt;/p&gt;
&lt;p&gt;En el panel de Cloudflare configuras el &lt;em&gt;public hostname&lt;/em&gt; (por ejemplo, un comodín bajo tu dominio) apuntando al servicio &lt;code&gt;&lt;a href="http://traefik:80" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="http://traefik:80" rel="noopener noreferrer"&gt;http://traefik:80&lt;/a&gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A partir de ahí, cada app nueva solo se une a la misma red y declara su enrutamiento:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:&lt;br&gt;&lt;br&gt;
  myapp:&lt;br&gt;&lt;br&gt;
    image: myapp:dev&lt;br&gt;&lt;br&gt;
    networks: [edge]&lt;br&gt;&lt;br&gt;
    labels:&lt;br&gt;&lt;br&gt;
      - traefik.enable=true&lt;br&gt;&lt;br&gt;
      - traefik.http.routers.myapp.rule=Host(&lt;code&gt;myapp.dev.example.com&lt;/code&gt;)&lt;br&gt;&lt;br&gt;
      - traefik.http.routers.myapp.entrypoints=web&lt;br&gt;&lt;br&gt;
      - traefik.http.services.myapp.loadbalancer.server.port=80

&lt;/code&gt;&lt;p&gt;&lt;code&gt;networks:&lt;br&gt;&lt;br&gt;
  edge:&lt;br&gt;&lt;br&gt;
    external: true&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;Con eso, una app queda en vivo por HTTPS. Pero si es una app Laravel, hay un detalle.&lt;/p&gt;
&lt;h2 id="el-detalle-que-rompe-laravel"&gt;El detalle que rompe Laravel&lt;/h2&gt;
&lt;p&gt;Como el TLS termina en Cloudflare y Traefik habla HTTP con la app, Laravel ve peticiones HTTP. Los síntomas típicos: URLs generadas con &lt;code&gt;http://&lt;/code&gt;, redirecciones rotas y &lt;em&gt;mixed content&lt;/em&gt; (contenido mixto).&lt;/p&gt;
&lt;p&gt;Se arregla confiando en los headers &lt;code&gt;X-Forwarded-&lt;em&gt;&lt;/em&gt;&lt;/code&gt;&lt;em&gt; del proxy y forzando HTTPS. En Laravel 11 o superior, en &lt;code&gt;bootstrap/app.php&lt;/code&gt;:&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;em&gt;&lt;code&gt;-&amp;gt;withMiddleware(function (Middleware $middleware) {&lt;br&gt;&lt;br&gt;
    $middleware-&amp;gt;trustProxies(at: '&lt;/code&gt;&lt;/em&gt;&lt;code&gt;');&lt;br&gt;&lt;br&gt;
})&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y en el service provider, forzar el esquema:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;URL::forceScheme('https');&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Funciona. Pero mira bien ese &lt;code&gt;'&lt;em&gt;'&lt;/em&gt;&lt;/code&gt;&lt;em&gt;, porque trae un riesgo.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="la-advertencia-importante"&gt;&lt;em&gt;La advertencia importante&lt;/em&gt;&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;&lt;code&gt;trustProxies(at: '&lt;/code&gt;&lt;/em&gt;&lt;code&gt;')&lt;/code&gt; significa "confío en los headers de &lt;strong&gt;cualquiera&lt;/strong&gt;". Solo es seguro &lt;strong&gt;si el origen es inalcanzable salvo a través del túnel&lt;/strong&gt;. Si alguien puede llegar directo al contenedor, puede falsificar &lt;code&gt;X-Forwarded-For&lt;/code&gt; o &lt;code&gt;X-Forwarded-Proto&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Por eso conviene combinar tres cosas:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;No publicar los puertos de las apps.&lt;/li&gt;
&lt;li&gt;Cerrar 80/443 en el firewall.&lt;/li&gt;
&lt;li&gt;Limitar en Traefik qué IPs son de confianza para los headers reenviados (&lt;code&gt;forwardedHeaders.trustedIPs&lt;/code&gt; en el &lt;em&gt;entrypoint&lt;/em&gt;), usando la red interna del túnel.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Lo que lleva a la pregunta de la que depende todo el diseño: ¿está realmente cerrado el origen?&lt;/p&gt;
&lt;h2 id="cerrar-los-puertos-de-verdad"&gt;Cerrar los puertos de verdad&lt;/h2&gt;
&lt;p&gt;Hay capas, de la más a la menos confiable:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No publiques puertos en Docker.&lt;/strong&gt; Sin &lt;code&gt;ports:&lt;/code&gt;, no hay nada que filtrar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firewall del proveedor de nube&lt;/strong&gt; (reglas de seguridad de red): permite solo lo estrictamente necesario, por ejemplo SSH desde tu IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reglas en el host&lt;/strong&gt; como defensa adicional. Ojo: Docker manipula &lt;code&gt;iptables&lt;/code&gt; por su cuenta y puede saltarse reglas pensadas para firewalls de nivel superior; para filtrar el tráfico hacia contenedores publicados, la cadena &lt;code&gt;DOCKER-USER&lt;/code&gt; es el lugar previsto.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Verifica siempre &lt;strong&gt;desde afuera&lt;/strong&gt; con un escaneo de puertos, no solo leyendo la configuración.&lt;/p&gt;
&lt;h2 id="la-%C3%BAltima-pieza-llegar-a-las-bases-de-datos"&gt;La última pieza: llegar a las bases de datos&lt;/h2&gt;
&lt;p&gt;Las apps eran públicas y los puertos estaban cerrados. Pero para depurar yo seguía queriendo conectarme a las bases de datos del entorno, sin exponerlas a nadie.&lt;/p&gt;
&lt;p&gt;Ahí entra &lt;strong&gt;Cloudflare One WARP&lt;/strong&gt;. Con una ruta de red privada a través del mismo túnel, mi máquina llega a la red de Docker como si estuviera dentro. Esto permite:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Darles a las bases de datos una &lt;strong&gt;IP fija&lt;/strong&gt; en la red interna y conectarme con mi cliente de siempre.&lt;/li&gt;
&lt;li&gt;Que solo los &lt;strong&gt;dispositivos autorizados&lt;/strong&gt; en la política de Zero Trust puedan acceder.&lt;/li&gt;
&lt;li&gt;No abrir ningún puerto de base de datos hacia afuera.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="producci%C3%B3n-es-otra-decisi%C3%B3n"&gt;Producción es otra decisión&lt;/h2&gt;
&lt;p&gt;¿Se traslada la misma configuración a producción? No del todo. En producción uso Portainer &lt;strong&gt;sin Traefik&lt;/strong&gt;: el túnel apunta directamente al contenedor de cada aplicación. Menos piezas, menos superficie y menos cosas que pueden fallar. Traefik me resuelve un problema de &lt;strong&gt;velocidad de despliegue&lt;/strong&gt; en desarrollo; en producción, donde los cambios son menos frecuentes, ese problema no existe.&lt;/p&gt;
&lt;h2 id="lo-que-cedes"&gt;Lo que cedes&lt;/h2&gt;
&lt;p&gt;Nada de esto es gratis:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dependencia de Cloudflare.&lt;/strong&gt; Si el servicio tiene una caída, tus entornos quedan inalcanzables. Para desarrollo es tolerable; para producción hay que evaluarlo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El token del túnel es un secreto.&lt;/strong&gt; Quien lo tenga puede ejecutar un conector. Guárdalo como secreto, no en el repositorio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El socket de Docker.&lt;/strong&gt; Traefik lo monta en solo lectura, pero sigue siendo un acceso sensible. Hay alternativas con un proxy de socket que limita las operaciones permitidas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Labels escritos a mano.&lt;/strong&gt; Es más trabajo por proyecto que un panel integrado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mantenimiento de WARP.&lt;/strong&gt; Hay que gestionar el enrolamiento de dispositivos y las políticas de acceso.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versiones fijadas.&lt;/strong&gt; Evita &lt;code&gt;latest&lt;/code&gt; para &lt;code&gt;cloudflared&lt;/code&gt; y Traefik si quieres despliegues reproducibles.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="lo-que-me-llevo"&gt;Lo que me llevo&lt;/h2&gt;
&lt;p&gt;El servidor que quería mantener oculto está oculto: nada escucha en Internet público, y aun así despliego con frecuencia con HTTPS y llego a las bases de datos desde mi propia máquina.&lt;/p&gt;
&lt;p&gt;Si te quedas con pocas cosas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Un túnel saliente elimina la necesidad de abrir puertos y simplifica el TLS.&lt;/li&gt;
&lt;li&gt;No publicar puertos en Docker es la defensa más fuerte y más barata.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;trustProxies(at: '*')&lt;/code&gt; solo es seguro si el origen es inalcanzable por cualquier otra ruta.&lt;/li&gt;
&lt;li&gt;Traefik acelera el desarrollo; en producción, menos piezas suelen ser mejor.&lt;/li&gt;
&lt;li&gt;Verifica la exposición desde afuera, no solo en la configuración.&lt;/li&gt;
&lt;/ul&gt;


</description>
      <category>cloud</category>
      <category>infrastructure</category>
      <category>networking</category>
      <category>security</category>
    </item>
  </channel>
</rss>
