<?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: Vultr</title>
    <description>The latest articles on DEV Community by Vultr (vultr).</description>
    <link>https://dev.to/vultr</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%2Forganization%2Fprofile_image%2F13147%2Fb80ac1b8-cd21-44b9-9bf8-38224e413737.png</url>
      <title>DEV Community: Vultr</title>
      <link>https://dev.to/vultr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vultr"/>
    <language>en</language>
    <item>
      <title>Deploying Typebot - Open-Source Conversational Form Builder</title>
      <dc:creator>Sanskriti Harmukh</dc:creator>
      <pubDate>Wed, 23 Sep 2026 19:27:14 +0000</pubDate>
      <link>https://dev.to/vultr/deploying-typebot-open-source-conversational-form-builder-52j3</link>
      <guid>https://dev.to/vultr/deploying-typebot-open-source-conversational-form-builder-52j3</guid>
      <description>&lt;p&gt;&lt;a href="https://typebot.io/" rel="noopener noreferrer"&gt;Typebot&lt;/a&gt; is an open-source, visually-driven conversational form and chatbot builder. It serves as a self-hosted alternative to hosted form and chatbot builders, giving you full data ownership and control over integrations and embedding. This guide deploys Typebot on a Linux server using Docker Compose with PostgreSQL, Redis, and Traefik for reverse proxy and TLS termination. By the end, you'll have a working Typebot instance with a published bot embedded on a sample page.&lt;/p&gt;




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

&lt;p&gt;Before you begin, you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have access to a Linux-based server as a non-root user with &lt;code&gt;sudo&lt;/code&gt; privileges.&lt;/li&gt;
&lt;li&gt;Install Docker and Docker Compose.&lt;/li&gt;
&lt;li&gt;Configure two domain A records pointing to your server, such as &lt;code&gt;builder.example.com&lt;/code&gt; and &lt;code&gt;viewer.example.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Have an email address for Let's Encrypt certificate registration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Set Up the Directory Structure and Environment Variables
&lt;/h2&gt;

&lt;p&gt;To prevent data loss during container restarts or updates, the deployment relies on host-mounted volumes for PostgreSQL, Redis, and TLS certificates. Docker Compose reads secrets, URLs, and credentials from a &lt;code&gt;.env&lt;/code&gt; file in the project directory and substitutes them into the service definitions at startup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create a project directory for the Typebot deployment:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/typebot/&lt;span class="o"&gt;{&lt;/span&gt;pgdata,redisdata,letsencrypt,data&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command creates four subdirectories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pgdata&lt;/code&gt;: Persists PostgreSQL database files.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;redisdata&lt;/code&gt;: Stores Redis data used by Typebot's Redis-backed features, such as sign-in rate limiting and media uploads.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;letsencrypt&lt;/code&gt;: Stores Traefik ACME certificates for automatic HTTPS renewal.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data&lt;/code&gt;: Stores Mailpit local email data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Navigate to the project directory:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/typebot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Generate a strong random encryption secret that is used to encrypt sensitive data such as credentials and bot content:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the output. Use this value for the &lt;code&gt;ENCRYPTION_SECRET&lt;/code&gt; variable in the next steps when creating the &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Store this value securely and never change it once the deployment starts handling real data. Typebot uses &lt;code&gt;ENCRYPTION_SECRET&lt;/code&gt; to encrypt stored credentials, and rotating it makes any previously encrypted data unreadable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;4. Create a &lt;code&gt;.env&lt;/code&gt; file to store the environment variables:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Add the following variables:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;DOMAIN_BUILDER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;builder.example.com&lt;/span&gt;
&lt;span class="py"&gt;DOMAIN_VIEWER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;viewer.example.com&lt;/span&gt;
&lt;span class="py"&gt;LETSENCRYPT_EMAIL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;admin@example.com&lt;/span&gt;

&lt;span class="py"&gt;ENCRYPTION_SECRET&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;YOUR_GENERATED_SECRET&lt;/span&gt;

&lt;span class="py"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;typebot&lt;/span&gt;
&lt;span class="py"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;typebot&lt;/span&gt;
&lt;span class="py"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;STRONG_DATABASE_PASSWORD&lt;/span&gt;
&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}&lt;/span&gt;

&lt;span class="py"&gt;REDIS_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;redis://redis:6379&lt;/span&gt;

&lt;span class="py"&gt;NEXTAUTH_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://${DOMAIN_BUILDER}&lt;/span&gt;
&lt;span class="py"&gt;NEXT_PUBLIC_VIEWER_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://${DOMAIN_VIEWER}&lt;/span&gt;

&lt;span class="py"&gt;ADMIN_EMAIL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;admin@example.com&lt;/span&gt;
&lt;span class="py"&gt;DEFAULT_WORKSPACE_PLAN&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;UNLIMITED&lt;/span&gt;
&lt;span class="py"&gt;DISABLE_SIGNUP&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;

&lt;span class="py"&gt;SMTP_HOST&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;mailpit&lt;/span&gt;
&lt;span class="py"&gt;SMTP_PORT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1025&lt;/span&gt;
&lt;span class="py"&gt;SMTP_SECURE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;NEXT_PUBLIC_SMTP_FROM&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Typebot Notifications &amp;lt;notifications@example.com&amp;gt;"&lt;/span&gt;
&lt;span class="py"&gt;SMTP_IGNORE_TLS&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;SMTP_USERNAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;YOUR_SMTP_USERNAME&lt;/span&gt;
&lt;span class="py"&gt;SMTP_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;YOUR_SMTP_PASSWORD&lt;/span&gt;


&lt;span class="py"&gt;TYPEBOT_DEBUG&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;AUTH_TRUST_HOST&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DEFAULT_WORKSPACE_PLAN=UNLIMITED&lt;/code&gt; applies the unlimited plan to every new workspace, not only the administrator's. Signup stays open until later in this guide, so anyone who registers during that window also receives an unlimited workspace.&lt;/p&gt;

&lt;p&gt;Replace the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;builder.example.com&lt;/code&gt; and &lt;code&gt;viewer.example.com&lt;/code&gt; with your actual domains pointing to the server.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;admin@example.com&lt;/code&gt; with your email for Let's Encrypt and admin access.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;YOUR_GENERATED_SECRET&lt;/code&gt; with the output from the openssl command.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;STRONG_DATABASE_PASSWORD&lt;/code&gt; with a secure password for PostgreSQL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;YOUR_SMTP_USERNAME&lt;/code&gt; with your username.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;YOUR_SMTP_PASSWORD&lt;/code&gt; with a strong, secure password for your mail.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;notifications@example.com&lt;/code&gt; in &lt;code&gt;NEXT_PUBLIC_SMTP_FROM&lt;/code&gt; with a sender address on your own domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save and close the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy with Docker Compose
&lt;/h2&gt;

&lt;p&gt;Docker Compose orchestrates the full Typebot stack: Traefik for reverse proxy and HTTPS, PostgreSQL for persistent storage, Redis for sessions and caching, the Builder and Viewer services, and Mailpit for email. This configuration is adapted from the &lt;a href="https://docs.typebot.com/self-hosting/deploy/docker" rel="noopener noreferrer"&gt;official Typebot Docker setup&lt;/a&gt; to use Traefik and persistent volumes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the Docker Compose manifest:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano docker-compose.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Add the following content:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik:v3.7.8&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker.exposedbydefault=false"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.address=:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.websecure.address=:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.to=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.scheme=https"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.letsencrypt.acme.httpchallenge=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./letsencrypt:/letsencrypt"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock:ro"&lt;/span&gt;

  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;typebot-postgres&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./pgdata:/var/lib/postgresql/data"&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-d"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_DB}"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-U"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_USER}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:8-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;typebot-redis&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-server"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--appendonly"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./redisdata:/data"&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

  &lt;span class="na"&gt;typebot-builder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;baptistearno/typebot-builder:3.17.2&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;typebot-builder&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.typebot-builder.rule=Host(`${DOMAIN_BUILDER}`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.typebot-builder.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.typebot-builder.tls.certresolver=letsencrypt"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.typebot-builder.loadbalancer.server.port=3000"&lt;/span&gt;

  &lt;span class="na"&gt;typebot-viewer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;baptistearno/typebot-viewer:3.17.2&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;typebot-viewer&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.typebot-viewer.rule=Host(`${DOMAIN_VIEWER}`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.typebot-viewer.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.typebot-viewer.tls.certresolver=letsencrypt"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.typebot-viewer.loadbalancer.server.port=3000"&lt;/span&gt;

  &lt;span class="na"&gt;mailpit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;axllent/mailpit:v1.30.5&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mailpit&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:8025:8025"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:1025:1025"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MP_MAX_MESSAGES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt;
      &lt;span class="na"&gt;MP_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/data/mailpit.db&lt;/span&gt;
      &lt;span class="na"&gt;MP_SMTP_AUTH_ACCEPT_ANY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;MP_SMTP_AUTH_ALLOW_INSECURE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data:/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;traefik&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Acts as a reverse proxy and HTTPS termination layer for both the Builder and Viewer.&lt;/li&gt;
&lt;li&gt;Listens on ports 80 and 443 for incoming web traffic and automatically redirects HTTP to HTTPS.&lt;/li&gt;
&lt;li&gt;Requests and renews TLS certificates from Let's Encrypt using the email address defined in &lt;code&gt;LETSENCRYPT_EMAIL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Stores certificates persistently in the &lt;code&gt;./letsencrypt&lt;/code&gt; directory.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;postgres&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs PostgreSQL 16 as the primary database to store all bots, user responses, workspaces, and configuration data.&lt;/li&gt;
&lt;li&gt;Uses database credentials defined in the &lt;code&gt;.env&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Persists database files in the &lt;code&gt;./pgdata&lt;/code&gt; directory on the host.&lt;/li&gt;
&lt;li&gt;Includes a health check to ensure the database is fully initialized before the Typebot services start.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;redis&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs Redis 8 (Alpine), a required dependency for the Builder and Viewer services to start.&lt;/li&gt;
&lt;li&gt;Supports sign-in rate limiting by IP and multiple media uploads on WhatsApp.&lt;/li&gt;
&lt;li&gt;Persists data in the &lt;code&gt;./redisdata&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;Includes a health check to verify Redis is responsive.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;typebot-builder&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs the official Typebot Builder application (the visual editor).&lt;/li&gt;
&lt;li&gt;Loads all configuration and secrets from the &lt;code&gt;.env&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Connects to both PostgreSQL and Redis, waiting for their health checks to pass before starting.&lt;/li&gt;
&lt;li&gt;Registers itself with Traefik using Docker labels so it can be securely accessed via your builder domain.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;typebot-viewer&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs the official Typebot Viewer application (the public-facing bot runtime).&lt;/li&gt;
&lt;li&gt;Loads all configuration and secrets from the &lt;code&gt;.env&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Connects to both PostgreSQL and Redis, waiting for their health checks to pass before starting.&lt;/li&gt;
&lt;li&gt;Registers itself with Traefik using Docker labels so published bots can be accessed and embedded via your viewer domain.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mailpit&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs Mailpit as a local email testing tool, bound to &lt;code&gt;127.0.0.1&lt;/code&gt; so it is not exposed publicly.&lt;/li&gt;
&lt;li&gt;Exposes the web UI on port &lt;code&gt;8025&lt;/code&gt; and the SMTP service on port &lt;code&gt;1025&lt;/code&gt; locally.&lt;/li&gt;
&lt;li&gt;Stores email data in the &lt;code&gt;./data&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;Lets Typebot send sign-in verification codes to a local inbox instead of a real mailbox.&lt;/li&gt;
&lt;li&gt;Lets you read messages in your local browser at &lt;code&gt;http://localhost:8025&lt;/code&gt; via an SSH tunnel after deployment.&lt;/li&gt;
&lt;/ul&gt;


&lt;blockquote&gt;

&lt;p&gt;Mailpit only captures mail locally. It never delivers to a real inbox, so it is not a production email path. To send real email, either request that your provider unblock outbound port 25 on this instance and self-host a mail delivery service such as &lt;a href="https://docs.postalserver.io/" rel="noopener noreferrer"&gt;Postal&lt;/a&gt;, pointing &lt;code&gt;SMTP_HOST&lt;/code&gt;, &lt;code&gt;SMTP_PORT&lt;/code&gt;, &lt;code&gt;SMTP_USERNAME&lt;/code&gt;, and &lt;code&gt;SMTP_PASSWORD&lt;/code&gt; at it, or route outbound mail through an authenticated relay on port 587 or 465. Many cloud providers block outbound port 25 by default on new instances, so check with your provider before choosing a self-hosted mail path.&lt;/p&gt;


&lt;/blockquote&gt;
&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Save and close the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Validate the syntax of the file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Start the services in detached mode:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Verify that the containers are running and healthy:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All six containers should show a status of &lt;code&gt;Up&lt;/code&gt;, with &lt;code&gt;mailpit&lt;/code&gt;, &lt;code&gt;typebot-postgres&lt;/code&gt;, and &lt;code&gt;typebot-redis&lt;/code&gt; also showing &lt;code&gt;(healthy)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. View the logs for the Builder service to ensure it connected successfully to the database and Redis:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose logs typebot-builder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. View the logs for the Viewer service to ensure it connected successfully to the database and Redis:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose logs typebot-viewer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Access and Configure Typebot
&lt;/h2&gt;

&lt;p&gt;Typebot requires email-based verification instead of a password for the first sign-in, which this deployment routes through Mailpit. This section confirms the administrator account, verifies that both the Builder and Viewer domains are reachable, and closes public registration.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your web browser and navigate to the Builder domain using HTTPS.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   https://builder.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;builder.example.com&lt;/code&gt; with the actual domain you set in the &lt;code&gt;.env&lt;/code&gt; file for the Builder.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign in with the email address defined in the &lt;code&gt;ADMIN_EMAIL&lt;/code&gt; variable. A six-digit verification code is sent to your Mailpit email server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm6pyl51qq4gj3mlbxtgr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm6pyl51qq4gj3mlbxtgr.png" alt="Typebot email sign-in verification screen" width="794" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Because Mailpit is bound to &lt;code&gt;127.0.0.1&lt;/code&gt; and not exposed publicly, access its web interface by setting up an SSH local port forwarding tunnel from your local terminal.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;   $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ssh &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; 8025:localhost:8025 USERNAME@YOUR_SERVER_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;USERNAME&lt;/code&gt; with your server's username and &lt;code&gt;YOUR_SERVER_IP&lt;/code&gt; with your server's IP. If your server uses key-based SSH authentication, add &lt;code&gt;-i /path/to/your-private-key&lt;/code&gt; before &lt;code&gt;-N&lt;/code&gt;. The &lt;code&gt;-N&lt;/code&gt; flag tells SSH to only forward the port instead of opening a remote shell.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Open your local web browser and navigate to the Mailpit inbox at &lt;code&gt;http://localhost:8025&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the Mailpit inbox and copy the verification code sent by Typebot.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyzi34bpfidm7ov1ggqa4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyzi34bpfidm7ov1ggqa4.jpg" alt="Viewing the sign-in verification code inside the Mailpit inbox" width="800" height="89"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Return to the Builder tab and enter the code to complete sign-in.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Return to the terminal running the SSH tunnel and press Ctrl+C to close it, since Mailpit access is no longer needed until the next sign-in.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the Viewer domain in a new tab.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   https://viewer.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;viewer.example.com&lt;/code&gt; with the domain you configured in the &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click the dashboard link on the Viewer page to verify that it opens the Builder interface.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Futeionlxvcjnhfog05fr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Futeionlxvcjnhfog05fr.png" alt="Typebot viewer landing page displaying the dashboard link" width="799" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open your &lt;code&gt;.env&lt;/code&gt; file to disable public registrations now that your admin account is created.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Update the signup configuration.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;DISABLE_SIGNUP&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Save and close the file, then apply the changes to the Builder container.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--force-recreate&lt;/span&gt; typebot-builder
&lt;/code&gt;&lt;/pre&gt;


&lt;blockquote&gt;
&lt;p&gt;This prevents unauthorized users from registering new accounts on your public Builder instance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Create and Embed a Typebot
&lt;/h2&gt;

&lt;p&gt;Typebot publishes each bot as a hosted page on the Viewer domain and provides a JavaScript snippet that embeds that page into any website. Publishing a bot makes it reachable at that public link before you add it to a page.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;In the Builder tab still open from the previous section, click &lt;strong&gt;Create a typebot&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select &lt;strong&gt;Start from scratch&lt;/strong&gt; to create a new bot manually.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5kansw4z9ypyxl1j0c4q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5kansw4z9ypyxl1j0c4q.png" alt="Selecting start from scratch on the Typebot creation screen" width="799" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;From the top left corner, change the typebot name to your preferred title, for example, &lt;strong&gt;My first bot&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In the visual editor, drag a &lt;strong&gt;Text&lt;/strong&gt; bubble block onto the canvas and enter a welcome message, for example, "Hello! How can I help you today?"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Drag a connection line from the &lt;strong&gt;Start&lt;/strong&gt; block's output dot to the &lt;strong&gt;Text&lt;/strong&gt; bubble block so the flow begins there.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Drag an Input block, for example &lt;strong&gt;Text&lt;/strong&gt;, and connect it to the previous block.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2obvupydfznvqdy5ld63.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2obvupydfznvqdy5ld63.png" alt="Connecting a text block and input block in the Typebot visual editor" width="799" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Click &lt;strong&gt;Publish&lt;/strong&gt; in the top-right corner.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Under &lt;strong&gt;Embed your typebot&lt;/strong&gt;, click &lt;strong&gt;Iframe&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Copy the &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; snippet shown in the dialog.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the main HTML file of your sample website or any page where you want to add the bot.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Paste the snippet just before the closing &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt; tag.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt;
    &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"Typebot"&lt;/span&gt;
    &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://viewer.example.com/your-bot-id"&lt;/span&gt;
    &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"border: none; width: 100%; height: 600px"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Replace &lt;code&gt;viewer.example.com/your-bot-id&lt;/code&gt; with the link shown in the Iframe dialog.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Save the file and open your website in a browser to test the bot.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Return to the Typebot Builder, open the &lt;strong&gt;Results&lt;/strong&gt; tab, and verify that responses are being captured.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Test a Bot
&lt;/h2&gt;

&lt;p&gt;Typebot's built-in templates route respondents through a Choice input block, so each button ends its own path through the flow instead of all leading to the same message. The Results tab records which option a respondent picked as its own column, alongside any text or email fields the flow collects.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Create a typebot&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Start from template&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Choose the &lt;strong&gt;Customer Support&lt;/strong&gt; template from the left pane, then click &lt;strong&gt;Use this template&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Publish&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Open the link shown under &lt;strong&gt;Your typebot links&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click one of the response options, for example &lt;strong&gt;I have a feature request&lt;/strong&gt;. Verify that the bot responds with a follow-up message and a link, then click &lt;strong&gt;Restart&lt;/strong&gt; to return to the beginning.&lt;/li&gt;
&lt;li&gt;Return to the Typebot Builder, open the &lt;strong&gt;Results&lt;/strong&gt; tab, and verify that the option you clicked appears under the &lt;strong&gt;Menu&lt;/strong&gt; column.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Connect a real SMTP provider so sign-in codes and notifications reach real inboxes&lt;/li&gt;
&lt;li&gt;Explore Typebot's integrations, such as Google Sheets, webhooks, and Zapier&lt;/li&gt;
&lt;li&gt;Build a multi-step lead qualification or support triage flow using Choice and Condition blocks&lt;/li&gt;
&lt;li&gt;Set up scheduled backups of the PostgreSQL volume before handling production traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full guide with additional tips, visit the original article on &lt;strong&gt;&lt;a href="https://docs.vultr.com/how-to-deploy-typebot-open-source-conversational-form-builder" rel="noopener noreferrer"&gt;Vultr Docs&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>selfhosted</category>
      <category>chatbot</category>
      <category>traefik</category>
    </item>
    <item>
      <title>Deploying BookStack - Open-Source Documentation Platform</title>
      <dc:creator>Sanskriti Harmukh</dc:creator>
      <pubDate>Wed, 23 Sep 2026 19:26:11 +0000</pubDate>
      <link>https://dev.to/vultr/deploying-bookstack-open-source-documentation-platform-4563</link>
      <guid>https://dev.to/vultr/deploying-bookstack-open-source-documentation-platform-4563</guid>
      <description>&lt;p&gt;&lt;a href="https://www.bookstackapp.com/" rel="noopener noreferrer"&gt;BookStack&lt;/a&gt; is an open-source documentation platform for creating, organizing, and managing knowledge bases. It provides a web interface for structuring documentation into Shelves, Books, Chapters, and Pages, making it easy to organize technical documentation, internal wikis, project documentation, and team knowledge. This guide deploys BookStack on a Linux server using Docker Compose with MariaDB for data storage and Traefik as the reverse proxy for TLS termination, then walks through creating Shelves, Books, Chapters, and Pages through the web interface. By the end, you'll have a working BookStack instance with a sample documentation hierarchy served securely over HTTPS.&lt;/p&gt;




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

&lt;p&gt;Before you begin, you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have access to a Linux-based server (with at least 4 CPU cores and 8 GB of RAM) as a non-root user with sudo privileges.&lt;/li&gt;
&lt;li&gt;Install Docker and Docker Compose.&lt;/li&gt;
&lt;li&gt;Create a DNS A record pointing to your server's IP address (for example, &lt;code&gt;book.example.com&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Set Up the Directory Structure, Configuration, and Environment Variables
&lt;/h2&gt;

&lt;p&gt;BookStack requires a configuration file to define environment variables, database connections, and application settings. The setup includes persistent storage for the MariaDB database, uploaded files, and application configuration to ensure data is retained across server restarts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the project directory:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; ~/bookstack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Navigate to the project directory:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/bookstack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Generate a secret key for the BookStack application:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"base64:&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the output for use in the environment file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Create an environment file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Add the following configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;DOMAIN&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;book.example.com&lt;/span&gt;
&lt;span class="py"&gt;LETSENCRYPT_EMAIL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;admin@example.com&lt;/span&gt;

&lt;span class="c"&gt;# BookStack Settings
&lt;/span&gt;&lt;span class="py"&gt;APP_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://book.example.com&lt;/span&gt;
&lt;span class="py"&gt;APP_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;YOUR_GENERATED_APP_KEY&lt;/span&gt;

&lt;span class="c"&gt;# Database Settings
&lt;/span&gt;&lt;span class="py"&gt;DB_DATABASE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;bookstack&lt;/span&gt;
&lt;span class="py"&gt;DB_USERNAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;bookstack&lt;/span&gt;
&lt;span class="py"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;STRONG_DATABASE_PASSWORD_1&lt;/span&gt;
&lt;span class="py"&gt;DB_ROOT_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;STRONG_DATABASE_PASSWORD_2&lt;/span&gt;

&lt;span class="c"&gt;# Time Zone
&lt;/span&gt;&lt;span class="py"&gt;TZ&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;UTC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;book.example.com&lt;/code&gt; with your domain name.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;admin@example.com&lt;/code&gt; with your email address.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;YOUR_GENERATED_APP_KEY&lt;/code&gt; with the output from the earlier step.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;STRONG_DATABASE_PASSWORD_1&lt;/code&gt; and &lt;code&gt;STRONG_DATABASE_PASSWORD_2&lt;/code&gt; with two different secure passwords.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save and close the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy with Docker Compose
&lt;/h2&gt;

&lt;p&gt;The deployment stack uses Traefik as the reverse proxy for TLS termination and deploys BookStack and MariaDB containers with mounted application and database volumes. This configuration is based on the &lt;a href="https://codeberg.org/bookstack/devops/src/branch/main/config/lsio-docker/docker-compose.yml" rel="noopener noreferrer"&gt;official BookStack Docker Compose configuration&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the Docker Compose manifest file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano docker-compose.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Add the following configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik:v3.7.11&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker.exposedbydefault=false"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.address=:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.websecure.address=:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entryPoint.to=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entryPoint.scheme=https"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.myresolver.acme.tlschallenge=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.myresolver.acme.email=${LETSENCRYPT_EMAIL}"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./letsencrypt:/letsencrypt&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;mariadb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lscr.io/linuxserver/mariadb:11.8.8&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bookstack_mariadb&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PUID=1000&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PGID=1000&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TZ=${TZ}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_DATABASE=${DB_DATABASE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_USER=${DB_USERNAME}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_PASSWORD=${DB_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./bookstack_data/mariadb_data:/config&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;bookstack&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lscr.io/linuxserver/bookstack:version-v26.05.4&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bookstack&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mariadb&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PUID=1000&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PGID=1000&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TZ=${TZ}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;APP_URL=${APP_URL}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;APP_KEY=${APP_KEY}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_HOST=mariadb&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_PORT=3306&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_DATABASE=${DB_DATABASE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_USERNAME=${DB_USERNAME}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_PASSWORD=${DB_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./bookstack_data/app_data:/config&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.bookstack.rule=Host(`${DOMAIN}`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.bookstack.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.bookstack.tls.certresolver=myresolver"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.bookstack.loadbalancer.server.port=80"&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and close the file.&lt;/p&gt;

&lt;p&gt;In the above manifest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;traefik&lt;/code&gt;: Serves as the reverse proxy and TLS termination point, using the official Traefik image. It exposes ports &lt;code&gt;80&lt;/code&gt; and &lt;code&gt;443&lt;/code&gt; for HTTP and HTTPS traffic, stores certificates in the &lt;code&gt;./letsencrypt&lt;/code&gt; directory, and automatically provisions them through Let's Encrypt.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mariadb&lt;/code&gt;: Stores the BookStack database, including users, Shelves, Books, Chapters, Pages, and application data, using the LinuxServer.io MariaDB image. The database name, username, password, and root password come from the &lt;code&gt;.env&lt;/code&gt; file, and the data persists in the &lt;code&gt;./bookstack_data/mariadb_data&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bookstack&lt;/code&gt;: Runs the BookStack web application, using the LinuxServer.io BookStack image, and starts only after the &lt;code&gt;mariadb&lt;/code&gt; service is available. The application URL, application key, database connection settings, and time zone come from the &lt;code&gt;.env&lt;/code&gt; file, the application configuration and uploaded files persist in the &lt;code&gt;./bookstack_data/app_data&lt;/code&gt; directory, and the Traefik labels route HTTPS requests for your domain to this container.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three services use &lt;code&gt;restart: unless-stopped&lt;/code&gt;, so they restart automatically if they fail or the server reboots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Start all services in detached mode:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Verify that the services are running:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output displays all the containers in the &lt;code&gt;Up&lt;/code&gt; state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. View the service logs to confirm all components started successfully:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Access and Configure BookStack
&lt;/h2&gt;

&lt;p&gt;BookStack provides a web interface for organizing and managing documentation using shelves, books, and pages. The dashboard provides a central place to manage your documentation and workspace.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your web browser and navigate to BookStack at &lt;code&gt;https://book.example.com&lt;/code&gt;, replacing &lt;code&gt;book.example.com&lt;/code&gt; with your configured domain.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;On the screen, enter &lt;strong&gt;Email&lt;/strong&gt; as &lt;code&gt;admin@admin.com&lt;/code&gt; and &lt;strong&gt;Password&lt;/strong&gt; as &lt;code&gt;password&lt;/code&gt; to create the admin account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click &lt;strong&gt;Log In&lt;/strong&gt; to access the BookStack dashboard.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;To change the login password, click the &lt;strong&gt;Admin&lt;/strong&gt; menu in the top-right navigation and select &lt;strong&gt;My Account&lt;/strong&gt;. Then, under the &lt;strong&gt;My Account&lt;/strong&gt; menu on the left, select &lt;strong&gt;Access &amp;amp; Security&lt;/strong&gt;. From here, you can change your password and configure Multi-Factor Authentication (MFA).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Build and Organize Your Documentation in BookStack
&lt;/h2&gt;

&lt;p&gt;BookStack organizes documentation into a hierarchy that makes related content easy to manage and navigate. This workflow creates a Shelf, builds a Book and Chapter within it, and adds a Page containing sample documentation to validate the setup and demonstrate the platform's core features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a Shelf
&lt;/h3&gt;

&lt;p&gt;A Shelf serves as the top-level container for organizing related Books within a documentation collection.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Shelves&lt;/strong&gt; in the top navigation menu.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create one now&lt;/strong&gt; or &lt;strong&gt;New Shelf&lt;/strong&gt; under the &lt;strong&gt;Actions&lt;/strong&gt; panel.&lt;/li&gt;
&lt;li&gt;Enter a shelf name, such as &lt;code&gt;Team Documentation&lt;/code&gt;, and an optional description.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save Shelf&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h3&gt;
  
  
  Create a Book within the Shelf
&lt;/h3&gt;

&lt;p&gt;A Book groups related Chapters and Pages together within a Shelf.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;code&gt;Team Documentation&lt;/code&gt; shelf you created in the previous step.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create New Book&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter a book name, such as &lt;code&gt;Employee Onboarding&lt;/code&gt;, and an optional description.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save Book&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h3&gt;
  
  
  Create a Chapter within the Book
&lt;/h3&gt;

&lt;p&gt;Chapters organize related Pages into logical sections within a Book.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;code&gt;Employee Onboarding&lt;/code&gt; book you created in the previous step.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add a chapter&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter a chapter name, such as &lt;code&gt;Getting Started&lt;/code&gt;, and an optional description.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save Chapter&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h3&gt;
  
  
  Create and Edit a Page within the Chapter
&lt;/h3&gt;

&lt;p&gt;Pages are where you create, edit, and organize documentation using the built-in rich text editor.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;code&gt;Getting Started&lt;/code&gt; chapter you created in the previous step.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create a new page&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter a page title such as &lt;code&gt;Development Environment Setup&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add a brief introduction, followed by a heading titled &lt;code&gt;Prerequisites&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add a bulleted list describing the required software and tools.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save Page&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;Reopen the &lt;code&gt;Development Environment Setup&lt;/code&gt; page, click &lt;strong&gt;Edit&lt;/strong&gt; under the &lt;strong&gt;Actions&lt;/strong&gt; panel, and add another heading named &lt;code&gt;Next Steps&lt;/code&gt;, then click &lt;strong&gt;Save Page&lt;/strong&gt; again.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Invite additional users and configure role-based permissions for each Shelf or Book&lt;/li&gt;
&lt;li&gt;Enable a search index and explore BookStack's built-in page revision history&lt;/li&gt;
&lt;li&gt;Configure an external authentication provider such as LDAP or SAML for team sign-in&lt;/li&gt;
&lt;li&gt;Set up scheduled backups of the MariaDB database and the uploaded file storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full guide with additional tips, visit the original article on &lt;strong&gt;&lt;a href="https://docs.vultr.com/how-to-deploy-bookstack-open-source-documentation-platform" rel="noopener noreferrer"&gt;Vultr Docs&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>selfhosted</category>
      <category>documentation</category>
      <category>traefik</category>
    </item>
    <item>
      <title>Deploying Uptime Kuma - Self-Hosted Status Page and Monitoring Tool</title>
      <dc:creator>Sanskriti Harmukh</dc:creator>
      <pubDate>Wed, 23 Sep 2026 19:24:27 +0000</pubDate>
      <link>https://dev.to/vultr/deploying-uptime-kuma-self-hosted-status-page-and-monitoring-tool-4cj1</link>
      <guid>https://dev.to/vultr/deploying-uptime-kuma-self-hosted-status-page-and-monitoring-tool-4cj1</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/louislam/uptime-kuma" rel="noopener noreferrer"&gt;Uptime Kuma&lt;/a&gt; is a self-hosted monitoring tool that tracks the availability of websites, APIs, TCP ports, DNS records, and other services. It provides a clean dashboard to display uptime metrics and trigger alerts through 90+ notification services, including email, Slack, and Telegram, when a service goes down or recovers. This guide deploys Uptime Kuma on a Linux server using Docker Compose behind a Traefik reverse proxy. By the end, you'll have a working Uptime Kuma instance with an active monitor, a notification channel, and a public status page.&lt;/p&gt;




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

&lt;p&gt;Before you begin, you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have access to a Linux-based server as a non-root user with sudo privileges.&lt;/li&gt;
&lt;li&gt;Install Docker and Docker Compose.&lt;/li&gt;
&lt;li&gt;Configure a domain A record, such as &lt;code&gt;kuma.example.com&lt;/code&gt;, pointing to your server's public IP address.&lt;/li&gt;
&lt;li&gt;Add your user to the &lt;code&gt;docker&lt;/code&gt; group to run Docker commands without &lt;code&gt;sudo&lt;/code&gt;, then start a new shell session to apply the change.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Set Up the Directory Structure and Environment Variables
&lt;/h2&gt;

&lt;p&gt;The project directory holds the Docker Compose file, the persistent data directory, and the environment variables shared between the Traefik and Uptime Kuma containers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the project directory:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; ~/uptime-kuma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Enter the project directory:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/uptime-kuma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Create the persistent data directory for Uptime Kuma:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Create the environment variable file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Add the following variables to the file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;DOMAIN&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;kuma.example.com&lt;/span&gt;
&lt;span class="py"&gt;LETSENCRYPT_EMAIL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;admin@example.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the following placeholders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kuma.example.com&lt;/code&gt;: Your registered domain name pointing to the server's IP address.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;admin@example.com&lt;/code&gt;: Your email address for Let's Encrypt certificate notifications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save and close the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy with Docker Compose
&lt;/h2&gt;

&lt;p&gt;Traefik and Uptime Kuma run as separate containers on a shared Docker network. Traefik terminates TLS on the configured domain and forwards requests to Uptime Kuma's internal port, keeping Uptime Kuma off the public internet entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the Docker Compose file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano docker-compose.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Add the following configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik:v3.7.11&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker.exposedbydefault=false"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.address=:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.websecure.address=:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.to=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.scheme=https"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.le.acme.httpchallenge=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.le.acme.email=${LETSENCRYPT_EMAIL}"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./letsencrypt:/letsencrypt&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;kuma-net&lt;/span&gt;

  &lt;span class="na"&gt;uptime-kuma&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;louislam/uptime-kuma:2.5.3&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uptime-kuma&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data:/app/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;kuma-net&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.kuma.rule=Host(`${DOMAIN}`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.kuma.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.kuma.tls=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.kuma.tls.certresolver=le"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.kuma.loadbalancer.server.port=3001"&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Save and close the file.&lt;/p&gt;

&lt;p&gt;In the above configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;traefik&lt;/code&gt;: The reverse proxy that listens on ports &lt;code&gt;80&lt;/code&gt; and &lt;code&gt;443&lt;/code&gt;, redirects all HTTP traffic to HTTPS, and automatically provisions TLS certificates from Let's Encrypt using the HTTP challenge method.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uptime-kuma&lt;/code&gt;: The monitoring service that stores data in the &lt;code&gt;./data&lt;/code&gt; directory. The Traefik labels configure domain routing, enable HTTPS, and forward traffic to the Uptime Kuma application on port &lt;code&gt;3001&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kuma-net&lt;/code&gt;: A shared bridge network that allows Traefik and Uptime Kuma to communicate internally without exposing internal ports to the host.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Start all services in detached mode:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Verify that all containers are running:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output displays two running containers: Traefik and Uptime Kuma.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure Uptime Kuma
&lt;/h2&gt;

&lt;p&gt;Uptime Kuma does not report any status information until you create an administrator account and configure at least one monitor.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a web browser and navigate to the domain configured in the &lt;code&gt;.env&lt;/code&gt; file.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   https://kuma.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;On the database setup page, select &lt;strong&gt;SQLite&lt;/strong&gt;, then click &lt;strong&gt;Next&lt;/strong&gt;. SQLite requires no extra configuration and works with the single-container deployment in this guide. The &lt;strong&gt;MariaDB/MySQL&lt;/strong&gt; option connects to a separate database server that this deployment does not provision.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The account creation page appears. Enter a username, enter a strong password in both the &lt;strong&gt;Password&lt;/strong&gt; and &lt;strong&gt;Repeat Password&lt;/strong&gt; fields, then click &lt;strong&gt;Create&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After you create the account, the Uptime Kuma dashboard opens. Click &lt;strong&gt;+ Add New Monitor&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flhr1.vultrobjects.com%2Fdocs-main-doc-assets-1%2F2659%2F65edbe5a-8917-4ac3-9b40-ef4525cb1f8b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flhr1.vultrobjects.com%2Fdocs-main-doc-assets-1%2F2659%2F65edbe5a-8917-4ac3-9b40-ef4525cb1f8b.png" alt="Uptime Kuma Dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure the monitor settings:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monitor Type&lt;/strong&gt;: Select the type of service to monitor. Choose &lt;strong&gt;HTTP(s)&lt;/strong&gt; for websites and APIs, &lt;strong&gt;TCP Port&lt;/strong&gt; for port-based services, or &lt;strong&gt;DNS&lt;/strong&gt; for domain records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Friendly Name&lt;/strong&gt;: Enter a descriptive name for the monitor (for example, &lt;code&gt;My Website&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL&lt;/strong&gt;: Enter the full URL of the service to monitor (for example, &lt;code&gt;https://example.com&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heartbeat Interval&lt;/strong&gt;: Set the polling frequency in seconds. The default is &lt;code&gt;60&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt; to activate the monitor. The dashboard adds the monitor with an &lt;strong&gt;Unknown&lt;/strong&gt; status. The status updates to &lt;strong&gt;Up&lt;/strong&gt; or &lt;strong&gt;Down&lt;/strong&gt; after the first heartbeat check runs within 60 seconds.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Configure Alert Notifications
&lt;/h3&gt;

&lt;p&gt;Uptime Kuma does not send alerts by default. You must configure a notification channel and assign it to individual monitors before it triggers on a status change.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click the profile avatar in the top-right corner and select &lt;strong&gt;Settings&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Navigate to the &lt;strong&gt;Notifications&lt;/strong&gt; tab and click &lt;strong&gt;Set Up Notification&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select a notification type from the dropdown. Supported channels include email (SMTP), Slack, Telegram, Discord, and PagerDuty, among others.&lt;/li&gt;
&lt;li&gt;Enter the required credentials or webhook URL of the selected notification channel.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Test&lt;/strong&gt; to send a test notification and verify the configuration is working.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt; to confirm the notification setup.&lt;/li&gt;
&lt;li&gt;Open the settings for each monitor that should use this notification channel, select the notification from the &lt;strong&gt;Notifications&lt;/strong&gt; list, and click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Create a Status Page
&lt;/h2&gt;

&lt;p&gt;A status page groups one or more monitors into a public view, separate from the administrator dashboard, so you can share it with users or team members without granting them access to the full Uptime Kuma interface.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Status Pages&lt;/strong&gt; in the top navigation bar.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;New Status Page&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter a &lt;strong&gt;Name&lt;/strong&gt; for the status page (for example, &lt;code&gt;Service Status&lt;/code&gt;), then enter a &lt;strong&gt;Slug&lt;/strong&gt; using only lowercase letters, numbers, and hyphens (for example, &lt;code&gt;status&lt;/code&gt; creates the URL &lt;code&gt;https://kuma.example.com/status/status&lt;/code&gt;). Click &lt;strong&gt;Next&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The status page editor opens. The &lt;strong&gt;Title&lt;/strong&gt; field is pre-filled with the name entered in the previous step. Add a &lt;strong&gt;Description&lt;/strong&gt; if needed, then configure the remaining display settings as needed.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add Group&lt;/strong&gt; to create a section, then use the &lt;strong&gt;Add a monitor&lt;/strong&gt; dropdown to select a monitor and add it to the group.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt; to publish the status page.&lt;/li&gt;
&lt;li&gt;Open the status page URL in a browser to verify it loads and displays the monitor statuses.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   https://kuma.example.com/status/status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the path with the slug configured in step 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Add more monitors for your other websites, APIs, and internal services&lt;/li&gt;
&lt;li&gt;Configure additional notification channels so different teams get alerted through their preferred tool&lt;/li&gt;
&lt;li&gt;Build separate status pages for internal and public audiences&lt;/li&gt;
&lt;li&gt;Explore Uptime Kuma's tag and group features to organize monitors at scale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full guide with additional tips, visit the original article on &lt;strong&gt;&lt;a href="https://docs.vultr.com/how-to-deploy-uptime-kuma-self-hosted-status-page-and-monitoring-tool" rel="noopener noreferrer"&gt;Vultr Docs&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>monitoring</category>
      <category>selfhosted</category>
      <category>traefik</category>
    </item>
    <item>
      <title>Deploying Kubeflow as an Azure ML Alternative</title>
      <dc:creator>Sanskriti Harmukh</dc:creator>
      <pubDate>Wed, 23 Sep 2026 19:23:38 +0000</pubDate>
      <link>https://dev.to/vultr/deploying-kubeflow-as-an-azure-ml-alternative-188i</link>
      <guid>https://dev.to/vultr/deploying-kubeflow-as-an-azure-ml-alternative-188i</guid>
      <description>&lt;p&gt;Azure Machine Learning is Microsoft's cloud-native machine learning platform that provides experiment tracking, managed compute, pipelines, a model registry, and model serving as hosted services within the Azure ecosystem, but it ties teams to Azure-specific APIs, managed compute pricing, and Microsoft's tooling. &lt;a href="https://www.kubeflow.org/" rel="noopener noreferrer"&gt;Kubeflow&lt;/a&gt; is an open-source machine learning platform built on Kubernetes that provides self-hosted alternatives to Azure ML capabilities through modular, portable components, letting organizations retain complete control over infrastructure, data residency, scalability, and operational costs. This guide deploys Kubeflow on a Kubernetes cluster as a self-managed replacement for Azure Machine Learning, covering installation with Kustomize manifests, notebook setup, ML pipeline orchestration, distributed training with the Trainer v2 API, model serving through KServe, hyperparameter optimization using Katib, RBAC and access management, object storage integration, and migration considerations for existing Azure ML workflows. By the end, you'll have a self-hosted ML platform running notebooks, pipelines, distributed training, model serving, and automated hyperparameter tuning.&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding Azure Machine Learning vs Kubeflow
&lt;/h2&gt;

&lt;p&gt;Azure ML and Kubeflow provide comparable ML platform capabilities, but they differ in deployment and operational models. Azure ML delivers these as fully managed services within the Azure ecosystem, while Kubeflow provides equivalent open-source components that run on any Kubernetes cluster. The following table maps each Azure ML feature to its Kubeflow counterpart.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Azure Machine Learning&lt;/th&gt;
&lt;th&gt;Kubeflow Equivalent&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Azure ML Notebooks&lt;/td&gt;
&lt;td&gt;Kubeflow Notebooks&lt;/td&gt;
&lt;td&gt;Interactive development environments with JupyterLab, VS Code, and RStudio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure ML Jobs&lt;/td&gt;
&lt;td&gt;Kubeflow Trainer&lt;/td&gt;
&lt;td&gt;Distributed training for PyTorch, DeepSpeed, MLX, JAX, and XGBoost workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure ML Pipelines&lt;/td&gt;
&lt;td&gt;Kubeflow Pipelines (KFP)&lt;/td&gt;
&lt;td&gt;Directed Acyclic Graph (DAG) based ML workflow orchestration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure ML Model Registry&lt;/td&gt;
&lt;td&gt;Kubeflow Model Registry&lt;/td&gt;
&lt;td&gt;Versioned model artifact management with metadata tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure ML Endpoints&lt;/td&gt;
&lt;td&gt;KServe&lt;/td&gt;
&lt;td&gt;Serverless model serving with autoscaling and canary deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure ML Experiments&lt;/td&gt;
&lt;td&gt;Katib&lt;/td&gt;
&lt;td&gt;Automated hyperparameter tuning with multiple search algorithms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Self-hosting with Kubeflow eliminates per-minute compute charges, keeps all data within your own cluster, runs on any cloud provider or on-premises hardware, and allows complete customization of every component.&lt;/p&gt;

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

&lt;p&gt;Before you begin, you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have access to a multi-node Kubernetes cluster that runs Kubernetes 1.31 or later with at least 4 CPU cores and 16 GB of RAM per node (minimum 3 nodes recommended).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kubernetes.io/docs/tasks/tools/" rel="noopener noreferrer"&gt;Install kubectl&lt;/a&gt; and configure it to connect to your cluster.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kubectl.docs.kubernetes.io/installation/kustomize/" rel="noopener noreferrer"&gt;Install Kustomize&lt;/a&gt; version 5.4.3 or later.&lt;/li&gt;
&lt;li&gt;Have a default &lt;code&gt;StorageClass&lt;/code&gt; that is configured in your cluster for provisioning persistent volumes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Install Kubeflow
&lt;/h2&gt;

&lt;p&gt;Kubeflow uses Kustomize to deploy its components as Kubernetes resources. The official &lt;code&gt;kubeflow/manifests&lt;/code&gt; repository contains all component manifests that are organized under &lt;code&gt;common/&lt;/code&gt; for shared infrastructure services such as Istio, cert-manager, and Dex, and under &lt;code&gt;applications/&lt;/code&gt; for Kubeflow-specific applications such as Pipelines, Notebooks, and KServe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy Kubeflow via Manifests
&lt;/h3&gt;

&lt;p&gt;The following steps clone the Kubeflow manifests repository and deploy all components to the cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Verify the Kubernetes cluster connection:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl cluster-info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Check the Kubernetes server version:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that the &lt;code&gt;Server Version&lt;/code&gt; field shows version 1.31 or later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Clone the official Kubeflow manifests repository:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git clone https://github.com/kubeflow/manifests.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Switch to the manifests directory:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;manifests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Check out the latest stable release tag:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git checkout 26.03
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Deploy all Kubeflow components:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The command uses a bounded retry loop that attempts the installation up to 5 times, which accommodates the time that Kubernetes CRDs and webhooks need to register before dependent resources apply. The loop exits automatically after a successful apply or after reaching the retry limit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in &lt;/span&gt;1 2 3 4 5&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;kustomize build example | kubectl apply &lt;span class="nt"&gt;--server-side&lt;/span&gt; &lt;span class="nt"&gt;--force-conflicts&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; - &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Attempt &lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt; failed, retrying in 30s..."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;30&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first one or two attempts may output errors about CRDs or webhooks not being established. These errors are expected and resolve on subsequent attempts after the CRDs register. The loop exits automatically when the apply succeeds, which typically happens on the second or third attempt. The full installation takes approximately 10 to 15 minutes after the final successful apply for all pods to reach a &lt;code&gt;Running&lt;/code&gt; state. The &lt;code&gt;--server-side --force-conflicts&lt;/code&gt; flags are required because some Kubeflow CRDs exceed the annotation size limit that standard &lt;code&gt;kubectl apply&lt;/code&gt; supports.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The default installation uses the email &lt;code&gt;user@example.com&lt;/code&gt; and password &lt;code&gt;12341234&lt;/code&gt;. Change these credentials before exposing Kubeflow to any network. See the &lt;strong&gt;Set Up Access Control&lt;/strong&gt; section later in this article for instructions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Verify Installation
&lt;/h3&gt;

&lt;p&gt;After the deployment completes, verify that all Kubeflow components are running and the CRDs are registered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Check that all pods in the &lt;code&gt;kubeflow&lt;/code&gt; namespace reach a &lt;code&gt;Running&lt;/code&gt; state:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; kubeflow &lt;span class="nt"&gt;--field-selector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;status.phase!&lt;span class="o"&gt;=&lt;/span&gt;Succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that all listed pods display a &lt;code&gt;Running&lt;/code&gt; status with all containers ready. If any pods show &lt;code&gt;CrashLoopBackOff&lt;/code&gt; or &lt;code&gt;Pending&lt;/code&gt;, check their logs with &lt;code&gt;kubectl logs -n kubeflow POD-NAME&lt;/code&gt; and verify that the cluster meets the minimum resource requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Check that the Istio ingress gateway service is running:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get svc istio-ingressgateway &lt;span class="nt"&gt;-n&lt;/span&gt; istio-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that the service appears in the output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Check that Kubeflow and its component CRDs are registered:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get crd | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"kubeflow|kserve|katib|istio|knative|trainer"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output displays the count of registered CRDs across Kubeflow and its components. A count of 40 or more indicates a complete installation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create Default User Profile
&lt;/h3&gt;

&lt;p&gt;Kubeflow uses profiles to provide namespace-level isolation for each user. The default installation does not automatically provision a user namespace, so you need to create one manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create a new file called &lt;code&gt;user-profile.yaml&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano user-profile.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Add the following configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubeflow.org/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Profile&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubeflow-user-example-com&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;User&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user@example.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and close the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Apply the profile manifest:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; user-profile.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command creates an isolated namespace called &lt;code&gt;kubeflow-user-example-com&lt;/code&gt; with default Role-Based Access Control (RBAC) policies and a service account for the default user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Verify that the namespace exists:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get namespace kubeflow-user-example-com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Verify that the default service account exists:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The service account takes a few seconds to provision after the profile is created. Wait 10 seconds before running this command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get serviceaccount default-editor &lt;span class="nt"&gt;-n&lt;/span&gt; kubeflow-user-example-com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure Storage
&lt;/h3&gt;

&lt;p&gt;Kubeflow components such as Notebooks, Pipelines, and the Model Registry require persistent storage. The cluster needs a default &lt;code&gt;StorageClass&lt;/code&gt; to dynamically provision Persistent Volume Claims (PVCs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Verify that a default &lt;code&gt;StorageClass&lt;/code&gt; exists:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get storageclass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default &lt;code&gt;StorageClass&lt;/code&gt; shows &lt;code&gt;(default)&lt;/code&gt; next to its name. If no default exists, set one by annotating an existing &lt;code&gt;StorageClass&lt;/code&gt;. Replace &lt;code&gt;STORAGE-CLASS-NAME&lt;/code&gt; with the name of an existing &lt;code&gt;StorageClass&lt;/code&gt; from the output above.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl patch storageclass STORAGE-CLASS-NAME &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"metadata": {"annotations": {"storageclass.kubernetes.io/is-default-class": "true"}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configure Kubeflow Notebooks
&lt;/h2&gt;

&lt;p&gt;Kubeflow Notebooks provides managed JupyterLab, VS Code, and RStudio environments that run as Kubernetes pods with direct access to cluster resources, GPUs, and persistent storage. This component serves as a self-hosted alternative to Azure Machine Learning notebooks and compute instances.&lt;/p&gt;

&lt;h3&gt;
  
  
  Access Dashboard
&lt;/h3&gt;

&lt;p&gt;Set up port forwarding and log in to the Kubeflow Central Dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Set up port forwarding to access the Kubeflow Central Dashboard:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl port-forward svc/istio-ingressgateway &lt;span class="nt"&gt;-n&lt;/span&gt; istio-system 8080:80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;http://localhost:8080&lt;/code&gt; in a web browser. The Kubeflow login screen appears. Click &lt;strong&gt;Sign in with Dex&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1xh80y74wed6iu0vqi4k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1xh80y74wed6iu0vqi4k.png" alt="Kubeflow Sign in with Dex" width="799" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enter the default credentials on the Dex login form and click &lt;strong&gt;Login&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Email:&lt;/strong&gt; &lt;code&gt;user@example.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password:&lt;/strong&gt; &lt;code&gt;12341234&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;The Kubeflow Central Dashboard loads with links to &lt;strong&gt;Notebooks&lt;/strong&gt;, &lt;strong&gt;Pipelines&lt;/strong&gt;, &lt;strong&gt;Katib Experiments&lt;/strong&gt;, &lt;strong&gt;KServe Endpoints&lt;/strong&gt;, and other components. Select &lt;code&gt;kubeflow-user-example-com&lt;/code&gt; from the namespace dropdown at the top.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h3&gt;
  
  
  Create Notebook Server
&lt;/h3&gt;

&lt;p&gt;Launch a new notebook server from the Kubeflow dashboard.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Notebooks&lt;/strong&gt; in the left sidebar and click &lt;strong&gt;New Notebook&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;ml-workspace&lt;/code&gt; in the &lt;strong&gt;Name&lt;/strong&gt; field.&lt;/li&gt;
&lt;li&gt;Select the notebook environment from the image cards. Choose &lt;strong&gt;JupyterLab&lt;/strong&gt; for a general-purpose data science environment. Select &lt;strong&gt;VisualStudio Code&lt;/strong&gt; for a code editor interface, or &lt;strong&gt;RStudio&lt;/strong&gt; for R-based statistical computing. To use a specific image version, select &lt;strong&gt;Custom Notebook&lt;/strong&gt; from the dropdown below the cards.&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Minimum CPU&lt;/strong&gt; to &lt;code&gt;0.5&lt;/code&gt; and &lt;strong&gt;Minimum Memory Gi&lt;/strong&gt; to &lt;code&gt;1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Leave the &lt;strong&gt;Workspace Volume&lt;/strong&gt; at the default &lt;code&gt;5Gi&lt;/code&gt;. This volume persists data across notebook restarts.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Launch&lt;/strong&gt; and wait for the notebook pod to reach a &lt;code&gt;Running&lt;/code&gt; state. The status indicator turns green when the notebook is ready.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Connect to Notebook
&lt;/h3&gt;

&lt;p&gt;Open the JupyterLab interface and verify that ML libraries are accessible.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Click &lt;strong&gt;Connect&lt;/strong&gt; next to the notebook server name. A new tab opens with the JupyterLab interface.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click &lt;strong&gt;Python 3 (ipykernel)&lt;/strong&gt; under the &lt;strong&gt;Notebook&lt;/strong&gt; section in the launcher to create a new notebook. Paste the following code into a cell and press Shift+Enter to run it.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
   &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.ensemble&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RandomForestClassifier&lt;/span&gt;
   &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;train_test_split&lt;/span&gt;

   &lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;train_test_split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RandomForestClassifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_estimators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accuracy: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cell outputs an accuracy score such as &lt;code&gt;Accuracy: 0.9750&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set Up Kubeflow Pipelines
&lt;/h2&gt;

&lt;p&gt;Kubeflow Pipelines is a workflow orchestration platform for building, automating, and managing machine learning pipelines as Directed Acyclic Graphs (DAGs). Each step in a pipeline executes within its own containerized environment, improving reproducibility, portability, and experiment versioning across ML workflows. It acts as an open-source, self-hosted alternative to Azure Machine Learning pipelines and workflow orchestration features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy Sample Pipeline
&lt;/h3&gt;

&lt;p&gt;Install the KFP SDK, define a three-step pipeline, compile it, and submit a run through the internal API. The following steps use both the notebook terminal and the cluster management terminal where specified.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inside JupyterLab, click &lt;strong&gt;File &amp;gt; New &amp;gt; Terminal&lt;/strong&gt; to open a terminal tab.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;2. Create the pipeline definition file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano sample_pipeline.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Add the following configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kfp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dsl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compiler&lt;/span&gt;

&lt;span class="nd"&gt;@dsl.component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python:3.11-slim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;preprocess&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;samples&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;features&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preprocessed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@dsl.component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python:3.11-slim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;random_forest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accuracy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@dsl.component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python:3.11-slim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Model: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, Accuracy: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accuracy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@dsl.pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample-ml-pipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ml_pipeline&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;preprocess_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;preprocess&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;train_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;preprocess_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;train_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;compiler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Compiler&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ml_pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pipeline.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pipeline compiled successfully&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and close the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Compile the pipeline to generate the YAML definition:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 sample_pipeline.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Switch to the cluster management terminal and create the authorization policy manifest:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This policy allows the notebook namespace to call the Kubeflow Pipelines API through the Istio service mesh.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano allow-pipeline-access.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Add the following configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security.istio.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AuthorizationPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow-notebook-to-pipeline&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubeflow&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ml-pipeline&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;namespaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kubeflow-user-example-com"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and close the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Apply the authorization policy:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; allow-pipeline-access.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;8. Switch to the notebook terminal and upload the compiled pipeline to Kubeflow Pipelines through the internal API:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"uploadfile=@pipeline.yaml"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"kubeflow-userid: user@example.com"&lt;/span&gt; http://ml-pipeline.kubeflow.svc.cluster.local:8888/apis/v2beta1/pipelines/upload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command returns a JSON response that contains the &lt;code&gt;pipeline_id&lt;/code&gt;. Note this value for the next step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Create an experiment to organize pipeline runs:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"kubeflow-userid: user@example.com"&lt;/span&gt; http://ml-pipeline.kubeflow.svc.cluster.local:8888/apis/v2beta1/experiments &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"display_name":"default","namespace":"kubeflow-user-example-com"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command returns a JSON response that contains the &lt;code&gt;experiment_id&lt;/code&gt;. Note this value for the next step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. Start a pipeline run:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Replace &lt;code&gt;PIPELINE-ID&lt;/code&gt; and &lt;code&gt;EXPERIMENT-ID&lt;/code&gt; with the values from the previous steps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"kubeflow-userid: user@example.com"&lt;/span&gt; http://ml-pipeline.kubeflow.svc.cluster.local:8888/apis/v2beta1/runs &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"display_name":"test-run","experiment_id":"EXPERIMENT-ID","pipeline_version_reference":{"pipeline_id":"PIPELINE-ID"},"runtime_config":{}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Monitor Execution
&lt;/h3&gt;

&lt;p&gt;The pipeline run status is visible from the command line and from the Kubeflow dashboard. The dashboard provides a graph view that shows each step's completion state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Verify that the workflow completed:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get workflows &lt;span class="nt"&gt;-n&lt;/span&gt; kubeflow-user-example-com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that the &lt;code&gt;STATUS&lt;/code&gt; column shows &lt;code&gt;Succeeded&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Pipelines&lt;/strong&gt; in the left sidebar of the Kubeflow dashboard, then click &lt;strong&gt;Experiments&lt;/strong&gt;. Click &lt;strong&gt;default&lt;/strong&gt;, then click the &lt;strong&gt;test-run&lt;/strong&gt; entry. The graph view shows the &lt;code&gt;preprocess&lt;/code&gt;, &lt;code&gt;train&lt;/code&gt;, and &lt;code&gt;evaluate&lt;/code&gt; steps each marked with a green checkmark when the run completes successfully.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h2&gt;
  
  
  Configure Distributed Training
&lt;/h2&gt;

&lt;p&gt;Kubeflow Trainer v2 provides a unified &lt;code&gt;TrainJob&lt;/code&gt; API for running distributed training jobs across frameworks including PyTorch, DeepSpeed, MLX, JAX, and XGBoost. The Trainer uses &lt;code&gt;ClusterTrainingRuntime&lt;/code&gt; resources that define pre-configured runtime environments, which separates infrastructure configuration from training logic. Kubeflow Trainer replaces Azure ML Jobs with native Kubernetes-based distributed training.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create Training Job
&lt;/h3&gt;

&lt;p&gt;Create and deploy a distributed PyTorch training job that uses the &lt;code&gt;torch-distributed&lt;/code&gt; runtime. Run the following commands from the cluster management terminal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the TrainJob manifest:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano trainjob.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Add the following configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trainer.kubeflow.org/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TrainJob&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytorch-training&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubeflow-user-example-com&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runtimeRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;torch-distributed&lt;/span&gt;
  &lt;span class="na"&gt;trainer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/kubeflow/katib/pytorch-mnist-cpu:v0.19.0&lt;/span&gt;
    &lt;span class="na"&gt;numNodes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="na"&gt;resourcesPerNode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;500m"&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1Gi"&lt;/span&gt;
      &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2Gi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and close the file. The &lt;code&gt;runtimeRef&lt;/code&gt; field references the &lt;code&gt;torch-distributed&lt;/code&gt; ClusterTrainingRuntime, which configures the PyTorch distributed training environment. The &lt;code&gt;numNodes&lt;/code&gt; field specifies the number of training nodes that Kubeflow provisions for the job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Apply the training manifest:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; trainjob.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Monitor Training
&lt;/h3&gt;

&lt;p&gt;Check the training job status from the cluster management terminal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Verify the TrainJob status:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get trainjob &lt;span class="nt"&gt;-n&lt;/span&gt; kubeflow-user-example-com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;STATE&lt;/code&gt; column shows &lt;code&gt;Complete&lt;/code&gt; when training finishes successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy Model Serving with KServe
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://kserve.github.io/website/" rel="noopener noreferrer"&gt;KServe&lt;/a&gt; provides a Kubernetes CRD called &lt;code&gt;InferenceService&lt;/code&gt; for deploying, scaling, and managing ML model endpoints. It supports serverless inference with autoscaling from zero, canary rollouts, and multi-model serving across frameworks including TensorFlow, PyTorch, scikit-learn, XGBoost, and ONNX. KServe also supports deploying models directly from Hugging Face Hub using the &lt;code&gt;hf://&lt;/code&gt; URI schema and from the Kubeflow Model Registry using the &lt;code&gt;model-registry://&lt;/code&gt; protocol. KServe is included in the Kubeflow installation and replaces Azure ML Endpoints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create InferenceService
&lt;/h3&gt;

&lt;p&gt;Deploy a pre-trained scikit-learn model and expose it as a serving endpoint. Run the following commands from the cluster management terminal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the model serving manifest:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano sklearn-iris.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Add the following configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;serving.kserve.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;InferenceService&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sklearn-iris&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubeflow-user-example-com&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sidecar.istio.io/inject&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;predictor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;modelFormat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sklearn&lt;/span&gt;
      &lt;span class="na"&gt;storageUri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gs://kfserving-examples/models/sklearn/1.0/model"&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;100m&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256Mi&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and close the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Apply the InferenceService manifest:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; sklearn-iris.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Wait for the InferenceService to become ready:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get inferenceservice sklearn-iris &lt;span class="nt"&gt;-n&lt;/span&gt; kubeflow-user-example-com &lt;span class="nt"&gt;-w&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;READY&lt;/code&gt; column changes to &lt;code&gt;True&lt;/code&gt; when the model is loaded and serving. Press Ctrl+C to stop watching. Navigate to &lt;strong&gt;KServe Endpoints&lt;/strong&gt; in the Kubeflow dashboard sidebar to view the deployed model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Model Endpoint
&lt;/h3&gt;

&lt;p&gt;Send a test inference request to verify that the model is serving predictions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Run the following command from the Kubeflow notebook terminal, which has direct access to the cluster-internal service endpoint:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 30 &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; http://sklearn-iris-predictor-00001-private.kubeflow-user-example-com.svc.cluster.local/v1/models/sklearn-iris:predict &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"instances": [[6.8, 2.8, 4.8, 1.4], [6.0, 3.4, 4.5, 1.6]]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response returns predicted class labels.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"predictions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configure Hyperparameter Tuning
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.kubeflow.org/docs/components/katib/" rel="noopener noreferrer"&gt;Katib&lt;/a&gt; is the Kubeflow component that provides automated hyperparameter tuning and neural architecture search. It supports multiple search algorithms including random search, grid search, Bayesian optimization, Tree-structured Parzen Estimator (TPE), and CMA Evolution Strategy. Katib replaces Azure ML Experiments and Automatic Model Tuning with a Kubernetes-native solution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create Tuning Experiment
&lt;/h3&gt;

&lt;p&gt;Katib experiments are defined and submitted through the &lt;a href="https://www.kubeflow.org/docs/components/katib/getting-started/" rel="noopener noreferrer"&gt;Katib Python SDK&lt;/a&gt; from a JupyterLab notebook cell. The SDK creates the experiment resource on the cluster and manages trial pod configuration and metrics collection automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Open a terminal in JupyterLab by clicking File &amp;gt; New &amp;gt; Terminal and install the Katib Python SDK:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;kubeflow-katib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Close the terminal tab and create a new Python notebook by clicking &lt;strong&gt;File &amp;gt; New &amp;gt; Notebook&lt;/strong&gt;, then selecting &lt;strong&gt;Python 3 (ipykernel)&lt;/strong&gt; as the kernel. Run the following code in a cell to define an objective function and launch a tuning experiment.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;kubeflow.katib&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;katib&lt;/span&gt;

   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
       &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
       &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="n"&gt;parameters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;katib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;katib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="n"&gt;katib_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;katib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;KatibClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kubeflow-user-example-com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tune-experiment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
   &lt;span class="n"&gt;katib_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tune&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;objective&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;objective_metric_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;objective_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maximize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;algorithm_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;random&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;max_trial_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;parallel_trial_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;resources_per_trial&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1Gi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tune()&lt;/code&gt; method creates a Katib experiment that runs 4 trials (2 in parallel) using random search. The cell output includes a &lt;code&gt;Katib Experiment tune-experiment link here&lt;/code&gt; line. Click &lt;strong&gt;here&lt;/strong&gt; to open the experiment directly in the Katib Experiments tab and monitor trial progress. The experiment status turns green when all trials complete.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Retrieve the optimal hyperparameters by running the following code in the next cell.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="n"&gt;katib_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_experiment_condition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;katib_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_optimal_hyperparameters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  Set Up Access Control
&lt;/h2&gt;

&lt;p&gt;Kubeflow uses Dex as its OpenID Connect (OIDC) identity provider and Istio for network-level authorization. Each user gets an isolated namespace, which is called a profile, with its own resources, secrets, and RBAC policies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure Authentication
&lt;/h3&gt;

&lt;p&gt;The Dex ConfigMap uses &lt;code&gt;hashFromEnv: DEX_USER_PASSWORD&lt;/code&gt; to read the password hash from an environment variable rather than storing it directly in the ConfigMap. To change the default password, update the Secret that provides this environment variable to the Dex pod.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Install the &lt;code&gt;bcrypt&lt;/code&gt; Python package to generate a password hash:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;bcrypt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Generate a bcrypt hash for the new password. Replace &lt;code&gt;YOUR-SECURE-PASSWORD&lt;/code&gt; with the password you want to set:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import bcrypt; print(bcrypt.hashpw(b'YOUR-SECURE-PASSWORD', bcrypt.gensalt()).decode())"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the output hash for use in the next step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Update the &lt;code&gt;dex-passwords&lt;/code&gt; Secret with the new hash. Replace &lt;code&gt;GENERATED-BCRYPT-HASH&lt;/code&gt; with the hash output from the previous step:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl create secret generic dex-passwords &lt;span class="nt"&gt;-n&lt;/span&gt; auth &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    --from-literal=DEX_USER_PASSWORD='GENERATED-BCRYPT-HASH' \
    --dry-run=client -o yaml | kubectl apply -f -
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The command outputs a warning about a missing annotation. This is expected because &lt;code&gt;dex-passwords&lt;/code&gt; was created by Kubeflow without &lt;code&gt;--save-config&lt;/code&gt;. Verify that the output ends with &lt;code&gt;secret/dex-passwords configured&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;4. Restart the Dex deployment to apply the changes:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl rollout restart deployment dex &lt;span class="nt"&gt;-n&lt;/span&gt; auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;To add additional static users or configure external identity providers such as Lightweight Directory Access Protocol (LDAP), GitHub, or Google, add entries to the &lt;code&gt;staticPasswords&lt;/code&gt; list or connector entries in the Dex ConfigMap. See the &lt;a href="https://dexidp.io/docs/" rel="noopener noreferrer"&gt;Dex documentation&lt;/a&gt; for details.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Implement RBAC
&lt;/h3&gt;

&lt;p&gt;Each additional user needs a profile that follows the same manifest structure as the default user profile. Replace the &lt;code&gt;metadata.name&lt;/code&gt; and &lt;code&gt;owner.name&lt;/code&gt; fields with the new user's details, then apply the manifest with &lt;code&gt;kubectl apply -f&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To give an existing user access to another user's namespace without creating a separate profile, navigate to the target namespace in the Kubeflow dashboard namespace dropdown. Click &lt;strong&gt;Manage Contributors&lt;/strong&gt; in the left sidebar and enter the user's email address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrate Object Storage
&lt;/h2&gt;

&lt;p&gt;ML workflows generate large artifacts including trained models, pipeline outputs, datasets, and logs. Kubeflow uses SeaweedFS as its default S3-compatible object storage backend for artifact persistence. KServe also supports S3-compatible storage for loading model artifacts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure Storage Backend
&lt;/h3&gt;

&lt;p&gt;The default Kubeflow installation deploys SeaweedFS in the &lt;code&gt;kubeflow&lt;/code&gt; namespace with pre-configured credentials. Verify that the storage deployment is running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Check the SeaweedFS pod status:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; kubeflow &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;seaweedfs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. For production deployments, replace SeaweedFS with an external S3-compatible object storage service:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Replace &lt;code&gt;YOUR-ACCESS-KEY&lt;/code&gt; and &lt;code&gt;YOUR-SECRET-KEY&lt;/code&gt; with the access key and secret key for your storage service, then update the &lt;code&gt;mlpipeline-minio-artifact&lt;/code&gt; secret with the new credentials.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl create secret generic mlpipeline-minio-artifact &lt;span class="nt"&gt;-n&lt;/span&gt; kubeflow &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;accesskey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR-ACCESS-KEY &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;secretkey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR-SECRET-KEY &lt;span class="nt"&gt;--dry-run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;client &lt;span class="nt"&gt;-o&lt;/span&gt; yaml | kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test Storage Integration
&lt;/h3&gt;

&lt;p&gt;Configure KServe to access SeaweedFS for loading model artifacts stored in the cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create a storage secret for KServe model storage:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano s3-storage-secret.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Add the following configuration. Replace &lt;code&gt;ACCESS-KEY&lt;/code&gt; and &lt;code&gt;SECRET-ACCESS-KEY&lt;/code&gt; with any strong keyword:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s3-storage-secret&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubeflow-user-example-com&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;serving.kserve.io/s3-endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seaweedfs.kubeflow:8333"&lt;/span&gt;
    &lt;span class="na"&gt;serving.kserve.io/s3-usehttps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0"&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Opaque&lt;/span&gt;
&lt;span class="na"&gt;stringData&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ACCESS-KEY"&lt;/span&gt;
  &lt;span class="na"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SECRET-ACCESS-KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and close the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Apply the storage secret:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; s3-storage-secret.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Migration from Azure Machine Learning
&lt;/h2&gt;

&lt;p&gt;Migrating from Azure ML to Kubeflow involves exporting existing assets and mapping each Azure ML component to its Kubeflow equivalent. Notebooks transfer without format changes since both platforms use the standard Jupyter notebook format. Training scripts, pipelines, and experiments require rewriting to replace Azure ML SDK calls with Kubeflow and KFP SDK equivalents.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Export Notebooks:&lt;/strong&gt; Download Azure ML Studio notebooks as &lt;code&gt;.ipynb&lt;/code&gt; files from the console or by using the Azure CLI. Upload them directly to Kubeflow Notebook servers, since both platforms use standard Jupyter notebook format. Update any &lt;code&gt;azure.ai.ml&lt;/code&gt; SDK calls that rely on Azure ML-specific APIs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Convert Training Scripts:&lt;/strong&gt; Azure ML training scripts that use &lt;code&gt;azure.ai.ml&lt;/code&gt; SDK job and command patterns such as &lt;code&gt;command(...)&lt;/code&gt;, &lt;code&gt;MLClient(...)&lt;/code&gt;, &lt;code&gt;ScriptRunConfig(...)&lt;/code&gt;, or framework-specific job configurations need to be converted into standard framework training scripts for Kubernetes-based execution. Replace Azure ML-specific environment variables, datastore mounts, and output paths such as &lt;code&gt;AZUREML_MODEL_DIR&lt;/code&gt;, &lt;code&gt;./outputs&lt;/code&gt;, and Azure ML input/output bindings with Kubernetes volume mount paths. Package the training code into container images and reference them in &lt;code&gt;TrainJob&lt;/code&gt; manifests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Migrate Pipelines:&lt;/strong&gt; Azure ML Pipelines are defined using the &lt;code&gt;azure.ai.ml&lt;/code&gt; SDK and need rewriting with the KFP SDK. Azure ML Pipelines use Python-based DAG definitions via the &lt;code&gt;azure.ai.ml&lt;/code&gt; SDK; replace it with KFP's &lt;code&gt;@dsl.pipeline&lt;/code&gt; decorated functions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Export Models:&lt;/strong&gt; Download trained model artifacts from Azure Machine Learning model registries, datastores, or blob storage using the Azure CLI or &lt;code&gt;azure.ai.ml&lt;/code&gt; SDK. Upload them to the object storage backend connected to Kubeflow and update the &lt;code&gt;storageUri&lt;/code&gt; field in KServe &lt;code&gt;InferenceService&lt;/code&gt; manifests to reference the new storage location. KServe supports the same model formats commonly used in Azure ML deployments, including TensorFlow SavedModel, TorchScript, ONNX, and scikit-learn pickle models, without requiring additional conversion.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Migrate Experiments:&lt;/strong&gt; Export Azure ML experiment tracking data using the MLflow SDK's &lt;code&gt;mlflow.artifacts.download_artifacts()&lt;/code&gt; API call. For hyperparameter tuning, recreate tuning jobs as Katib experiments with equivalent search spaces and objective metrics using the Katib Python SDK.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Configure GPU scheduling for notebooks and training jobs that need accelerated compute&lt;/li&gt;
&lt;li&gt;Set up multi-tenancy with additional user profiles and namespace-level resource quotas&lt;/li&gt;
&lt;li&gt;Harden the deployment for production, including TLS termination, network policies, and backup of the object storage backend&lt;/li&gt;
&lt;li&gt;Explore additional Katib search algorithms and KServe canary rollout strategies for safer model updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full guide with additional tips, visit the original article on &lt;strong&gt;&lt;a href="https://docs.vultr.com/how-to-deploy-kubeflow-as-an-azure-ml-alternative" rel="noopener noreferrer"&gt;Vultr Docs&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>machinelearning</category>
      <category>mlops</category>
      <category>python</category>
    </item>
    <item>
      <title>Deploying Logto as a GCP Identity Platform Alternative</title>
      <dc:creator>Sanskriti Harmukh</dc:creator>
      <pubDate>Wed, 23 Sep 2026 19:22:47 +0000</pubDate>
      <link>https://dev.to/vultr/deploying-logto-as-a-gcp-identity-platform-alternative-4p3j</link>
      <guid>https://dev.to/vultr/deploying-logto-as-a-gcp-identity-platform-alternative-4p3j</guid>
      <description>&lt;p&gt;&lt;a href="https://cloud.google.com/identity-platform" rel="noopener noreferrer"&gt;Google Cloud Identity Platform&lt;/a&gt; is Google's managed CIAM service for web and mobile applications, providing user account management, customizable authentication flows, social and enterprise identity federation, MFA, and integration with Google Cloud services. It removes the need to operate an identity store, but it bills per monthly active user (MAU) in tiers where email, phone, anonymous, and social sign-in are free to 50,000 MAU while OIDC and SAML federation are free only to 50 MAU before per-user charges begin, meters phone and multi-factor messages separately, and ties the user store and its flows to a Google Cloud project. &lt;a href="https://logto.io/" rel="noopener noreferrer"&gt;Logto&lt;/a&gt; is an open-source Customer Identity and Access Management (CIAM) platform built on OAuth 2.1 and OpenID Connect (OIDC) that covers the same ground on self-hosted infrastructure without per-user billing or cloud provider API dependencies — a branded sign-in experience, social and enterprise SSO federation, MFA, role-based access control (RBAC), organizations for multi-tenancy, and webhooks, all stored in a PostgreSQL database you control. This guide walks through deploying Logto as an alternative to Google Cloud Identity Platform: the Docker Compose deployment with Traefik and automatic HTTPS, the admin console, sign-in experience and branding, email and social login connectors, enterprise SSO, MFA, RBAC and API resources, organizations, application registration, webhooks, deployment verification, and migration from Identity Platform. By the end, you'll have a fully configured, self-hosted CIAM platform and a documented path for moving an existing Identity Platform user base onto it.&lt;/p&gt;

&lt;p&gt;Before you begin, you need a Linux-based server with at least 2 CPU cores and 8 GB of RAM as a non-root user with sudo privileges, Docker and Docker Compose installed, and DNS A records pointing to your server's IP address for two subdomains: &lt;code&gt;auth.example.com&lt;/code&gt; (Logto Core API and sign-in experience) and &lt;code&gt;admin.example.com&lt;/code&gt; (Admin Console).&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding Logto Architecture
&lt;/h2&gt;

&lt;p&gt;Logto covers most Identity Platform components through a self-hosted, OIDC-standard platform. The following table maps each Identity Platform feature to its Logto counterpart and notes where no equivalent exists.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Identity Platform&lt;/th&gt;
&lt;th&gt;Logto&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Identity Platform Users&lt;/td&gt;
&lt;td&gt;Logto Users&lt;/td&gt;
&lt;td&gt;User management and hosted login pages.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity Platform SAML/OIDC Federation&lt;/td&gt;
&lt;td&gt;Logto Enterprise SSO Connectors (SAML/OIDC)&lt;/td&gt;
&lt;td&gt;Federation with external identity providers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity Platform FirebaseUI&lt;/td&gt;
&lt;td&gt;Logto Sign-in Experience&lt;/td&gt;
&lt;td&gt;Fully customizable branded authentication flow.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity Platform Social Providers&lt;/td&gt;
&lt;td&gt;Logto Social Connectors&lt;/td&gt;
&lt;td&gt;Integration with over 30 social identity providers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity Platform MFA&lt;/td&gt;
&lt;td&gt;Logto MFA&lt;/td&gt;
&lt;td&gt;TOTP, WebAuthn/Passkeys, SMS, Email OTP, and backup codes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity Platform Blocking Functions&lt;/td&gt;
&lt;td&gt;Logto Webhooks&lt;/td&gt;
&lt;td&gt;Asynchronous event-driven webhooks (not inline).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity Platform OAuth Clients / SDK Configs&lt;/td&gt;
&lt;td&gt;Logto Applications&lt;/td&gt;
&lt;td&gt;OIDC clients for SPAs, web apps, and M2M services.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity Platform Tenants (multi-tenancy)&lt;/td&gt;
&lt;td&gt;Logto Organizations&lt;/td&gt;
&lt;td&gt;Role-based access control and multi-tenant management.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity Platform Account Defender&lt;/td&gt;
&lt;td&gt;No direct equivalent&lt;/td&gt;
&lt;td&gt;Use WAF, rate limiting, or a reverse proxy for protection.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firebase / GCP Admin SDKs&lt;/td&gt;
&lt;td&gt;Logto SDKs&lt;/td&gt;
&lt;td&gt;Official SDKs for over 30 modern development frameworks.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Logto Core service (&lt;code&gt;auth.example.com&lt;/code&gt;) exposes the OIDC endpoints, Management API (&lt;code&gt;/api&lt;/code&gt;), and the user-facing sign-in experience. The Admin Console (&lt;code&gt;admin.example.com&lt;/code&gt;) is a separate frontend that manages tenant configuration through the Management API.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Deploy Logto with Docker Compose
&lt;/h2&gt;

&lt;p&gt;Logto requires PostgreSQL 14 or later to store users, configuration, and session data. This section deploys Logto for production using Docker Compose, Traefik for automatic HTTPS with Let's Encrypt, and PostgreSQL for durable storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the project directory structure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/logto/traefik/letsencrypt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Navigate to the project directory:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/logto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Create the Logto environment file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Add the following configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# Domain Configuration
&lt;/span&gt;&lt;span class="py"&gt;LOGTO_CORE_DOMAIN&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;auth.example.com&lt;/span&gt;
&lt;span class="py"&gt;LOGTO_ADMIN_DOMAIN&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;admin.example.com&lt;/span&gt;

&lt;span class="c"&gt;# Logto Endpoints
&lt;/span&gt;&lt;span class="py"&gt;ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://auth.example.com&lt;/span&gt;
&lt;span class="py"&gt;ADMIN_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://admin.example.com&lt;/span&gt;

&lt;span class="c"&gt;# SSL Configuration
&lt;/span&gt;&lt;span class="py"&gt;LETSENCRYPT_EMAIL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;admin@example.com&lt;/span&gt;

&lt;span class="c"&gt;# Database
&lt;/span&gt;&lt;span class="py"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;DB-PASSWORD&lt;/span&gt;

&lt;span class="c"&gt;# Logto image version
&lt;/span&gt;&lt;span class="py"&gt;TAG&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1.40.1&lt;/span&gt;

&lt;span class="c"&gt;# Pins the Docker Engine API version to avoid socket communication errors with newer Traefik releases
&lt;/span&gt;&lt;span class="py"&gt;DOCKER_API_VERSION&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1.54&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;auth.example.com&lt;/code&gt; and &lt;code&gt;admin.example.com&lt;/code&gt; with your subdomains, &lt;code&gt;admin@example.com&lt;/code&gt; with your Let's Encrypt notification address, &lt;code&gt;DB-PASSWORD&lt;/code&gt; with a strong PostgreSQL password, and &lt;code&gt;1.40.1&lt;/code&gt; with the Logto release you want to deploy (check the &lt;a href="https://github.com/logto-io/logto/releases" rel="noopener noreferrer"&gt;Logto releases&lt;/a&gt; page for the latest stable version).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Download the Logto Docker Compose file for the pinned release.&lt;/strong&gt; Pin the compose file URL to the same release tag as &lt;code&gt;TAG&lt;/code&gt; in the environment file — this file is maintained by Logto.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;wget https://raw.githubusercontent.com/logto-io/logto/v1.40.1/docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Update the &lt;code&gt;docker-compose.yml&lt;/code&gt; file for production use.&lt;/strong&gt; The official compose file ships with hardcoded database credentials labeled for demonstration; replace those defaults before deployment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the &lt;code&gt;app&lt;/code&gt; service and update the &lt;code&gt;DB_URL&lt;/code&gt; under &lt;code&gt;environment&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_URL=postgres://postgres:${DB_PASSWORD}@postgres:5432/logto&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the &lt;code&gt;postgres&lt;/code&gt; service and update &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_PASSWORD}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pin the PostgreSQL image to a specific release for reproducible deployments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17.5-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. Create the Traefik Docker Compose override file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano traefik/docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;8. Add the following configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik:v3.7.0&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker.exposedbydefault=false"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.address=:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.websecure.address=:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.myresolver.acme.httpchallenge=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.myresolver.acme.email=${LETSENCRYPT_EMAIL}"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entryPoint.to=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entryPoint.scheme=https"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DOCKER_API_VERSION=${DOCKER_API_VERSION}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TRUST_PROXY_HEADER=true&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./letsencrypt:/letsencrypt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock:ro&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;aliases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${LOGTO_CORE_DOMAIN}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${LOGTO_ADMIN_DOMAIN}&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-core.rule=Host(`${LOGTO_CORE_DOMAIN}`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-core.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-core.tls.certresolver=myresolver"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-core.service=logto-core"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.logto-core.loadbalancer.server.port=3001"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-admin.rule=Host(`${LOGTO_ADMIN_DOMAIN}`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-admin.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-admin.tls.certresolver=myresolver"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-admin.service=logto-admin"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.logto-admin.loadbalancer.server.port=3002"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Traefik routes HTTPS traffic to Logto on subdomain-based host rules — port 3001 serves the Core API and sign-in experience, port 3002 serves the Admin Console. Let's Encrypt certificates are provisioned automatically once DNS resolves to this server. This stack pins &lt;code&gt;svhd/logto&lt;/code&gt; to the &lt;code&gt;TAG&lt;/code&gt; value in &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;traefik&lt;/code&gt; to &lt;code&gt;v3.7.0&lt;/code&gt;, and PostgreSQL to &lt;code&gt;17.5-alpine&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Start the Logto stack:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.yml &lt;span class="nt"&gt;-f&lt;/span&gt; traefik/docker-compose.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;10. Verify the containers are running:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output displays running containers for Traefik, Logto, and PostgreSQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;11. Verify HTTPS on the Admin Console:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://admin.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output returns &lt;code&gt;HTTP/2 302&lt;/code&gt; with &lt;code&gt;location: /console/welcome&lt;/code&gt; on first boot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;12. Verify the OIDC discovery endpoint:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://auth.example.com/oidc/.well-known/openid-configuration | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output returns JSON containing &lt;code&gt;authorization_endpoint&lt;/code&gt; and &lt;code&gt;issuer&lt;/code&gt; values scoped to your &lt;code&gt;auth.example.com&lt;/code&gt; domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Access the Admin Console
&lt;/h2&gt;

&lt;p&gt;The Admin Console is the central management interface for configuring the Logto tenant.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a web browser and navigate to the Logto Admin Console at &lt;code&gt;https://admin.example.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Follow the on-screen instructions to create the initial administrator account, then log in with the new credentials.&lt;/li&gt;
&lt;li&gt;Review the sidebar to confirm access to Applications, Sign-in &amp;amp; account, Multi-factor auth, Connectors, Enterprise SSO, API resources, Roles, Organizations, Webhooks, and Audit logs.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. Configure the Sign-in Experience
&lt;/h2&gt;

&lt;p&gt;The sign-in experience defines the look, feel, and authentication flow for your users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set the brand identity:&lt;/strong&gt; navigate to &lt;strong&gt;Sign-in &amp;amp; account&lt;/strong&gt; and select &lt;strong&gt;Branding&lt;/strong&gt;, upload your logo (for example, &lt;code&gt;https://example.com/logo.png&lt;/code&gt;) and set the primary brand color (for example, &lt;code&gt;#6139F6&lt;/code&gt;), then click &lt;strong&gt;Done&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configure the email connector:&lt;/strong&gt; navigate to &lt;strong&gt;Connectors&lt;/strong&gt; and select the &lt;strong&gt;Email and SMS connectors&lt;/strong&gt; tab, click &lt;strong&gt;Set up&lt;/strong&gt; under the &lt;strong&gt;Email connector&lt;/strong&gt; and select &lt;strong&gt;SMTP&lt;/strong&gt;. Obtain the SMTP host, port, username, and password from your email provider, and enter them in the Logto Console:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;host&lt;/strong&gt;: &lt;code&gt;SMTP-HOST&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port&lt;/strong&gt;: &lt;code&gt;SMTP-PORT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth&lt;/strong&gt;: &lt;strong&gt;type&lt;/strong&gt;: &lt;code&gt;login&lt;/code&gt;, &lt;strong&gt;user&lt;/strong&gt;: &lt;code&gt;SMTP-USERNAME&lt;/code&gt;, &lt;strong&gt;pass&lt;/strong&gt;: &lt;code&gt;SMTP-PASSWORD&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From email&lt;/strong&gt;: &lt;code&gt;SENDER-EMAIL-ADDRESS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reply to&lt;/strong&gt;: &lt;code&gt;REPLY-TO-EMAIL-ADDRESS&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Toggle &lt;strong&gt;Secure&lt;/strong&gt; to &lt;strong&gt;On&lt;/strong&gt; if your provider requires TLS, send a test email, then click &lt;strong&gt;Save and Done&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Define sign-up identifiers and sign-in methods:&lt;/strong&gt; on &lt;strong&gt;Sign-in &amp;amp; account → Sign-up and sign-in&lt;/strong&gt;, select sign-up identifiers (Username, Email address, Phone number), configure sign-in methods, and click &lt;strong&gt;Save Changes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable passwordless sign-in:&lt;/strong&gt; on the same tab, enable &lt;strong&gt;Email verification code&lt;/strong&gt; or &lt;strong&gt;SMS verification code&lt;/strong&gt; under &lt;strong&gt;Sign-in methods&lt;/strong&gt;, ensure the corresponding connector is configured, then click &lt;strong&gt;Save Changes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preview the sign-in UI:&lt;/strong&gt; click &lt;strong&gt;Live preview&lt;/strong&gt; on the &lt;strong&gt;Sign-up and sign-in&lt;/strong&gt; page to test the branded interface and enabled sign-in methods before applying changes to production traffic.&lt;/p&gt;

&lt;p&gt;Logto also supports SMS-based registration and sign-in via third-party providers such as Twilio or Vonage — configure an SMS connector the same way as the email connector. Users access the hosted sign-in UI through a registered application with a valid &lt;code&gt;client_id&lt;/code&gt; and redirect URI, not by visiting &lt;code&gt;auth.example.com&lt;/code&gt; directly; direct navigation without an application context returns an unknown-session page.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Set Up Social Login Connectors
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Connectors&lt;/strong&gt; in the left sidebar and select the &lt;strong&gt;Social connectors&lt;/strong&gt; tab, then click &lt;strong&gt;Add social connector&lt;/strong&gt; and select &lt;strong&gt;Google&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Create a project in the &lt;a href="https://console.cloud.google.com/" rel="noopener noreferrer"&gt;Google Cloud Console&lt;/a&gt;: create a new project, go to &lt;strong&gt;APIs &amp;amp; Services → OAuth consent screen&lt;/strong&gt;, configure the consent screen and add your domain (for example, &lt;code&gt;example.com&lt;/code&gt;) as an authorized domain, go to &lt;strong&gt;Credentials → Create Credentials → OAuth client ID&lt;/strong&gt;, select &lt;strong&gt;Web application&lt;/strong&gt;, and add the Logto callback URI (for example, &lt;code&gt;https://auth.example.com/callback/google&lt;/code&gt;) to &lt;strong&gt;Authorized redirect URIs&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;Client ID&lt;/strong&gt; and &lt;strong&gt;Client Secret&lt;/strong&gt; from Google and paste them into the Logto social connector configuration.&lt;/li&gt;
&lt;li&gt;Configure the &lt;strong&gt;Scope&lt;/strong&gt; field — Logto requests &lt;code&gt;openid&lt;/code&gt;, &lt;code&gt;profile&lt;/code&gt;, and &lt;code&gt;email&lt;/code&gt; by default; append additional scope URLs if the application needs more Google API access.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save and Done&lt;/strong&gt;, then repeat the process for &lt;strong&gt;GitHub&lt;/strong&gt; and &lt;strong&gt;Apple&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;On &lt;strong&gt;Sign-in &amp;amp; account → Sign-up and sign-in&lt;/strong&gt;, under &lt;strong&gt;Social sign-in&lt;/strong&gt;, click &lt;strong&gt;Add social connector&lt;/strong&gt; and select the connectors you configured, then &lt;strong&gt;Save Changes&lt;/strong&gt; and test with &lt;strong&gt;Live preview&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  5. Configure Enterprise SSO
&lt;/h2&gt;

&lt;p&gt;Enterprise Single Sign-On (SSO) enables corporate users to authenticate via their own Identity Provider (IdP), such as Okta or Microsoft Entra ID.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Enterprise SSO&lt;/strong&gt;, click &lt;strong&gt;Add enterprise connector&lt;/strong&gt;, and select &lt;strong&gt;Okta&lt;/strong&gt; (or &lt;strong&gt;SAML&lt;/strong&gt; / &lt;strong&gt;OIDC&lt;/strong&gt; for a generic connector). Use the &lt;strong&gt;Connection guide&lt;/strong&gt; button for provider-specific setup instructions.&lt;/li&gt;
&lt;li&gt;In your Okta dashboard, register a new OIDC application, set the &lt;strong&gt;Login redirect URI&lt;/strong&gt; to the value provided by Logto, and obtain the &lt;strong&gt;Client ID&lt;/strong&gt;, &lt;strong&gt;Client Secret&lt;/strong&gt;, and &lt;strong&gt;Issuer URL&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter these details into the Logto connector configuration and click &lt;strong&gt;Save&lt;/strong&gt;. For SAML connectors, exchange IdP metadata by uploading the metadata file or entering the metadata URL.&lt;/li&gt;
&lt;li&gt;On the &lt;strong&gt;SSO Experience&lt;/strong&gt; tab, add the email domains associated with the organization (for example, &lt;code&gt;example.com&lt;/code&gt;) — users with matching email domains are automatically routed to the enterprise IdP login page, and other sign-in methods are disabled for those domains.&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;JIT provisioning&lt;/strong&gt; to create users automatically on first SSO sign-in, then click &lt;strong&gt;Save changes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  6. Enable Multi-Factor Authentication
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Multi-factor auth&lt;/strong&gt; in the left sidebar.&lt;/li&gt;
&lt;li&gt;Toggle the desired MFA methods to &lt;strong&gt;On&lt;/strong&gt;: &lt;strong&gt;Authenticator app (TOTP)&lt;/strong&gt;, &lt;strong&gt;WebAuthn / Passkeys&lt;/strong&gt; (biometric or hardware key), &lt;strong&gt;Backup codes&lt;/strong&gt;, and &lt;strong&gt;SMS&lt;/strong&gt; or &lt;strong&gt;Email OTP&lt;/strong&gt; (requires the corresponding connector).&lt;/li&gt;
&lt;li&gt;Configure the &lt;strong&gt;MFA Policy&lt;/strong&gt; (optional MFA or required on every login), then click &lt;strong&gt;Save changes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  7. Configure RBAC and API Resources
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;API Resources&lt;/strong&gt;, click &lt;strong&gt;Create API resource&lt;/strong&gt;, enter an &lt;strong&gt;API Name&lt;/strong&gt; and a unique &lt;strong&gt;API Identifier&lt;/strong&gt; (for example, &lt;code&gt;https://api.example.com/orders&lt;/code&gt;), and define &lt;strong&gt;Permissions (Scopes)&lt;/strong&gt; such as &lt;code&gt;read:orders&lt;/code&gt; and &lt;code&gt;write:orders&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Roles&lt;/strong&gt;, click &lt;strong&gt;Create role&lt;/strong&gt;, assign the API permissions to the role, and click &lt;strong&gt;Save&lt;/strong&gt;. On the &lt;strong&gt;Assign user&lt;/strong&gt; screen, assign users now or click &lt;strong&gt;Skip&lt;/strong&gt; to assign them later from &lt;strong&gt;User management&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If global roles must appear in access tokens, navigate to &lt;strong&gt;Custom JWT&lt;/strong&gt; and configure an access token script to inject role claims into issued JWTs.&lt;/li&gt;
&lt;li&gt;Open the API resource you created and click &lt;strong&gt;Check guide&lt;/strong&gt; to view framework-specific integration code (Express, Python, Spring Boot, and others) for initializing the Logto SDK, protecting API routes, and validating the access token.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  8. Set Up Organizations for Multi-Tenancy
&lt;/h2&gt;

&lt;p&gt;Organizations offer multi-tenant isolation for B2B applications where users belong to different corporate entities. An organization template is a blueprint defining a consistent set of roles and permissions available to every organization in your Logto tenant.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Organization template&lt;/strong&gt; and create &lt;strong&gt;Organization permissions&lt;/strong&gt; (fine-grained, non-API actions such as &lt;code&gt;read:resource&lt;/code&gt;, &lt;code&gt;edit:resource&lt;/code&gt;, &lt;code&gt;delete:resource&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Create &lt;strong&gt;Organization roles&lt;/strong&gt; (for example, &lt;code&gt;Admin&lt;/code&gt; or &lt;code&gt;Member&lt;/code&gt;) and map the permissions to each role.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Organizations&lt;/strong&gt;, click &lt;strong&gt;Create organization&lt;/strong&gt;, select it, and go to the &lt;strong&gt;Members&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add Members&lt;/strong&gt;, assign an organization role to each member from the dropdown — Logto scopes JWT tokens to include the organization context when organization-scoped tokens are requested.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Check guide&lt;/strong&gt; to view integration tutorials for multi-tenant features and org-scoped tokens.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  9. Register Applications
&lt;/h2&gt;

&lt;p&gt;Register your frontend and backend services as applications within Logto to authenticate users and obtain tokens. Specific steps vary by application type — &lt;strong&gt;Single-Page App&lt;/strong&gt;, &lt;strong&gt;Traditional Web App&lt;/strong&gt;, &lt;strong&gt;Native App&lt;/strong&gt;, or &lt;strong&gt;Machine-to-Machine&lt;/strong&gt; — and the console provides a framework-specific tutorial for each.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Applications&lt;/strong&gt;. If none exist, select a supported framework (React, Next.js, Node.js, and others) and click &lt;strong&gt;Start building&lt;/strong&gt;; otherwise click &lt;strong&gt;Create application&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter your application name, click &lt;strong&gt;Create application&lt;/strong&gt;, and follow the Jumpstart guide to integrate Logto.&lt;/li&gt;
&lt;li&gt;Configure the &lt;strong&gt;Redirect URIs&lt;/strong&gt; and &lt;strong&gt;Post Sign-out Redirect URIs&lt;/strong&gt;, note the &lt;strong&gt;App ID&lt;/strong&gt; and integration endpoint, then click &lt;strong&gt;Save changes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Register at least three application types to cover common architectures: a &lt;strong&gt;Single-Page App (SPA)&lt;/strong&gt; for React/Vue/Angular frontends, a &lt;strong&gt;Traditional Web App&lt;/strong&gt; for server-rendered applications with a backend session, and a &lt;strong&gt;Machine-to-Machine (M2M)&lt;/strong&gt; application for backend services that call the Management API without user interaction. Open the application sign-in URL or use the Logto SDK in a test client to confirm the redirect-based sign-in flow completes successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Configure Webhooks
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Webhooks&lt;/strong&gt;, click &lt;strong&gt;Create webhook&lt;/strong&gt;, enter the backend &lt;strong&gt;Endpoint URL&lt;/strong&gt; and a descriptive name.&lt;/li&gt;
&lt;li&gt;Select the &lt;strong&gt;Webhook events&lt;/strong&gt; to monitor (for example, &lt;code&gt;User.Created&lt;/code&gt;, &lt;code&gt;PostSignIn&lt;/code&gt;, &lt;code&gt;User.Data.Updated&lt;/code&gt;), add optional &lt;strong&gt;Custom headers&lt;/strong&gt; if needed, and click &lt;strong&gt;Create webhook&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Use the &lt;strong&gt;Signing Key&lt;/strong&gt; shown in the webhook settings to cryptographically verify that incoming payloads originated from your Logto instance, then send a test payload to confirm the endpoint receives and processes events.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  11. Verify the Deployment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure&lt;/strong&gt;: Confirm all containers are running with &lt;code&gt;docker ps&lt;/code&gt; — Traefik, Logto, and PostgreSQL should report an &lt;code&gt;Up&lt;/code&gt; status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS&lt;/strong&gt;: Open &lt;code&gt;https://admin.example.com&lt;/code&gt; and &lt;code&gt;https://auth.example.com&lt;/code&gt; in a browser; both should load with valid Let's Encrypt certificates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OIDC discovery&lt;/strong&gt;: Run &lt;code&gt;curl -s https://auth.example.com/oidc/.well-known/openid-configuration&lt;/code&gt; and confirm the &lt;code&gt;issuer&lt;/code&gt; matches &lt;code&gt;https://auth.example.com/oidc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin Console&lt;/strong&gt;: Create the initial admin account and confirm the dashboard loads with all sidebar sections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User registration and sign-in&lt;/strong&gt;: Register a test user through your application and confirm password-based sign-in works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Social login, MFA, RBAC, and organizations&lt;/strong&gt;: Test each configured method end to end — social sign-in through Live preview, MFA enforcement on a test user's next sign-in, an access token containing expected scopes after role assignment, and organization context appearing in the token after adding a user to an organization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit logs&lt;/strong&gt;: Navigate to &lt;strong&gt;Audit logs&lt;/strong&gt; in the Admin Console and confirm sign-in and configuration events are recorded.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  12. Migrate from GCP Identity Platform to Logto
&lt;/h2&gt;

&lt;p&gt;Migrating from GCP Identity Platform to Logto involves mapping Identity Platform Users and Tenants to Logto Organizations and Users, replacing Firebase/GCP Admin SDK calls with Logto SDKs and the Management API, and converting Blocking Functions to webhooks or Custom JWT scripts. Because Identity Platform does not export password hashes or MFA secrets, most migrations follow a bulk user import with mandatory re-authentication strategy. The steps below assume you already completed the deployment, admin console, and application registration sections above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User migration.&lt;/strong&gt; GCP Identity Platform bundles user directories, authentication flows, OAuth client configurations, and tenant isolation into a single managed service; Logto splits these across users, the sign-in experience, applications, roles, and organizations.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Export&lt;/strong&gt;: retrieve user profiles using the &lt;a href="https://firebase.google.com/docs/auth/admin/manage-users" rel="noopener noreferrer"&gt;Firebase Admin SDK&lt;/a&gt; (&lt;code&gt;listUsers()&lt;/code&gt;) or the &lt;a href="https://docs.cloud.google.com/identity-platform/docs/reference/rest/v1/accounts" rel="noopener noreferrer"&gt;Identity Toolkit API&lt;/a&gt;. For multi-tenant projects, iterate through each tenant to export tenant-scoped users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import&lt;/strong&gt;: create users through the &lt;a href="https://openapi.logto.io/" rel="noopener noreferrer"&gt;Logto Management API&lt;/a&gt; (&lt;code&gt;POST /api/users&lt;/code&gt; at &lt;code&gt;https://auth.example.com/api&lt;/code&gt;), authenticated with an access token from a Logto Machine-to-Machine application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passwords&lt;/strong&gt;: Identity Platform does not expose password hashes — import users with temporary credentials and trigger a password reset on first login.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MFA enrollments&lt;/strong&gt;: store the original Identity Platform/Firebase &lt;code&gt;localId&lt;/code&gt; (UID) in Logto &lt;code&gt;customData&lt;/code&gt; (for example, &lt;code&gt;legacy_firebase_uid&lt;/code&gt;) to preserve linkages in downstream application databases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Application migration.&lt;/strong&gt; If applications use the Firebase Authentication SDK, replace Firebase Auth APIs with the Logto SDK: &lt;code&gt;signInWithEmailAndPassword&lt;/code&gt;/&lt;code&gt;signInWithPopup&lt;/code&gt;/embedded forms become redirect-based Universal Login; the Firebase API Key/OAuth Client ID becomes the Logto Application ID (&lt;code&gt;client_id&lt;/code&gt;); Authorized Domains/Callback URLs become Redirect URIs and Post Sign-out Redirect URIs; token refresh moves to the Logto SDK's session management or standard OIDC refresh flows. If applications call the Identity Toolkit REST API or Firebase Admin SDK directly, replace &lt;code&gt;verifyPassword&lt;/code&gt;/&lt;code&gt;verifyCustomToken&lt;/code&gt;/&lt;code&gt;signUp&lt;/code&gt; with the OIDC authorization code flow (PKCE) or client credentials flow (M2M), &lt;code&gt;getAccountInfo&lt;/code&gt; with the OIDC UserInfo endpoint or Management API user lookup, and validate Firebase ID tokens against the Logto JWKS endpoint (&lt;code&gt;https://auth.example.com/oidc/jwks&lt;/code&gt;, issuer &lt;code&gt;https://auth.example.com/oidc&lt;/code&gt;) instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role and group migration.&lt;/strong&gt; Identity Platform has no native group primitive — roles are typically assigned via custom claims (&lt;code&gt;setCustomUserClaims&lt;/code&gt;) or tenant isolation. Custom claims that gate API access or admin capabilities map to global Logto Roles: create each role (&lt;code&gt;POST /api/roles&lt;/code&gt;), define API resource permissions, and assign users (&lt;code&gt;POST /api/users/{userId}/roles&lt;/code&gt;) — configure a Custom JWT script to inject role claims, since global roles aren't included by default. Tenant-scoped users representing B2B customers map to Logto Organizations: create the organization (&lt;code&gt;POST /api/organizations&lt;/code&gt;), define organization roles in the Organization template, and add members (&lt;code&gt;POST /api/organizations/{id}/users&lt;/code&gt;), importing any relational tenant-membership data from Firestore or custom claims. Update backend route guards to parse the claims your Custom JWT script emits instead of Firebase custom claims (for example, &lt;code&gt;decodedToken.admin&lt;/code&gt; or &lt;code&gt;decodedToken.tenant_id&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blocking Function migration.&lt;/strong&gt; Post-confirmation/post-authentication triggers (async) map to Logto Webhooks (&lt;code&gt;User.Created&lt;/code&gt;, &lt;code&gt;PostSignIn&lt;/code&gt;, &lt;code&gt;User.Data.Updated&lt;/code&gt;). Custom claims and token enrichment map to Custom JWT access token scripts, replacing &lt;code&gt;setCustomUserClaims&lt;/code&gt; or blocking-function logic. Pre-authentication/pre-sign-up triggers (sync, blocking) map to application middleware, API gateway rules, or a custom authentication proxy — Logto webhooks cannot block or reject an in-progress sign-in or sign-up synchronously. Custom message triggers map to email/SMS connectors or a webhook consumer. Unlike Cloud Functions, which scale per invocation, Custom JWT scripts run on every token issuance and webhook consumers run as always-on services — plan capacity accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data storage considerations.&lt;/strong&gt; Identity Platform sessions and refresh tokens become invalid at cutover — force re-authentication and clear local Firebase SDK session state (&lt;code&gt;onAuthStateChanged&lt;/code&gt; listeners will fire with &lt;code&gt;null&lt;/code&gt;). Applications that store Identity Platform &lt;code&gt;localId&lt;/code&gt; values rely on the &lt;code&gt;legacy_identity_platform_uid&lt;/code&gt; mapping in Logto &lt;code&gt;customData&lt;/code&gt; until updated. Replace calls to the Identity Toolkit/Firebase Admin SDK (&lt;code&gt;getUser&lt;/code&gt;, &lt;code&gt;listUsers&lt;/code&gt;, &lt;code&gt;updateUser&lt;/code&gt;) with the Logto Management API at &lt;code&gt;https://auth.example.com/api&lt;/code&gt;. Recreate any Identity Platform SAML/OIDC federation as Logto Enterprise SSO or social connectors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Things to take care of during migration&lt;/strong&gt;: moving embedded Firebase sign-in forms (&lt;code&gt;signInWithEmailAndPassword&lt;/code&gt;, &lt;code&gt;signInWithPopup&lt;/code&gt;) and direct Identity Toolkit API calls to redirect-based OIDC is the largest application-code change in most migrations; Identity Platform supports synchronous blocking functions during sign-up and sign-in, but Logto webhooks are asynchronous only, so blocking validation needs application middleware or an API gateway; Identity Platform does not grant end users direct cloud-resource access via IAM roles, so there's no equivalent to migrate away from (a separate Workload Identity Federation setup for backend services is unrelated to end-user auth); Identity Platform's Account Defender has no direct Logto OSS equivalent, so implement rate limiting or WAF rules at the reverse proxy; TOTP secrets and enrolled phone numbers can't be extracted, so users must re-enroll after cutover; Identity Platform ID tokens nest provider and tenant metadata under a &lt;code&gt;firebase&lt;/code&gt; claim (&lt;code&gt;sign_in_provider&lt;/code&gt;, &lt;code&gt;tenant&lt;/code&gt;), while Logto issues standard OIDC claims (&lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;scope&lt;/code&gt;, &lt;code&gt;organization_id&lt;/code&gt;); and Identity Platform's tiered per-MAU pricing plus separate SMS/phone-auth charges changes to an infrastructure-based cost model, so calculate total cost of ownership before cutover. Run a phased migration: import users first, validate token flows in a staging tenant, then redirect production traffic to Logto and disable sign-in through the Identity Platform project or tenant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Configure a Custom JWT script to enrich access tokens with the role and organization claims your applications expect.&lt;/li&gt;
&lt;li&gt;Set up organization-scoped applications for each B2B tenant migrating off Identity Platform Tenants.&lt;/li&gt;
&lt;li&gt;Add rate limiting or WAF rules at the reverse proxy layer to replace Identity Platform's Account Defender.&lt;/li&gt;
&lt;li&gt;Run a staging-tenant migration rehearsal before cutting production traffic over from Identity Platform.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full guide with additional tips, visit the original article on &lt;strong&gt;&lt;a href="https://docs.vultr.com/how-to-deploy-logto-as-a-gcp-identity-platform-alternative" rel="noopener noreferrer"&gt;Vultr Docs&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>gcp</category>
      <category>oauth</category>
      <category>docker</category>
      <category>security</category>
    </item>
    <item>
      <title>Deploying Logto as an AWS Cognito Alternative</title>
      <dc:creator>Sanskriti Harmukh</dc:creator>
      <pubDate>Wed, 23 Sep 2026 19:21:55 +0000</pubDate>
      <link>https://dev.to/vultr/deploying-logto-as-an-aws-cognito-alternative-3nmb</link>
      <guid>https://dev.to/vultr/deploying-logto-as-an-aws-cognito-alternative-3nmb</guid>
      <description>&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html" rel="noopener noreferrer"&gt;AWS Cognito&lt;/a&gt; is Amazon's managed authentication service for web and mobile applications, providing User Pools, Identity Pools for federated AWS resource access, a hosted login UI, Lambda triggers, and passwordless sign-in across Lite, Essentials, and Plus tiers. It removes the need to operate an identity store, but it bills per monthly active user (MAU) beyond a free tier of 10,000 MAU that applies only to Lite and Essentials, gates threat protection and adaptive authentication behind the Plus tier (which has no free tier at all), and ties the user directory and its hooks to a specific cloud provider's regions, functions, and IAM. &lt;a href="https://logto.io/" rel="noopener noreferrer"&gt;Logto&lt;/a&gt; is an open-source Customer Identity and Access Management (CIAM) platform built on OAuth 2.1 and OpenID Connect (OIDC) that covers the same ground on self-hosted infrastructure without per-user billing or cloud provider API dependencies — a branded sign-in experience, social and enterprise SSO federation, multi-factor authentication (MFA), role-based access control (RBAC), organizations for multi-tenancy, and webhooks, all stored in a PostgreSQL database you control. This guide walks through deploying Logto as an alternative to AWS Cognito: the Docker Compose deployment with Traefik and automatic HTTPS, the admin console, sign-in experience and branding, email and social login connectors, enterprise SSO, MFA, RBAC and API resources, organizations, application registration, webhooks, deployment verification, and migration from AWS Cognito. By the end, you'll have a fully configured, self-hosted CIAM platform and a documented path for moving an existing Cognito user base onto it.&lt;/p&gt;

&lt;p&gt;Before you begin, you need a Linux-based server with at least 2 CPU cores and 4 GB of RAM as a non-root user with sudo privileges, Docker and Docker Compose installed, and DNS A records pointing to your server's IP address for two subdomains: &lt;code&gt;auth.example.com&lt;/code&gt; (Logto Core API and sign-in experience) and &lt;code&gt;admin.example.com&lt;/code&gt; (Admin Console).&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding Logto Architecture
&lt;/h2&gt;

&lt;p&gt;Logto covers most AWS Cognito components through a self-hosted, OIDC-standard platform. The following table maps each Cognito feature to its Logto counterpart and notes where no equivalent exists.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AWS Cognito&lt;/th&gt;
&lt;th&gt;Logto&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cognito User Pools&lt;/td&gt;
&lt;td&gt;Logto Users + Sign-in Experience&lt;/td&gt;
&lt;td&gt;User management and hosted login pages.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cognito User Pool SAML and OIDC identity providers&lt;/td&gt;
&lt;td&gt;Logto Enterprise SSO Connectors (SAML/OIDC)&lt;/td&gt;
&lt;td&gt;Federation with external identity providers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cognito Identity Pools&lt;/td&gt;
&lt;td&gt;No direct equivalent&lt;/td&gt;
&lt;td&gt;Grants access to AWS resources through IAM roles.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cognito Hosted UI&lt;/td&gt;
&lt;td&gt;Logto Sign-in Experience&lt;/td&gt;
&lt;td&gt;Fully customizable branded authentication flow.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cognito Social Providers&lt;/td&gt;
&lt;td&gt;Logto Social Connectors&lt;/td&gt;
&lt;td&gt;Integration with over 30 social identity providers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cognito MFA (SMS, TOTP)&lt;/td&gt;
&lt;td&gt;Logto MFA&lt;/td&gt;
&lt;td&gt;TOTP, WebAuthn/Passkeys, SMS, Email OTP, and backup codes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cognito Lambda Triggers&lt;/td&gt;
&lt;td&gt;Logto Webhooks&lt;/td&gt;
&lt;td&gt;Asynchronous event-driven webhooks (not inline).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cognito App Clients&lt;/td&gt;
&lt;td&gt;Logto Applications&lt;/td&gt;
&lt;td&gt;OIDC clients for SPAs, web apps, and M2M services.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cognito Groups&lt;/td&gt;
&lt;td&gt;Logto Roles + Organizations&lt;/td&gt;
&lt;td&gt;Role-based access control and multi-tenant management.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cognito Advanced Security&lt;/td&gt;
&lt;td&gt;No direct equivalent&lt;/td&gt;
&lt;td&gt;Use WAF, rate limiting, or a reverse proxy for protection.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Amplify SDKs&lt;/td&gt;
&lt;td&gt;Logto SDKs&lt;/td&gt;
&lt;td&gt;Official SDKs for over 30 modern development frameworks.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Logto Core service (&lt;code&gt;auth.example.com&lt;/code&gt;) exposes the OIDC endpoints, Management API (&lt;code&gt;/api&lt;/code&gt;), and the user-facing sign-in experience. The Admin Console (&lt;code&gt;admin.example.com&lt;/code&gt;) is a separate frontend that manages tenant configuration through the Management API.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Deploy Logto with Docker Compose
&lt;/h2&gt;

&lt;p&gt;Logto requires PostgreSQL 14 or later to store users, configuration, and session data. This section deploys Logto for production using Docker Compose, Traefik for automatic HTTPS with Let's Encrypt, and PostgreSQL for durable storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the project directory structure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/logto/traefik/letsencrypt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Navigate to the project directory:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/logto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Create the Logto environment file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Add the following configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# Domain Configuration
&lt;/span&gt;&lt;span class="py"&gt;LOGTO_CORE_DOMAIN&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;auth.example.com&lt;/span&gt;
&lt;span class="py"&gt;LOGTO_ADMIN_DOMAIN&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;admin.example.com&lt;/span&gt;

&lt;span class="c"&gt;# Logto Endpoints
&lt;/span&gt;&lt;span class="py"&gt;ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://auth.example.com&lt;/span&gt;
&lt;span class="py"&gt;ADMIN_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://admin.example.com&lt;/span&gt;

&lt;span class="c"&gt;# SSL Configuration
&lt;/span&gt;&lt;span class="py"&gt;LETSENCRYPT_EMAIL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;admin@example.com&lt;/span&gt;

&lt;span class="c"&gt;# Database
&lt;/span&gt;&lt;span class="py"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;DB-PASSWORD&lt;/span&gt;

&lt;span class="c"&gt;# Logto image version
&lt;/span&gt;&lt;span class="py"&gt;TAG&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1.40.1&lt;/span&gt;

&lt;span class="c"&gt;# Pins the Docker Engine API version to avoid socket communication errors with newer Traefik releases
&lt;/span&gt;&lt;span class="py"&gt;DOCKER_API_VERSION&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1.54&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;auth.example.com&lt;/code&gt; and &lt;code&gt;admin.example.com&lt;/code&gt; with your subdomains, &lt;code&gt;admin@example.com&lt;/code&gt; with your Let's Encrypt notification address, &lt;code&gt;DB-PASSWORD&lt;/code&gt; with a strong PostgreSQL password, and &lt;code&gt;1.40.1&lt;/code&gt; with the Logto release you want to deploy (check the &lt;a href="https://github.com/logto-io/logto/releases" rel="noopener noreferrer"&gt;Logto releases&lt;/a&gt; page for the latest stable version).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Download the Logto Docker Compose file for the pinned release.&lt;/strong&gt; Pin the compose file URL to the same release tag as &lt;code&gt;TAG&lt;/code&gt; in the environment file — this file is maintained by Logto.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;wget https://raw.githubusercontent.com/logto-io/logto/v1.40.1/docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Update the &lt;code&gt;docker-compose.yml&lt;/code&gt; file for production use.&lt;/strong&gt; The official compose file ships with hardcoded database credentials labeled for demonstration; replace those defaults before deployment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the &lt;code&gt;app&lt;/code&gt; service and update the &lt;code&gt;DB_URL&lt;/code&gt; under &lt;code&gt;environment&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DB_URL=postgres://postgres:${DB_PASSWORD}@postgres:5432/logto&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the &lt;code&gt;postgres&lt;/code&gt; service and update &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_PASSWORD}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pin the PostgreSQL image to a specific release for reproducible deployments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17.5-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. Create the Traefik Docker Compose override file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano traefik/docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;8. Add the following configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik:v3.7.0&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker.exposedbydefault=false"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.address=:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.websecure.address=:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.myresolver.acme.httpchallenge=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.myresolver.acme.email=${LETSENCRYPT_EMAIL}"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entryPoint.to=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entryPoint.scheme=https"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DOCKER_API_VERSION=${DOCKER_API_VERSION}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TRUST_PROXY_HEADER=true&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./letsencrypt:/letsencrypt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock:ro&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;aliases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${LOGTO_CORE_DOMAIN}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${LOGTO_ADMIN_DOMAIN}&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-core.rule=Host(`${LOGTO_CORE_DOMAIN}`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-core.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-core.tls.certresolver=myresolver"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-core.service=logto-core"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.logto-core.loadbalancer.server.port=3001"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-admin.rule=Host(`${LOGTO_ADMIN_DOMAIN}`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-admin.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-admin.tls.certresolver=myresolver"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.logto-admin.service=logto-admin"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.logto-admin.loadbalancer.server.port=3002"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Traefik routes HTTPS traffic to Logto on subdomain-based host rules — port 3001 serves the Core API and sign-in experience, port 3002 serves the Admin Console. Let's Encrypt certificates are provisioned automatically once DNS resolves to this server. This stack pins &lt;code&gt;svhd/logto&lt;/code&gt; to the &lt;code&gt;TAG&lt;/code&gt; value in &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;traefik&lt;/code&gt; to &lt;code&gt;v3.7.0&lt;/code&gt;, and PostgreSQL to &lt;code&gt;17.5-alpine&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Start the Logto stack:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.yml &lt;span class="nt"&gt;-f&lt;/span&gt; traefik/docker-compose.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;10. Verify the containers are running:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output displays running containers for Traefik, Logto, and PostgreSQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;11. Verify HTTPS on the Admin Console:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://admin.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output returns &lt;code&gt;HTTP/2 302&lt;/code&gt; with &lt;code&gt;location: /console/welcome&lt;/code&gt; on first boot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;12. Verify the OIDC discovery endpoint:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://auth.example.com/oidc/.well-known/openid-configuration | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output returns JSON containing &lt;code&gt;authorization_endpoint&lt;/code&gt; and &lt;code&gt;issuer&lt;/code&gt; values scoped to your &lt;code&gt;auth.example.com&lt;/code&gt; domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Access the Admin Console
&lt;/h2&gt;

&lt;p&gt;The Admin Console is the central management interface for configuring the Logto tenant.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a web browser and navigate to the Logto Admin Console at &lt;code&gt;https://admin.example.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Follow the on-screen instructions to create the initial administrator account, then log in with the new credentials.&lt;/li&gt;
&lt;li&gt;Review the sidebar to confirm access to Applications, Sign-in &amp;amp; account, Multi-factor auth, Connectors, Enterprise SSO, API resources, Roles, Organizations, Webhooks, and Audit logs.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. Configure the Sign-in Experience
&lt;/h2&gt;

&lt;p&gt;The sign-in experience defines the look, feel, and authentication flow for your users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set the brand identity:&lt;/strong&gt; navigate to &lt;strong&gt;Sign-in &amp;amp; account&lt;/strong&gt; in the left sidebar (on first visit, click &lt;strong&gt;Get started&lt;/strong&gt; then &lt;strong&gt;Got it&lt;/strong&gt;), upload your logo (for example, &lt;code&gt;https://example.com/logo.png&lt;/code&gt;) and set the primary brand color (for example, &lt;code&gt;#6139F6&lt;/code&gt;) under &lt;strong&gt;Branding area&lt;/strong&gt;, then click &lt;strong&gt;Done&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configure the email connector:&lt;/strong&gt; navigate to &lt;strong&gt;Connectors&lt;/strong&gt; and select the &lt;strong&gt;Email and SMS connectors&lt;/strong&gt; tab, click &lt;strong&gt;Set up&lt;/strong&gt; under the &lt;strong&gt;Email connector&lt;/strong&gt; and select &lt;strong&gt;SMTP&lt;/strong&gt;. Obtain the SMTP host, port, username, and password from your email provider, and enter them in the Logto Console:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;host&lt;/strong&gt;: &lt;code&gt;SMTP-HOST&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port&lt;/strong&gt;: &lt;code&gt;SMTP-PORT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth&lt;/strong&gt;: &lt;strong&gt;type&lt;/strong&gt;: &lt;code&gt;login&lt;/code&gt;, &lt;strong&gt;user&lt;/strong&gt;: &lt;code&gt;SMTP-USERNAME&lt;/code&gt;, &lt;strong&gt;pass&lt;/strong&gt;: &lt;code&gt;SMTP-PASSWORD&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From email&lt;/strong&gt;: &lt;code&gt;SENDER-EMAIL-ADDRESS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reply to&lt;/strong&gt;: &lt;code&gt;REPLY-TO-EMAIL-ADDRESS&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Toggle &lt;strong&gt;Secure&lt;/strong&gt; to &lt;strong&gt;On&lt;/strong&gt; if your provider requires TLS, send a test email, then click &lt;strong&gt;Save and Done&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Define sign-up identifiers and sign-in methods:&lt;/strong&gt; on &lt;strong&gt;Sign-in &amp;amp; account → Sign-up and sign-in&lt;/strong&gt;, select sign-up identifiers (Username, Email address, Phone number), configure sign-in methods, and click &lt;strong&gt;Save Changes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable passwordless sign-in:&lt;/strong&gt; on the same tab, enable &lt;strong&gt;Email verification code&lt;/strong&gt; or &lt;strong&gt;SMS verification code&lt;/strong&gt; under &lt;strong&gt;Sign-in methods&lt;/strong&gt;, ensure the corresponding connector is configured, then click &lt;strong&gt;Save Changes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preview the sign-in UI:&lt;/strong&gt; click &lt;strong&gt;Live preview&lt;/strong&gt; on the &lt;strong&gt;Sign-up and sign-in&lt;/strong&gt; page to test the branded interface and enabled sign-in methods before applying changes to production traffic.&lt;/p&gt;

&lt;p&gt;Logto also supports SMS-based registration and sign-in via third-party providers such as Twilio or Vonage — configure an SMS connector the same way as the email connector. Users access the hosted sign-in UI through a registered application with a valid &lt;code&gt;client_id&lt;/code&gt; and redirect URI, not by visiting &lt;code&gt;auth.example.com&lt;/code&gt; directly; direct navigation without an application context returns an unknown-session page.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Set Up Social Login Connectors
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Connectors&lt;/strong&gt; in the left sidebar and select the &lt;strong&gt;Social connectors&lt;/strong&gt; tab, then click &lt;strong&gt;Add Social Connector&lt;/strong&gt; and select &lt;strong&gt;Google&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Create a project in the &lt;a href="https://console.cloud.google.com/" rel="noopener noreferrer"&gt;Google Cloud Console&lt;/a&gt;: create a new project, go to &lt;strong&gt;APIs &amp;amp; Services → OAuth consent screen&lt;/strong&gt;, configure the consent screen and add your domain (for example, &lt;code&gt;example.com&lt;/code&gt;) as an authorized domain, go to &lt;strong&gt;Credentials → Create Credentials → OAuth client ID&lt;/strong&gt;, select &lt;strong&gt;Web application&lt;/strong&gt;, and add the Logto callback URI (for example, &lt;code&gt;https://auth.example.com/callback/google&lt;/code&gt;) to &lt;strong&gt;Authorized redirect URIs&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;Client ID&lt;/strong&gt; and &lt;strong&gt;Client Secret&lt;/strong&gt; from Google and paste them into the Logto social connector configuration.&lt;/li&gt;
&lt;li&gt;Configure the &lt;strong&gt;Scope&lt;/strong&gt; field — Logto requests &lt;code&gt;openid&lt;/code&gt;, &lt;code&gt;profile&lt;/code&gt;, and &lt;code&gt;email&lt;/code&gt; by default; append additional scope URLs if the application needs more Google API access.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save and Done&lt;/strong&gt;, then repeat the process for &lt;strong&gt;GitHub&lt;/strong&gt; and &lt;strong&gt;Apple&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;On &lt;strong&gt;Sign-in &amp;amp; account → Sign-up and sign-in&lt;/strong&gt;, under &lt;strong&gt;Social sign-in&lt;/strong&gt;, click &lt;strong&gt;Add Social Connector&lt;/strong&gt; and select the connectors you configured, then &lt;strong&gt;Save Changes&lt;/strong&gt; and test with &lt;strong&gt;Live preview&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  5. Configure Enterprise SSO
&lt;/h2&gt;

&lt;p&gt;Enterprise Single Sign-On (SSO) enables corporate users to authenticate via their own Identity Provider (IdP), such as Okta or Microsoft Entra ID.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Enterprise SSO&lt;/strong&gt;, click &lt;strong&gt;Add enterprise connector&lt;/strong&gt;, and select &lt;strong&gt;Okta&lt;/strong&gt; (or &lt;strong&gt;SAML&lt;/strong&gt; / &lt;strong&gt;OIDC&lt;/strong&gt; for a generic connector). Use the &lt;strong&gt;Connection guide&lt;/strong&gt; button for provider-specific setup instructions.&lt;/li&gt;
&lt;li&gt;In your Okta dashboard, register a new OIDC application, set the &lt;strong&gt;Login redirect URI&lt;/strong&gt; to the value provided by Logto, and obtain the &lt;strong&gt;Client ID&lt;/strong&gt;, &lt;strong&gt;Client Secret&lt;/strong&gt;, and &lt;strong&gt;Issuer URL&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter these details into the Logto connector configuration and click &lt;strong&gt;Save&lt;/strong&gt;. For SAML connectors, exchange IdP metadata by uploading the metadata file or entering the metadata URL.&lt;/li&gt;
&lt;li&gt;On the &lt;strong&gt;SSO Experience&lt;/strong&gt; tab, add the email domains associated with the organization (for example, &lt;code&gt;example.com&lt;/code&gt;) — users with matching email domains are automatically routed to the enterprise IdP login page, and other sign-in methods are disabled for those domains.&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;JIT provisioning&lt;/strong&gt; to create users automatically on first SSO sign-in, then click &lt;strong&gt;Save changes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  6. Enable Multi-Factor Authentication
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Multi-factor auth&lt;/strong&gt; in the left sidebar.&lt;/li&gt;
&lt;li&gt;Toggle the desired MFA methods to &lt;strong&gt;On&lt;/strong&gt;: &lt;strong&gt;Authenticator app&lt;/strong&gt; (TOTP), &lt;strong&gt;Passkeys&lt;/strong&gt; (WebAuthn/biometric/hardware key), &lt;strong&gt;Backup codes&lt;/strong&gt;, and &lt;strong&gt;SMS&lt;/strong&gt; or &lt;strong&gt;Email verification code&lt;/strong&gt; (requires the corresponding connector).&lt;/li&gt;
&lt;li&gt;Configure the &lt;strong&gt;MFA Policy&lt;/strong&gt; (optional MFA or required on every login), then click &lt;strong&gt;Save changes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  7. Configure RBAC and API Resources
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;API Resources&lt;/strong&gt;, click &lt;strong&gt;Create API resource&lt;/strong&gt;, enter an &lt;strong&gt;API Name&lt;/strong&gt; and a unique &lt;strong&gt;API Identifier&lt;/strong&gt; (for example, &lt;code&gt;https://api.example.com/orders&lt;/code&gt;), and define &lt;strong&gt;Permissions (Scopes)&lt;/strong&gt; such as &lt;code&gt;read:orders&lt;/code&gt; and &lt;code&gt;write:orders&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Roles&lt;/strong&gt;, click &lt;strong&gt;Create role&lt;/strong&gt;, assign the API permissions to the role, and click &lt;strong&gt;Save&lt;/strong&gt;. On the &lt;strong&gt;Assign user&lt;/strong&gt; screen, assign users now or click &lt;strong&gt;Skip&lt;/strong&gt; to assign them later from &lt;strong&gt;User management&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If global roles must appear in access tokens, navigate to &lt;strong&gt;Custom JWT&lt;/strong&gt; and configure an access token script to inject role claims into issued JWTs.&lt;/li&gt;
&lt;li&gt;Open the API resource you created and click &lt;strong&gt;Check guide&lt;/strong&gt; to view framework-specific integration code (Express, Python, Spring Boot, and others) for initializing the Logto SDK, protecting API routes, and validating the access token.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  8. Set Up Organizations for Multi-Tenancy
&lt;/h2&gt;

&lt;p&gt;Organizations offer multi-tenant isolation for B2B applications where users belong to different corporate entities. An organization template is a blueprint defining a consistent set of roles and permissions available to every organization in your Logto tenant.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Organization template&lt;/strong&gt; and create &lt;strong&gt;Organization permissions&lt;/strong&gt; (fine-grained, non-API actions such as &lt;code&gt;read:resource&lt;/code&gt;, &lt;code&gt;edit:resource&lt;/code&gt;, &lt;code&gt;delete:resource&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Create &lt;strong&gt;Organization roles&lt;/strong&gt; (for example, &lt;code&gt;Admin&lt;/code&gt; or &lt;code&gt;Member&lt;/code&gt;) and map the permissions to each role.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Organizations&lt;/strong&gt;, click &lt;strong&gt;Create organization&lt;/strong&gt;, select it, and go to the &lt;strong&gt;Members&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add Members&lt;/strong&gt;, assign an organization role to each member from the dropdown — Logto scopes JWT tokens to include the organization context when organization-scoped tokens are requested.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Check guide&lt;/strong&gt; to view integration tutorials for multi-tenant features and org-scoped tokens.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  9. Register Applications
&lt;/h2&gt;

&lt;p&gt;Register your frontend and backend services as applications within Logto to authenticate users and obtain tokens. Specific steps vary by application type — &lt;strong&gt;Single-Page App&lt;/strong&gt;, &lt;strong&gt;Traditional Web App&lt;/strong&gt;, &lt;strong&gt;Native App&lt;/strong&gt;, or &lt;strong&gt;Machine-to-Machine&lt;/strong&gt; — and the console provides a framework-specific tutorial for each.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Applications&lt;/strong&gt;. If none exist, select a supported framework (React, Next.js, Node.js, and others) and click &lt;strong&gt;Start building&lt;/strong&gt;; otherwise click &lt;strong&gt;Create application&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter your application name, click &lt;strong&gt;Create application&lt;/strong&gt;, and follow the Jumpstart guide to integrate Logto.&lt;/li&gt;
&lt;li&gt;Configure the &lt;strong&gt;Redirect URIs&lt;/strong&gt; and &lt;strong&gt;Post Sign-out Redirect URIs&lt;/strong&gt;, note the &lt;strong&gt;App ID&lt;/strong&gt; and integration endpoint, then click &lt;strong&gt;Save changes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Register at least three application types to cover common architectures: a &lt;strong&gt;Single-Page App (SPA)&lt;/strong&gt; for React/Vue/Angular frontends, a &lt;strong&gt;Traditional Web App&lt;/strong&gt; for server-rendered applications with a backend session, and a &lt;strong&gt;Machine-to-Machine (M2M)&lt;/strong&gt; application for backend services that call the Management API without user interaction. Open the application sign-in URL or use the Logto SDK in a test client to confirm the redirect-based sign-in flow completes successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Configure Webhooks
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Webhooks&lt;/strong&gt;, click &lt;strong&gt;Create webhook&lt;/strong&gt;, enter the backend &lt;strong&gt;Endpoint URL&lt;/strong&gt; and a descriptive name.&lt;/li&gt;
&lt;li&gt;Select the &lt;strong&gt;Webhook events&lt;/strong&gt; to monitor (for example, &lt;code&gt;User.Created&lt;/code&gt;, &lt;code&gt;PostSignIn&lt;/code&gt;, &lt;code&gt;User.Data.Updated&lt;/code&gt;), add optional &lt;strong&gt;Custom headers&lt;/strong&gt; if needed, and click &lt;strong&gt;Create webhook&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Use the &lt;strong&gt;Signing Key&lt;/strong&gt; shown in the webhook settings to cryptographically verify that incoming payloads originated from your Logto instance, then send a test payload to confirm the endpoint receives and processes events.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  11. Verify the Deployment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure&lt;/strong&gt;: Confirm all containers are running with &lt;code&gt;docker ps&lt;/code&gt; — Traefik, Logto, and PostgreSQL should report an &lt;code&gt;Up&lt;/code&gt; status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS&lt;/strong&gt;: Open &lt;code&gt;https://admin.example.com&lt;/code&gt; and &lt;code&gt;https://auth.example.com&lt;/code&gt; in a browser; both should load with valid Let's Encrypt certificates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OIDC discovery&lt;/strong&gt;: Run &lt;code&gt;curl -s https://auth.example.com/oidc/.well-known/openid-configuration&lt;/code&gt; and confirm the &lt;code&gt;issuer&lt;/code&gt; matches &lt;code&gt;https://auth.example.com/oidc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin Console&lt;/strong&gt;: Create the initial admin account and confirm the dashboard loads with all sidebar sections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User registration and sign-in&lt;/strong&gt;: Register a test user through your application and confirm password-based sign-in works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Social login, MFA, RBAC, and organizations&lt;/strong&gt;: Test each configured method end to end — social sign-in through Live preview, MFA enforcement on a test user's next sign-in, an access token containing expected scopes after role assignment, and organization context appearing in the token after adding a user to an organization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit logs&lt;/strong&gt;: Navigate to &lt;strong&gt;Audit logs&lt;/strong&gt; in the Admin Console and confirm sign-in and configuration events are recorded.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  12. Migrate from AWS Cognito to Logto
&lt;/h2&gt;

&lt;p&gt;Migrating from AWS Cognito to Logto involves mapping User Pool resources to Logto tenants, replacing Amplify or Cognito SDK calls with Logto SDKs, and converting Lambda triggers to webhooks or Custom JWT scripts. Because Cognito does not export password hashes or MFA secrets, most migrations follow a bulk user import with mandatory re-authentication strategy. The steps below assume you already completed the deployment, admin console, and application registration sections above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User migration.&lt;/strong&gt; An AWS Cognito User Pool bundles user directories, authentication flows, app clients, and groups into a single managed resource; Logto splits these across users, the sign-in experience, applications, roles, and organizations.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Export&lt;/strong&gt;: retrieve user profiles from Cognito using the AWS CLI (&lt;code&gt;list-users&lt;/code&gt;) or the &lt;a href="https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ListUsers.html" rel="noopener noreferrer"&gt;ListUsers API&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import&lt;/strong&gt;: create users through the &lt;a href="https://openapi.logto.io/" rel="noopener noreferrer"&gt;Logto Management API&lt;/a&gt; (&lt;code&gt;POST /api/users&lt;/code&gt; at &lt;code&gt;https://auth.example.com/api&lt;/code&gt;), authenticated with an access token from a Logto Machine-to-Machine application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passwords&lt;/strong&gt;: Cognito does not expose password hashes — import users with temporary credentials and trigger a password reset on first login.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MFA enrollments&lt;/strong&gt;: TOTP seeds and WebAuthn credentials cannot be exported; users must re-enroll after migration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity mapping&lt;/strong&gt;: store the original Cognito &lt;code&gt;sub&lt;/code&gt; UUID in Logto &lt;code&gt;customData&lt;/code&gt; (for example, &lt;code&gt;legacy_cognito_sub&lt;/code&gt;) to preserve linkages in downstream application databases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Application migration.&lt;/strong&gt; If applications use the AWS Amplify Auth library, replace Amplify Auth APIs with the Logto SDK: &lt;code&gt;signInWithPassword&lt;/code&gt;/embedded forms become redirect-based Universal Login; the User Pool App Client ID becomes the Logto Application ID (&lt;code&gt;client_id&lt;/code&gt;); Callback URLs become Redirect URIs and Post Sign-out Redirect URIs; token refresh moves to the Logto SDK's session management or standard OIDC refresh flows. If applications call the Cognito API directly, replace &lt;code&gt;InitiateAuth&lt;/code&gt;/&lt;code&gt;AdminInitiateAuth&lt;/code&gt; with the OIDC authorization code flow (PKCE) or client credentials flow (M2M), &lt;code&gt;GetUser&lt;/code&gt; with the OIDC UserInfo endpoint or Management API user lookup, and validate access tokens against the Logto JWKS endpoint (&lt;code&gt;https://auth.example.com/oidc/jwks&lt;/code&gt;, issuer &lt;code&gt;https://auth.example.com/oidc&lt;/code&gt;) instead of Cognito JWTs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role and group migration.&lt;/strong&gt; Cognito Groups that gate API access or admin capabilities map to global Logto Roles: create each role (&lt;code&gt;POST /api/roles&lt;/code&gt;), define API resource permissions, and assign users (&lt;code&gt;POST /api/users/{userId}/roles&lt;/code&gt;) — configure a Custom JWT script to inject role claims into access tokens, since global roles aren't included by default. Cognito Groups that represent B2B tenants map to Logto Organizations: create the organization (&lt;code&gt;POST /api/organizations&lt;/code&gt;), define organization roles in the Organization template, and add members (&lt;code&gt;POST /api/organizations/{id}/users&lt;/code&gt;). Update backend route guards to parse the claims your Custom JWT script emits instead of Cognito's &lt;code&gt;cognito:groups&lt;/code&gt; claim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lambda trigger migration.&lt;/strong&gt; Post-confirmation/post-authentication triggers (async) map to Logto Webhooks (&lt;code&gt;User.Created&lt;/code&gt;, &lt;code&gt;PostSignIn&lt;/code&gt;, &lt;code&gt;User.Data.Updated&lt;/code&gt;). Pre-token generation triggers (sync, claims modification) map to Custom JWT access token scripts. Pre-authentication/pre-sign-up triggers (sync, blocking) map to application middleware, API gateway rules, or a custom authentication proxy — Logto webhooks cannot block or reject an in-progress sign-in synchronously. Custom message triggers map to email/SMS connectors or a webhook consumer. Unlike Lambda triggers, which scale per invocation, Custom JWT scripts run on every token issuance and webhook consumers run as always-on services — plan capacity accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data storage considerations.&lt;/strong&gt; Cognito sessions and refresh tokens become invalid at cutover, so force re-authentication and clear local session caches. Applications that store Cognito &lt;code&gt;sub&lt;/code&gt; values rely on the &lt;code&gt;legacy_cognito_sub&lt;/code&gt; mapping in Logto &lt;code&gt;customData&lt;/code&gt; until updated. Replace calls to Cognito IDP endpoints (&lt;code&gt;AdminGetUser&lt;/code&gt;, &lt;code&gt;ListUsers&lt;/code&gt;, &lt;code&gt;AdminUpdateUserAttributes&lt;/code&gt;) with the Logto Management API at &lt;code&gt;https://auth.example.com/api&lt;/code&gt;. Recreate any Cognito SAML/OIDC federation as Logto Enterprise SSO or social connectors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Things to take care of during migration&lt;/strong&gt;: moving embedded Amplify sign-in forms to redirect-based OIDC is the largest application-code change in most migrations; Logto webhooks are asynchronous only, so blocking validation needs middleware or a gateway; Cognito Identity Pools (AWS resource access via IAM) have no Logto equivalent, so provide access through M2M API gateways, role assumption, or application-level credentials; Cognito Advanced Security has no direct Logto OSS equivalent, so implement rate limiting or WAF rules at the reverse proxy; MFA secrets can't be extracted, so users must re-enroll; Cognito's custom claims (&lt;code&gt;cognito:groups&lt;/code&gt;, &lt;code&gt;cognito:username&lt;/code&gt;) differ from Logto's standard OIDC claims (&lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;scope&lt;/code&gt;); Hosted UI CSS customization maps to Logto sign-in experience branding, though complex custom login pages need a custom UI built on the Logto SDK; and Cognito's per-MAU pricing changes to an infrastructure-based cost model, so calculate total cost of ownership before cutover. Run a phased migration: import users first, validate token flows in a staging tenant, then redirect production traffic to Logto and disable the Cognito User Pool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Configure a Custom JWT script to enrich access tokens with the role and organization claims your applications expect.&lt;/li&gt;
&lt;li&gt;Set up organization-scoped applications for each B2B tenant migrating off Cognito Groups.&lt;/li&gt;
&lt;li&gt;Add rate limiting or WAF rules at the reverse proxy layer to replace Cognito Advanced Security.&lt;/li&gt;
&lt;li&gt;Run a staging-tenant migration rehearsal before cutting production traffic over from Cognito.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full guide with additional tips, visit the original article on &lt;strong&gt;&lt;a href="https://docs.vultr.com/how-to-deploy-logto-as-an-aws-cognito-alternative" rel="noopener noreferrer"&gt;Vultr Docs&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>oauth</category>
      <category>docker</category>
      <category>security</category>
    </item>
    <item>
      <title>Deploying Ory Hydra: An Open-Source OAuth 2.0 and OpenID Connect Server</title>
      <dc:creator>Sanskriti Harmukh</dc:creator>
      <pubDate>Wed, 23 Sep 2026 19:20:47 +0000</pubDate>
      <link>https://dev.to/vultr/deploying-ory-hydra-an-open-source-oauth-20-and-openid-connect-server-2j9p</link>
      <guid>https://dev.to/vultr/deploying-ory-hydra-an-open-source-oauth-20-and-openid-connect-server-2j9p</guid>
      <description>&lt;p&gt;&lt;a href="https://www.ory.com/hydra" rel="noopener noreferrer"&gt;Ory Hydra&lt;/a&gt; is an open-source OAuth 2.0 Authorization Server and OpenID Connect (OIDC) provider. Unlike identity platforms that bundle user management, Hydra delegates authentication to a separate login and consent application that you control, and it handles the OAuth 2.0 authorization code flow, client credentials flow, token introspection, and token revocation through an API-first architecture that separates the authorization protocol from identity storage. This guide walks through deploying Ory Hydra on a Linux server using Docker Compose with PostgreSQL, Nginx, and a Java-based login and consent application. By the end, you'll have a working OAuth 2.0/OIDC server with its admin API bound to the loopback interface and a full authorization code flow you can test end to end.&lt;/p&gt;

&lt;p&gt;Before you begin, you need a Linux-based server with at least 2 CPU cores and 4 GB of RAM as a non-root user with sudo privileges, Docker and Docker Compose installed, and a DNS A record pointing to your server's IP address (for example, &lt;code&gt;hydra.example.com&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Set Up the Directory Structure, Configuration, and Environment Variables
&lt;/h2&gt;

&lt;p&gt;Ory Hydra reads its configuration from a YAML file and reads database credentials and secrets from environment variables at runtime. The directory structure separates the Hydra configuration, the Nginx reverse proxy configuration, and the TLS certificate storage into distinct paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the project directory with all required subdirectories:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/ory-hydra/&lt;span class="o"&gt;{&lt;/span&gt;config/nginx,data/&lt;span class="o"&gt;{&lt;/span&gt;postgres,certbot/conf&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;config/&lt;/code&gt; stores the Hydra configuration file and the Nginx server configuration, &lt;code&gt;data/postgres/&lt;/code&gt; persists PostgreSQL database files across container restarts, and &lt;code&gt;data/certbot/conf/&lt;/code&gt; stores the Let's Encrypt TLS certificate files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Navigate to the project directory:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/ory-hydra
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Clone the Java reference login and consent application into the project directory:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git clone https://github.com/ardetrick/ory-hydra-refrence-java.git reference-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have your own application that implements the Hydra login and consent protocol, skip this step and replace &lt;code&gt;./reference-app&lt;/code&gt; in the Docker Compose file with your application's source directory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Generate a system secret for signing tokens and encrypting database records (Hydra requires at least 16 characters). Run this command twice:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the first output as &lt;code&gt;YOUR_SYSTEM_SECRET&lt;/code&gt; and the second as &lt;code&gt;YOUR_PAIRWISE_SALT&lt;/code&gt; (the OIDC pairwise subject identifier salt). The system secret is written into &lt;code&gt;hydra.yml&lt;/code&gt; for reference, but the &lt;code&gt;SECRETS_SYSTEM&lt;/code&gt; environment variable defined in &lt;code&gt;.env&lt;/code&gt; takes precedence at runtime because Docker Compose passes it directly to the Hydra container — replace &lt;code&gt;YOUR_SYSTEM_SECRET&lt;/code&gt; in both files with the same generated value to keep them consistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Create the Hydra configuration file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano config/hydra.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Add the following content, replacing &lt;code&gt;hydra.example.com&lt;/code&gt; with your domain name, &lt;code&gt;YOUR_SYSTEM_SECRET&lt;/code&gt; with the first generated value, and &lt;code&gt;YOUR_PAIRWISE_SALT&lt;/code&gt; with the second:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;serve&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;public&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;base_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://hydra.example.com/&lt;/span&gt;
    &lt;span class="na"&gt;cors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;allowed_origins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https://hydra.example.com&lt;/span&gt;
      &lt;span class="na"&gt;allowed_methods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POST&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PUT&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DELETE&lt;/span&gt;
      &lt;span class="na"&gt;allowed_headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Authorization&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Content-Type&lt;/span&gt;
      &lt;span class="na"&gt;exposed_headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Content-Type&lt;/span&gt;
      &lt;span class="na"&gt;allow_credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;admin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;base_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:4445/&lt;/span&gt;

&lt;span class="na"&gt;urls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;self&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://hydra.example.com/&lt;/span&gt;
  &lt;span class="na"&gt;login&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://hydra.example.com/login&lt;/span&gt;
  &lt;span class="na"&gt;consent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://hydra.example.com/consent&lt;/span&gt;
  &lt;span class="na"&gt;logout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://hydra.example.com/logout&lt;/span&gt;

&lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;YOUR_SYSTEM_SECRET&lt;/span&gt;

&lt;span class="na"&gt;oidc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;subject_identifiers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;supported_types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;public&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pairwise&lt;/span&gt;
    &lt;span class="na"&gt;pairwise&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;salt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_PAIRWISE_SALT&lt;/span&gt;

&lt;span class="na"&gt;strategies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;access_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opaque&lt;/span&gt;

&lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;login_consent_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30m&lt;/span&gt;
  &lt;span class="na"&gt;access_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;
  &lt;span class="na"&gt;refresh_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;720h&lt;/span&gt;
  &lt;span class="na"&gt;id_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;
  &lt;span class="na"&gt;auth_code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;

&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;info&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;text&lt;/span&gt;
  &lt;span class="na"&gt;leak_sensitive_values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;serve.public&lt;/code&gt; sets the public API base URL and restricts CORS to your domain. &lt;code&gt;serve.admin&lt;/code&gt; declares the admin API base URL — the actual network restriction is enforced by the Docker Compose port binding (&lt;code&gt;127.0.0.1:4445:4445&lt;/code&gt;), which limits the admin API to the loopback interface. &lt;code&gt;urls&lt;/code&gt; tells Hydra where to redirect users during login and consent. &lt;code&gt;secrets.system&lt;/code&gt; signs access tokens and encrypts sensitive database records; changing it invalidates all existing tokens. &lt;code&gt;strategies.access_token: opaque&lt;/code&gt; means tokens are random strings validated through the introspection endpoint (the alternative, &lt;code&gt;jwt&lt;/code&gt;, allows stateless validation but cannot be revoked before expiry). &lt;code&gt;ttl&lt;/code&gt; configures token lifetimes, and &lt;code&gt;log&lt;/code&gt; disables sensitive value exposure in log output. The admin API &lt;code&gt;base_url&lt;/code&gt; uses &lt;code&gt;http://127.0.0.1:4445/&lt;/code&gt; intentionally — the admin API accepts and rejects login and consent requests without authentication and must never be exposed through the public reverse proxy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Create the Nginx configuration file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano config/nginx/default.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;8. Add the following content, replacing all instances of &lt;code&gt;hydra.example.com&lt;/code&gt; with your actual domain name:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;server&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;listen&lt;/span&gt; &lt;span class="err"&gt;80&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;    &lt;span class="err"&gt;server_name&lt;/span&gt; &lt;span class="err"&gt;hydra.example.com&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;    &lt;span class="err"&gt;return&lt;/span&gt; &lt;span class="err"&gt;301&lt;/span&gt; &lt;span class="err"&gt;https://$host$request_uri&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="err"&gt;server&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;listen&lt;/span&gt; &lt;span class="err"&gt;443&lt;/span&gt; &lt;span class="err"&gt;ssl&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;    &lt;span class="err"&gt;server_name&lt;/span&gt; &lt;span class="err"&gt;hydra.example.com&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;
    &lt;span class="err"&gt;ssl_certificate&lt;/span&gt; &lt;span class="err"&gt;/etc/letsencrypt/live/hydra.example.com/fullchain.pem&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;    &lt;span class="err"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="err"&gt;/etc/letsencrypt/live/hydra.example.com/privkey.pem&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;
    &lt;span class="err"&gt;resolver&lt;/span&gt; &lt;span class="err"&gt;127.0.0.11&lt;/span&gt; &lt;span class="py"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30s;&lt;/span&gt;

    &lt;span class="c"&gt;# Callback page for the OAuth 2.0 authorization code flow
&lt;/span&gt;    &lt;span class="py"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/callback {&lt;/span&gt;
        &lt;span class="err"&gt;root&lt;/span&gt; &lt;span class="err"&gt;/etc/nginx/html&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;try_files&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="py"&gt;callback.html&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;404;&lt;/span&gt;
    &lt;span class="err"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;# Login and consent app routes
&lt;/span&gt;    &lt;span class="err"&gt;location&lt;/span&gt; &lt;span class="err"&gt;~&lt;/span&gt; &lt;span class="err"&gt;^/(login|consent|logout|demo)&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;set&lt;/span&gt; &lt;span class="err"&gt;$login_consent&lt;/span&gt; &lt;span class="err"&gt;hydra:8080&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;proxy_pass&lt;/span&gt; &lt;span class="err"&gt;http://$login_consent&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;proxy_set_header&lt;/span&gt; &lt;span class="err"&gt;Host&lt;/span&gt; &lt;span class="err"&gt;$host&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;proxy_set_header&lt;/span&gt; &lt;span class="err"&gt;X-Real-IP&lt;/span&gt; &lt;span class="err"&gt;$remote_addr&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;proxy_set_header&lt;/span&gt; &lt;span class="err"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="err"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;proxy_set_header&lt;/span&gt; &lt;span class="err"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="err"&gt;$scheme&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;    &lt;span class="err"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;# Hydra public OAuth 2.0 and OIDC endpoints
&lt;/span&gt;    &lt;span class="err"&gt;location&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;set&lt;/span&gt; &lt;span class="err"&gt;$hydra&lt;/span&gt; &lt;span class="err"&gt;hydra:4444&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;proxy_pass&lt;/span&gt; &lt;span class="err"&gt;http://$hydra&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;proxy_set_header&lt;/span&gt; &lt;span class="err"&gt;Host&lt;/span&gt; &lt;span class="err"&gt;$host&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;proxy_set_header&lt;/span&gt; &lt;span class="err"&gt;X-Real-IP&lt;/span&gt; &lt;span class="err"&gt;$remote_addr&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;proxy_set_header&lt;/span&gt; &lt;span class="err"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="err"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;proxy_set_header&lt;/span&gt; &lt;span class="err"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="err"&gt;$scheme&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;    &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first server block redirects all HTTP traffic to HTTPS. The second listens on &lt;code&gt;443&lt;/code&gt; with TLS and contains three location blocks: &lt;code&gt;/callback&lt;/code&gt; serves the static callback HTML page directly; &lt;code&gt;^/(login|consent|logout|demo)&lt;/code&gt; routes to the login and consent app on port &lt;code&gt;8080&lt;/code&gt;; and &lt;code&gt;/&lt;/code&gt; routes everything else to the Hydra public API on port &lt;code&gt;4444&lt;/code&gt;, covering the authorization endpoint, token endpoint, revocation endpoint, and the &lt;code&gt;/.well-known/openid-configuration&lt;/code&gt; discovery document. Because all services run on the same Docker network, Nginx resolves the &lt;code&gt;hydra&lt;/code&gt; hostname through Docker's internal DNS — the &lt;code&gt;resolver 127.0.0.11 valid=30s&lt;/code&gt; directive is required when using variables in &lt;code&gt;proxy_pass&lt;/code&gt; directives. The &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; header ensures Hydra recognizes that the original client connection uses HTTPS, which is required for secure redirect generation and cookie handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Create the callback page that displays the authorization code after a successful OAuth 2.0 flow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano config/nginx/callback.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;10. Add the following content:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;OAuth2 Callback&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.0/vue.global.prod.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Authorization Failed&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Error:&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt; {{ error }}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Reason:&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt; {{ errorDesc }}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Authorization Successful&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Code:&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;code&amp;gt;&lt;/span&gt;{{ code }}&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;pre&amp;gt;&lt;/span&gt;{{ curlCmd }}&lt;span class="nt"&gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-else&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;No code or error received.&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createApp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Vue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errorDesc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error_description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;errorDesc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;curlCmd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;curl -X POST &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/oauth2/token &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;  -H 'Content-Type: application/x-www-form-urlencoded' &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;  -d 'grant_type=authorization_code' &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;  -d 'code=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;' &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;  -d 'redirect_uri=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/callback' &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;  -d 'client_id=YOUR_CLIENT_ID' &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;  -d 'client_secret=YOUR_CLIENT_SECRET'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The callback page reads the &lt;code&gt;code&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt; query parameters from the URL and renders the result using Vue. On a successful flow, it displays the authorization code and a ready-to-run curl command to exchange it for tokens; on a failed flow, it displays the error and reason returned by Hydra.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;11. Create the environment variables file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;12. Add the following content, replacing &lt;code&gt;EXAMPLE_DB_PASSWORD&lt;/code&gt; with a strong, unique database password and &lt;code&gt;YOUR_SYSTEM_SECRET&lt;/code&gt; with the same value you placed in &lt;code&gt;config/hydra.yml&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;HYDRA_VERSION&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;v26.2.0&lt;/span&gt;
&lt;span class="py"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;hydra&lt;/span&gt;
&lt;span class="py"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;EXAMPLE_DB_PASSWORD&lt;/span&gt;
&lt;span class="py"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;hydradb&lt;/span&gt;
&lt;span class="py"&gt;SECRETS_SYSTEM&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;YOUR_SYSTEM_SECRET&lt;/span&gt;
&lt;span class="py"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;info&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Deploy with Docker Compose
&lt;/h2&gt;

&lt;p&gt;Docker Compose manages all services as a single deployment unit. Certbot runs once as a standalone container to obtain the initial TLS certificate before Nginx starts, which requires port &lt;code&gt;80&lt;/code&gt; to be free at that point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the Docker Compose file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Add the following content:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/postgres:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;hydra-network&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_DB}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.0'&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512M&lt;/span&gt;

  &lt;span class="na"&gt;hydra-migrate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oryd/hydra:${HYDRA_VERSION}&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DSN=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;migrate sql -e --yes&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;hydra-network&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;on-failure&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.50'&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256M&lt;/span&gt;

  &lt;span class="na"&gt;hydra&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oryd/hydra:${HYDRA_VERSION}&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:4445:4445"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DSN=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SECRETS_SYSTEM=${SECRETS_SYSTEM}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;LOG_LEVEL=${LOG_LEVEL}&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;serve all --config /etc/config/hydra/hydra.yml&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config:/etc/config/hydra&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;hydra-migrate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_completed_successfully&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;hydra-network&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.0'&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512M&lt;/span&gt;

  &lt;span class="na"&gt;hydra-login-consent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./reference-app&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile_inline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;FROM gradle:8-jdk21 AS build&lt;/span&gt;
        &lt;span class="s"&gt;WORKDIR /app&lt;/span&gt;
        &lt;span class="s"&gt;COPY . .&lt;/span&gt;
        &lt;span class="s"&gt;ENV GRADLE_OPTS="-Xmx256m -Xms64m -Dfile.encoding=UTF-8"&lt;/span&gt;
        &lt;span class="s"&gt;RUN gradle bootJar --no-daemon&lt;/span&gt;

        &lt;span class="s"&gt;FROM eclipse-temurin:21-jre&lt;/span&gt;
        &lt;span class="s"&gt;WORKDIR /app&lt;/span&gt;
        &lt;span class="s"&gt;COPY --from=build /app/reference-app/build/libs/*.jar app.jar&lt;/span&gt;
        &lt;span class="s"&gt;ENTRYPOINT ["java", "-jar", "-Xmx200m", "-Xms64m", "app.jar"]&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;hydra&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service:hydra"&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.50'&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512M&lt;/span&gt;

  &lt;span class="na"&gt;nginx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/certbot/conf:/etc/letsencrypt:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config/nginx/callback.html:/etc/nginx/html/callback.html:ro&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;hydra&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;hydra-login-consent&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;hydra-network&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.50'&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;128M&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;postgres&lt;/code&gt; stores OAuth 2.0 clients, authorization codes, tokens, and consent records, with a &lt;code&gt;pg_isready&lt;/code&gt; health check gating dependent services. &lt;code&gt;hydra-migrate&lt;/code&gt; runs database migrations once and exits with code &lt;code&gt;0&lt;/code&gt;; &lt;code&gt;hydra&lt;/code&gt; waits for that exit before starting. &lt;code&gt;hydra&lt;/code&gt; runs &lt;code&gt;serve all&lt;/code&gt;, starting the public API on port &lt;code&gt;4444&lt;/code&gt; and the admin API on port &lt;code&gt;4445&lt;/code&gt; — the admin port is bound to &lt;code&gt;127.0.0.1&lt;/code&gt; on the host, restricting it to the loopback interface. The &lt;code&gt;SECRETS_SYSTEM&lt;/code&gt; environment variable overrides the value in &lt;code&gt;hydra.yml&lt;/code&gt; at runtime. &lt;code&gt;hydra-login-consent&lt;/code&gt; builds the Java reference app via a multi-stage Gradle and Eclipse Temurin build and, using &lt;code&gt;network_mode: "service:hydra"&lt;/code&gt;, shares Hydra's network namespace to reach the admin API at &lt;code&gt;127.0.0.1:4445&lt;/code&gt;. &lt;code&gt;nginx&lt;/code&gt; terminates TLS and is the only service that exposes ports to the host. All services connect to a shared &lt;code&gt;hydra-network&lt;/code&gt; bridge network, and each includes a &lt;code&gt;deploy.resources.limits&lt;/code&gt; block capping CPU and memory. The PostgreSQL DSN uses &lt;code&gt;sslmode=disable&lt;/code&gt; because the connection travels over the internal Docker bridge network; if you move PostgreSQL to a separate host, change this to &lt;code&gt;sslmode=require&lt;/code&gt; and configure TLS on the PostgreSQL server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Run Certbot as a Docker container in standalone mode to get a TLS certificate from Let's Encrypt, replacing &lt;code&gt;admin@example.com&lt;/code&gt; and &lt;code&gt;hydra.example.com&lt;/code&gt; with your own values:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 80:80 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    -v ~/ory-hydra/data/certbot/conf:/etc/letsencrypt \
    certbot/certbot certonly --standalone \
    --non-interactive --agree-tos --no-eff-email \
    --email admin@example.com \
    -d hydra.example.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Check that the certificate files exist, replacing &lt;code&gt;hydra.example.com&lt;/code&gt; with your domain name:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo ls&lt;/span&gt; ~/ory-hydra/data/certbot/conf/live/hydra.example.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that the output lists &lt;code&gt;fullchain.pem&lt;/code&gt;, &lt;code&gt;privkey.pem&lt;/code&gt;, &lt;code&gt;chain.pem&lt;/code&gt;, &lt;code&gt;cert.pem&lt;/code&gt;, and &lt;code&gt;README&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Create the certificate renewal script:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano ~/ory-hydra/renew-cert.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Add the following content:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;cd&lt;/span&gt; ~/ory-hydra
docker compose stop nginx
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 80:80 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; ~/ory-hydra/data/certbot/conf:/etc/letsencrypt &lt;span class="se"&gt;\&lt;/span&gt;
    certbot/certbot renew &lt;span class="nt"&gt;--non-interactive&lt;/span&gt;
docker compose start nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and close the file, then make it executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ~/ory-hydra/renew-cert.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. Open the crontab editor:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;crontab &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this is the first time you run &lt;code&gt;crontab -e&lt;/code&gt;, the system prompts you to select an editor from a numbered list. Enter the number corresponding to &lt;code&gt;/bin/nano&lt;/code&gt; or your preferred editor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Add the following line to run the renewal script at 3:00 a.m. on the 1st and 15th of every month:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;0 3 1,15 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /bin/bash ~/ory-hydra/renew-cert.sh &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/ory-hydra/renew-cert.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's Encrypt certificates expire after 90 days, so running the job twice a month keeps the certificate updated. Certbot only renews certificates that expire within 30 days, so running the script twice a month is safe, and a transient failure on the 1st is retried on the 15th.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Start all services in detached mode:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait about 30 seconds for the database to initialize and migrations to complete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. Check the status of all containers:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose ps &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that &lt;code&gt;postgres&lt;/code&gt;, &lt;code&gt;hydra&lt;/code&gt;, &lt;code&gt;hydra-login-consent&lt;/code&gt;, and &lt;code&gt;nginx&lt;/code&gt; all show &lt;code&gt;Up&lt;/code&gt;, and that &lt;code&gt;hydra-migrate&lt;/code&gt; shows &lt;code&gt;Exited (0)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;11. Check the Hydra logs to confirm that the server started without errors:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose logs hydra | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Setting up http server"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that the output contains two lines showing Hydra listening on &lt;code&gt;0.0.0.0:4444&lt;/code&gt; and &lt;code&gt;0.0.0.0:4445&lt;/code&gt;. If you see database connection errors, wait a few seconds and restart the Hydra container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose restart hydra
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Verify the Deployment and Test the Authorization Code Flow
&lt;/h2&gt;

&lt;p&gt;Hydra exposes a public API on port &lt;code&gt;4444&lt;/code&gt; for OAuth 2.0 and OIDC requests, and an admin API on port &lt;code&gt;4445&lt;/code&gt; for client management, accessible only from the server through the loopback port binding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Test the Hydra admin API health endpoint from the server:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:4445/health/alive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful response returns &lt;code&gt;{"status":"ok"}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Test the public API through the Nginx HTTPS reverse proxy, replacing &lt;code&gt;hydra.example.com&lt;/code&gt; with your domain name:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://hydra.example.com/health/alive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Verify that the OpenID Connect discovery document is accessible, replacing &lt;code&gt;hydra.example.com&lt;/code&gt; with your domain name:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://hydra.example.com/.well-known/openid-configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that the response contains a JSON document with the &lt;code&gt;issuer&lt;/code&gt;, &lt;code&gt;authorization_endpoint&lt;/code&gt;, &lt;code&gt;token_endpoint&lt;/code&gt;, and &lt;code&gt;jwks_uri&lt;/code&gt; fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Register a new OAuth 2.0 client using the Hydra CLI inside the running container, replacing &lt;code&gt;hydra.example.com&lt;/code&gt; with your domain name:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;hydra hydra create oauth2-client &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    --endpoint http://127.0.0.1:4445 \
    --format json \
    --grant-type authorization_code,refresh_token \
    --response-type code \
    --scope openid,offline_access,profile \
    --redirect-uri https://hydra.example.com/callback \
    --token-endpoint-auth-method client_secret_post \
    --name "My Test App"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is a JSON object containing the registered client. Copy the &lt;code&gt;client_id&lt;/code&gt; and &lt;code&gt;client_secret&lt;/code&gt; values. The &lt;code&gt;offline_access&lt;/code&gt; scope requests a refresh token alongside the access token; the &lt;code&gt;openid&lt;/code&gt; scope triggers Hydra to issue an ID token identifying the authenticated user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Open a web browser and navigate to the authorization URL below, replacing both instances of &lt;code&gt;hydra.example.com&lt;/code&gt; with your domain name and &lt;code&gt;CLIENT_ID&lt;/code&gt; with the &lt;code&gt;client_id&lt;/code&gt; from the previous step:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;https://hydra.example.com/oauth2/auth?response_type=code&amp;amp;client_id=CLIENT_ID&amp;amp;redirect_uri=https://hydra.example.com/callback&amp;amp;scope=openid+offline_access+profile&amp;amp;state=random-state-value
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hydra checks for an active session and, finding none, redirects the browser to the login page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. On the login page, enter the demo credentials below and click Log in:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;email:&lt;/strong&gt; &lt;code&gt;foo@bar.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;password:&lt;/strong&gt; &lt;code&gt;password&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The login app accepts the challenge through the Hydra admin API and redirects the browser to the consent page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. On the consent page, review the requested scopes and click Allow access.&lt;/strong&gt; The consent app accepts the challenge and Hydra redirects the browser to &lt;code&gt;https://hydra.example.com/callback&lt;/code&gt; with the authorization code in the &lt;code&gt;code&lt;/code&gt; query parameter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. The callback page displays the authorization code and a ready-to-run curl command to exchange it for tokens.&lt;/strong&gt; Replace &lt;code&gt;YOUR_CLIENT_ID&lt;/code&gt; and &lt;code&gt;YOUR_CLIENT_SECRET&lt;/code&gt; in the displayed command with the values from client registration, then run the command from your server terminal. A successful response returns a JSON object containing an &lt;code&gt;access_token&lt;/code&gt;, &lt;code&gt;refresh_token&lt;/code&gt;, and &lt;code&gt;id_token&lt;/code&gt;. Copy the &lt;code&gt;access_token&lt;/code&gt; value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Introspect the access token to confirm that it is active, replacing &lt;code&gt;ACCESS_TOKEN&lt;/code&gt; with the access token from the previous step:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:4445/admin/oauth2/introspect &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    -H 'Content-Type: application/x-www-form-urlencoded' \
    -d 'token=ACCESS_TOKEN'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A valid, active token returns a JSON response with &lt;code&gt;"active": true&lt;/code&gt;, the &lt;code&gt;sub&lt;/code&gt; field containing the authenticated user's identifier, the &lt;code&gt;scope&lt;/code&gt; field listing the granted scopes, and token metadata including &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;iat&lt;/code&gt;, and &lt;code&gt;exp&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Replace the Java reference login and consent app with your own implementation, backed by your real user directory.&lt;/li&gt;
&lt;li&gt;Configure additional OAuth 2.0 clients for each application that needs tokens from this server.&lt;/li&gt;
&lt;li&gt;Switch &lt;code&gt;strategies.access_token&lt;/code&gt; to &lt;code&gt;jwt&lt;/code&gt; if you need stateless validation and can accept the revocation trade-off.&lt;/li&gt;
&lt;li&gt;Set up centralized logging and alerting on the Hydra and Nginx containers for production monitoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full guide with additional tips, visit the original article on &lt;strong&gt;&lt;a href="https://docs.vultr.com/how-to-deploy-ory-hydra-open-source-oauth-20-and-openid-connect-server" rel="noopener noreferrer"&gt;Vultr Docs&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>security</category>
      <category>docker</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Deploying LiteLLM: An Open-Source AI Gateway</title>
      <dc:creator>Sanskriti Harmukh</dc:creator>
      <pubDate>Wed, 23 Sep 2026 19:19:19 +0000</pubDate>
      <link>https://dev.to/vultr/deploying-litellm-an-open-source-ai-gateway-2idp</link>
      <guid>https://dev.to/vultr/deploying-litellm-an-open-source-ai-gateway-2idp</guid>
      <description>&lt;p&gt;&lt;a href="https://litellm.ai" rel="noopener noreferrer"&gt;LiteLLM&lt;/a&gt; is an open-source AI gateway that provides a unified, OpenAI-compatible API for over 100 large language model (LLM) providers, removing the need to integrate with each provider's SDK and authentication scheme. In production, teams use LiteLLM to issue virtual keys with budget limits, track token usage and spend across providers, and configure centralized access control and routing. This guide walks through deploying LiteLLM on a Linux server using Docker Compose with PostgreSQL for persistent storage, Prometheus for metrics collection, and Traefik as the reverse proxy for HTTPS access. By the end, you'll have a fully functional, OpenAI-compatible AI gateway accessible over a custom domain, with virtual key management, spend tracking, and provider routing configured.&lt;/p&gt;

&lt;p&gt;Before you begin, you need a Linux-based server with at least 4 CPU cores and 8 GB of RAM as a non-root user with sudo privileges, Docker and Docker Compose installed, a DNS A record (such as &lt;code&gt;litellm.example.com&lt;/code&gt;) pointing to your server's IP address, and an API key from at least one &lt;a href="https://docs.litellm.ai/docs/providers" rel="noopener noreferrer"&gt;supported LLM provider&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Set Up the Directory Structure, Configuration, and Environment Variables
&lt;/h2&gt;

&lt;p&gt;LiteLLM requires a configuration file to define model providers and routing rules, a Prometheus configuration file for metrics scraping, and environment variables for secrets and database credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the project directory with subdirectories for persistent data:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/litellm/&lt;span class="o"&gt;{&lt;/span&gt;letsencrypt,postgres,prometheus&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;letsencrypt&lt;/code&gt; stores SSL/TLS certificates, &lt;code&gt;postgres&lt;/code&gt; persists PostgreSQL database files, and &lt;code&gt;prometheus&lt;/code&gt; persists Prometheus metrics data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Navigate to the project directory:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/litellm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Set matching ownership on the Prometheus host directory (Prometheus runs as UID 65534 inside the container):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 65534:65534 prometheus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Generate a master key and salt key for LiteLLM. Run this command twice to produce two separate values:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save both values for the &lt;code&gt;LITELLM_MASTER_KEY&lt;/code&gt; and &lt;code&gt;LITELLM_SALT_KEY&lt;/code&gt; fields in the next step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Create the &lt;code&gt;.env&lt;/code&gt; file to store credentials and secrets:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Add the following values:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;DOMAIN&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;litellm.example.com&lt;/span&gt;
&lt;span class="py"&gt;LETSENCRYPT_EMAIL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;admin@example.com&lt;/span&gt;
&lt;span class="py"&gt;LITELLM_MASTER_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sk-YOUR_MASTER_KEY&lt;/span&gt;
&lt;span class="py"&gt;LITELLM_SALT_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sk-YOUR_SALT_KEY&lt;/span&gt;
&lt;span class="py"&gt;LLM_PROVIDER_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;YOUR_LLM_PROVIDER_API_KEY&lt;/span&gt;
&lt;span class="py"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;STRONG_DATABASE_PASSWORD&lt;/span&gt;
&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;postgresql://llmproxy:${POSTGRES_PASSWORD}@db:5432/litellm&lt;/span&gt;
&lt;span class="py"&gt;UI_USERNAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;
&lt;span class="py"&gt;UI_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;YOUR_UI_PASSWORD&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the placeholders with your own values: &lt;code&gt;litellm.example.com&lt;/code&gt; is the domain pointing to your server's IP address; &lt;code&gt;admin@example.com&lt;/code&gt; is the email address for Let's Encrypt notifications; &lt;code&gt;sk-YOUR_MASTER_KEY&lt;/code&gt; is the admin key used to authenticate with the LiteLLM API (replace &lt;code&gt;YOUR_MASTER_KEY&lt;/code&gt; with the first generated value and keep the &lt;code&gt;sk-&lt;/code&gt; prefix); &lt;code&gt;sk-YOUR_SALT_KEY&lt;/code&gt; encrypts provider credentials stored in PostgreSQL and cannot be changed after the first model is added; &lt;code&gt;YOUR_LLM_PROVIDER_API_KEY&lt;/code&gt; is the API key for the provider configured in &lt;code&gt;config.yaml&lt;/code&gt;; &lt;code&gt;STRONG_DATABASE_PASSWORD&lt;/code&gt; is the PostgreSQL password used by both the database container and the connection string; &lt;code&gt;admin&lt;/code&gt;/&lt;code&gt;YOUR_UI_PASSWORD&lt;/code&gt; are the LiteLLM dashboard credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Create the LiteLLM configuration file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;8. Add the following contents:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;model_list&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-model&lt;/span&gt;
    &lt;span class="na"&gt;litellm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;provider/model&lt;/span&gt;
      &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;os.environ/LLM_PROVIDER_API_KEY&lt;/span&gt;

&lt;span class="na"&gt;litellm_settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;callbacks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;prometheus&lt;/span&gt;
  &lt;span class="na"&gt;require_auth_for_metrics_endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;general_settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;master_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;os.environ/LITELLM_MASTER_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;my-model&lt;/code&gt; with a name of your choice to identify this model within LiteLLM, and &lt;code&gt;provider/model&lt;/code&gt; with the LiteLLM provider prefix followed by the model identifier (for example, &lt;code&gt;anthropic/claude-haiku-4-5&lt;/code&gt;). The &lt;code&gt;os.environ/&lt;/code&gt; prefix tells LiteLLM to read the value from an environment variable at runtime rather than hardcoding it. The &lt;code&gt;litellm_settings&lt;/code&gt; block enables Prometheus metrics via the &lt;code&gt;callbacks&lt;/code&gt; field, and &lt;code&gt;require_auth_for_metrics_endpoint: true&lt;/code&gt; restricts the &lt;code&gt;/metrics&lt;/code&gt; endpoint to authenticated requests, so only Prometheus (sending the master key as a Bearer token) can scrape metrics successfully. The &lt;code&gt;general_settings&lt;/code&gt; block defines the master key LiteLLM uses to authenticate admin API requests and virtual key management operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Create the Prometheus configuration file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano prometheus.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;10. Add the following contents:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scrape_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
  &lt;span class="na"&gt;evaluation_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;

&lt;span class="na"&gt;scrape_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;litellm"&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;litellm:4000"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;bearer_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-YOUR_MASTER_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;sk-YOUR_MASTER_KEY&lt;/code&gt; with the value of &lt;code&gt;LITELLM_MASTER_KEY&lt;/code&gt; from your &lt;code&gt;.env&lt;/code&gt; file. Prometheus uses this token to authenticate its scrape requests against the protected &lt;code&gt;/metrics&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Deploy with Docker Compose
&lt;/h2&gt;

&lt;p&gt;The deployment stack runs LiteLLM behind Traefik, which handles TLS termination and automatic certificate provisioning through Let's Encrypt. PostgreSQL provides persistent storage for virtual keys, spend data, and usage logs. Prometheus collects gateway metrics by scraping the LiteLLM &lt;code&gt;/metrics&lt;/code&gt; endpoint every 15 seconds over the internal Docker network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Add your user account to the Docker group:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Apply the new group membership:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;newgrp docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Create the Docker Compose manifest file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano docker-compose.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Add the following contents:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik:v3.6&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker.exposedbydefault=false"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.address=:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.websecure.address=:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.to=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.scheme=https"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.le.acme.httpchallenge=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.le.acme.email=${LETSENCRYPT_EMAIL}"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./letsencrypt:/letsencrypt&lt;/span&gt;

  &lt;span class="na"&gt;litellm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.litellm.ai/berriai/litellm:v1.89.1&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litellm&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config.yaml:/app/config.yaml&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--config=/app/config.yaml"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DATABASE_URL}&lt;/span&gt;
      &lt;span class="na"&gt;STORE_MODEL_IN_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;True"&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4000"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CMD-SHELL&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveliness')"&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;40s&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.litellm.rule=Host(`${DOMAIN}`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.litellm.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.litellm.tls=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.litellm.tls.certresolver=le"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.litellm.loadbalancer.server.port=4000"&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litellm_db&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litellm&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;llmproxy&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./postgres:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432"&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;litellm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;llmproxy"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;

  &lt;span class="na"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prom/prometheus&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litellm_prometheus&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./prometheus:/prometheus&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./prometheus.yml:/etc/prometheus/prometheus.yml&lt;/span&gt;
    &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9090"&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--config.file=/etc/prometheus/prometheus.yml"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--storage.tsdb.path=/prometheus"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--storage.tsdb.retention.time=15d"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration deploys four services behind a single HTTPS endpoint: &lt;code&gt;traefik&lt;/code&gt; acts as the reverse proxy and TLS terminator, redirecting HTTP to HTTPS via Let's Encrypt; &lt;code&gt;litellm&lt;/code&gt; runs the proxy image pinned to a specific release tag, mounts &lt;code&gt;config.yaml&lt;/code&gt;, and waits for PostgreSQL to become healthy before starting; &lt;code&gt;db&lt;/code&gt; runs PostgreSQL 16 as the persistent backend for virtual keys, spend data, and usage logs; &lt;code&gt;prometheus&lt;/code&gt; scrapes LiteLLM metrics every 15 seconds with a 15-day retention window. The named volumes for &lt;code&gt;db&lt;/code&gt; and &lt;code&gt;prometheus&lt;/code&gt; ensure their data survives container removal or recreation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify the Docker Image with Cosign
&lt;/h3&gt;

&lt;p&gt;LiteLLM Docker images are signed with &lt;a href="https://docs.sigstore.dev/quickstart/quickstart-cosign/" rel="noopener noreferrer"&gt;Cosign&lt;/a&gt;. Verifying the image signature before deployment confirms the image has not been tampered with since it was published by the LiteLLM team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Download and install the Cosign binary:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Move the binary into your system path:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo mv &lt;/span&gt;cosign-linux-amd64 /usr/local/bin/cosign
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Make the binary executable:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; +x /usr/local/bin/cosign
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Verify the installation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cosign version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Verify the LiteLLM image signature using the pinned public key.&lt;/strong&gt; This checks the same release tag deployed in the Docker Compose file. LiteLLM publishes signatures for the &lt;code&gt;ghcr.io&lt;/code&gt; registry specifically, so this confirms the &lt;code&gt;v1.89.1&lt;/code&gt; release itself, not the &lt;code&gt;docker.litellm.ai&lt;/code&gt; mirror byte-for-byte.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cosign verify &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
    ghcr.io/berriai/litellm:v1.89.1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful verification outputs a JSON payload confirming the image was signed with the LiteLLM public key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start the Stack
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Start the services:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Verify all containers are running:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that all four containers show an &lt;code&gt;Up&lt;/code&gt; status, with &lt;code&gt;litellm&lt;/code&gt; and &lt;code&gt;db&lt;/code&gt; marked &lt;code&gt;(healthy)&lt;/code&gt; and &lt;code&gt;traefik&lt;/code&gt; listing ports 80 and 443 under &lt;code&gt;PORTS&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Access and Configure LiteLLM
&lt;/h2&gt;

&lt;p&gt;LiteLLM exposes a web-based admin dashboard at the &lt;code&gt;/ui&lt;/code&gt; path of your configured domain, with visibility into model configuration, virtual keys, usage, and spend tracking.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your web browser and navigate to &lt;code&gt;https://litellm.example.com/ui&lt;/code&gt;, replacing &lt;code&gt;litellm.example.com&lt;/code&gt; with your configured domain.&lt;/li&gt;
&lt;li&gt;Log in using the credentials from your &lt;code&gt;.env&lt;/code&gt; file — the value of &lt;code&gt;UI_USERNAME&lt;/code&gt; in the &lt;strong&gt;Username&lt;/strong&gt; field and &lt;code&gt;UI_PASSWORD&lt;/code&gt; in the &lt;strong&gt;Password&lt;/strong&gt; field, then click &lt;strong&gt;Login&lt;/strong&gt;. Verify that the dashboard loads and displays the main navigation panels.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Models + Endpoints&lt;/strong&gt; in the left sidebar to verify the configured model providers. The model defined in &lt;code&gt;config.yaml&lt;/code&gt; appears in the list with its alias and underlying provider model. The left sidebar also provides access to &lt;strong&gt;Virtual Keys&lt;/strong&gt; for managing scoped access credentials, &lt;strong&gt;Usage&lt;/strong&gt; for per-model and per-key request tracking, and &lt;strong&gt;Logs&lt;/strong&gt; for spend and cost breakdowns by model, key, and team.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  4. Test the Gateway and Create a Virtual Key
&lt;/h2&gt;

&lt;p&gt;LiteLLM exposes an OpenAI-compatible API, so any application built for the OpenAI SDK works with LiteLLM by pointing &lt;code&gt;base_url&lt;/code&gt; at the gateway. Virtual keys provide scoped, credential-isolated access without exposing the master key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Install the Python virtual environment package:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; python3-venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Create a Python virtual environment:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv litellm-env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Activate the virtual environment:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;source &lt;/span&gt;litellm-env/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Install the OpenAI Python SDK:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;openai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Export your master key as an environment variable, replacing &lt;code&gt;sk-YOUR_MASTER_KEY&lt;/code&gt; with the &lt;code&gt;LITELLM_MASTER_KEY&lt;/code&gt; value from your &lt;code&gt;.env&lt;/code&gt; file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LITELLM_MASTER_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk-YOUR_MASTER_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Export your gateway domain as an environment variable, replacing &lt;code&gt;litellm.example.com&lt;/code&gt; with your configured domain:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LITELLM_DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"litellm.example.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. Create the test script:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano test_litellm.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;8. Add the following contents, replacing &lt;code&gt;my-model&lt;/code&gt; with the model name you set in &lt;code&gt;config.yaml&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LITELLM_MASTER_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;LITELLM_DOMAIN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is an AI gateway?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;9. Run the script:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 test_litellm.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script returns a response from the configured LLM provider, confirming the gateway is routing requests correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. Create a virtual key using the LiteLLM API, replacing &lt;code&gt;my-model&lt;/code&gt; with the model name you set in &lt;code&gt;config.yaml&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LITELLM_DOMAIN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/key/generate"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;    -H "Authorization: Bearer $&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;LITELLM_MASTER_KEY&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="go"&gt;    -H "Content-Type: application/json" \
    -d '{"key_alias": "test-app-key", "models": ["my-model"], "max_budget": 5.00}' \
&lt;/span&gt;&lt;span class="gp"&gt;    | python3 -c "import sys, json;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;print&lt;span class="o"&gt;(&lt;/span&gt;json.load&lt;span class="o"&gt;(&lt;/span&gt;sys.stdin&lt;span class="o"&gt;)[&lt;/span&gt;&lt;span class="s1"&gt;'key'&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;&lt;span class="s2"&gt;"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command prints the generated virtual key, similar to &lt;code&gt;sk-M9M0_aUDLi7AuhXtOAw6uw&lt;/code&gt;. Each key can have its own model access list, budget limit, and rate limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;11. Export the virtual key returned in the previous step, replacing &lt;code&gt;sk-your-virtual-key&lt;/code&gt; with the full key string printed:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LITELLM_VIRTUAL_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk-your-virtual-key"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;12. Send a request using the virtual key, replacing &lt;code&gt;my-model&lt;/code&gt; with the model name you set in &lt;code&gt;config.yaml&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LITELLM_DOMAIN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/chat/completions"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;    -H "Authorization: Bearer $&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;LITELLM_VIRTUAL_KEY&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="go"&gt;    -H "Content-Type: application/json" \
    -d '{
      "model": "my-model",
      "messages": [{"role": "user", "content": "Hello from a virtual key!"}]
    }'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway authenticates the virtual key, routes the request to the configured provider, and returns the model response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;13. Return to the dashboard at &lt;code&gt;https://litellm.example.com/ui&lt;/code&gt; and click Logs in the left sidebar.&lt;/strong&gt; Each entry shows the model alias, key alias, token count, and estimated cost for requests sent through both the master key and the virtual key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Add more models to &lt;code&gt;config.yaml&lt;/code&gt; and issue per-team or per-application virtual keys with distinct budget limits.&lt;/li&gt;
&lt;li&gt;Build Grafana dashboards on top of the Prometheus metrics for gateway observability.&lt;/li&gt;
&lt;li&gt;Set up alerting on budget thresholds and failed provider requests.&lt;/li&gt;
&lt;li&gt;Rotate the master key and salt key periodically and re-verify Docker image signatures on every upgrade.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full guide with additional tips, visit the original article on &lt;strong&gt;&lt;a href="https://docs.vultr.com/how-to-deploy-litellm-open-source-ai-gateway" rel="noopener noreferrer"&gt;Vultr Docs&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>docker</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Deploying Langflow: An Open-Source Visual Framework for Building AI Applications</title>
      <dc:creator>Sanskriti Harmukh</dc:creator>
      <pubDate>Wed, 23 Sep 2026 19:16:50 +0000</pubDate>
      <link>https://dev.to/vultr/deploying-langflow-an-open-source-visual-framework-for-building-ai-applications-19ol</link>
      <guid>https://dev.to/vultr/deploying-langflow-an-open-source-visual-framework-for-building-ai-applications-19ol</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/langflow-ai/langflow" rel="noopener noreferrer"&gt;Langflow&lt;/a&gt; is an open-source, low-code visual framework for building artificial intelligence (AI) agents, workflows, and retrieval-augmented generation (RAG) applications. Developers use its visual builder to assemble large language model (LLM) pipelines from prebuilt components and test them in an interactive Playground, and finished flows run as API endpoints or Model Context Protocol (MCP) servers without extra boilerplate code. This guide walks through self-hosting a production-ready Langflow instance on a Linux server with Docker Compose, covering PostgreSQL persistence, Traefik reverse proxying with automatic HTTPS certificates, authentication for the visual editor, and validation of the deployment through a RAG chatbot that answers questions from an uploaded document. By the end, you'll have a secured Langflow deployment running behind HTTPS with a working RAG chatbot proving that ingestion, retrieval, and generation all work end to end.&lt;/p&gt;

&lt;p&gt;Before you begin, you need a Linux-based server with at least 2 CPU cores and 4 GB of RAM as a non-root user with sudo privileges, Docker and Docker Compose installed, a domain A record pointing to the server's public IP address (for example, &lt;code&gt;langflow.example.com&lt;/code&gt;), and an API key from a supported LLM provider — this deployment uses OpenAI models for embeddings and chat responses.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Set Up the Project Directory and Environment
&lt;/h2&gt;

&lt;p&gt;Langflow reads its runtime configuration from environment variables, so a dedicated project directory with a &lt;code&gt;.env&lt;/code&gt; file keeps credentials out of the Compose manifest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the project directory and switch into it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; ~/langflow &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/langflow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Generate a Langflow secret key and write it to the environment file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from secrets import token_urlsafe; print(f'LANGFLOW_SECRET_KEY={token_urlsafe(32)}')"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Langflow encrypts stored credentials with this Fernet key. Without an explicit key, Langflow generates a random one at startup and encrypted values become unreadable after a restart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Verify that the file contains the key without displaying its value:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"LANGFLOW_SECRET_KEY"&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Open the &lt;code&gt;.env&lt;/code&gt; file with a text editor such as &lt;code&gt;nano&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Add the following variables below the existing &lt;code&gt;LANGFLOW_SECRET_KEY&lt;/code&gt; line, replacing every placeholder with your own values:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# Domain and certificate settings
&lt;/span&gt;&lt;span class="py"&gt;LANGFLOW_HOSTNAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;langflow.example.com&lt;/span&gt;
&lt;span class="py"&gt;LETSENCRYPT_EMAIL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;admin@example.com&lt;/span&gt;

&lt;span class="c"&gt;# PostgreSQL credentials
&lt;/span&gt;&lt;span class="py"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;langflow&lt;/span&gt;
&lt;span class="py"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;DATABASE_PASSWORD&lt;/span&gt;
&lt;span class="py"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;langflow&lt;/span&gt;

&lt;span class="c"&gt;# Langflow storage paths
&lt;/span&gt;&lt;span class="py"&gt;LANGFLOW_CONFIG_DIR&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/app/langflow&lt;/span&gt;
&lt;span class="py"&gt;LANGFLOW_KNOWLEDGE_BASES_DIR&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/app/langflow/knowledge_bases&lt;/span&gt;

&lt;span class="c"&gt;# Authentication settings
&lt;/span&gt;&lt;span class="py"&gt;LANGFLOW_AUTO_LOGIN&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;False&lt;/span&gt;
&lt;span class="py"&gt;LANGFLOW_SUPERUSER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;administrator&lt;/span&gt;
&lt;span class="py"&gt;LANGFLOW_SUPERUSER_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ADMIN_PASSWORD&lt;/span&gt;
&lt;span class="py"&gt;LANGFLOW_NEW_USER_IS_ACTIVE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;False&lt;/span&gt;
&lt;span class="py"&gt;LANGFLOW_ENABLE_SUPERUSER_CLI&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;False&lt;/span&gt;

&lt;span class="c"&gt;# LLM provider credentials
&lt;/span&gt;&lt;span class="py"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;LANGFLOW_HOSTNAME&lt;/code&gt; and &lt;code&gt;LETSENCRYPT_EMAIL&lt;/code&gt; supply the domain for the Traefik routing rule and the contact address for certificate expiry notices. The &lt;code&gt;POSTGRES_*&lt;/code&gt; variables initialize the database container on first boot and are reused in the Langflow connection string — use only letters and numbers in the password, because symbols require %-encoding and &lt;code&gt;$&lt;/code&gt; conflicts with Compose interpolation. &lt;code&gt;LANGFLOW_CONFIG_DIR&lt;/code&gt; and &lt;code&gt;LANGFLOW_KNOWLEDGE_BASES_DIR&lt;/code&gt; place application data and knowledge base vectors on the same volume-mapped path; without the second variable, Langflow writes knowledge bases outside that volume and a container replacement deletes your vector data. &lt;code&gt;LANGFLOW_AUTO_LOGIN=False&lt;/code&gt; disables anonymous access, &lt;code&gt;LANGFLOW_SUPERUSER&lt;/code&gt;/&lt;code&gt;LANGFLOW_SUPERUSER_PASSWORD&lt;/code&gt; define the administrator account Langflow creates at startup, &lt;code&gt;LANGFLOW_NEW_USER_IS_ACTIVE=False&lt;/code&gt; keeps new accounts inactive until approved, and &lt;code&gt;LANGFLOW_ENABLE_SUPERUSER_CLI=False&lt;/code&gt; blocks superuser creation from the command line. &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; supplies the LLM provider credential, which Langflow stores as an encrypted global variable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Restrict the environment file so only its owner can read or modify it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Deploy with Docker Compose
&lt;/h2&gt;

&lt;p&gt;The stack runs three services. Traefik terminates HTTPS, Langflow serves the application on internal port &lt;code&gt;7860&lt;/code&gt;, and PostgreSQL stores flows, users, and settings. Langflow joins the &lt;code&gt;proxy&lt;/code&gt; network with Traefik and the &lt;code&gt;internal&lt;/code&gt; network with PostgreSQL, so the database stays unreachable from outside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the &lt;code&gt;docker-compose.yml&lt;/code&gt; file in the project directory:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Add the following service definitions to the file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik:v3.7&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--providers.docker=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--providers.docker.exposedbydefault=false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--providers.docker.network=proxy&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--entryPoints.web.address=:80&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--entryPoints.websecure.address=:443&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--entryPoints.websecure.http.tls=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--entryPoints.web.http.redirections.entryPoint.to=websecure&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--entryPoints.web.http.redirections.entryPoint.scheme=https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--certificatesresolvers.le.acme.email=${LETSENCRYPT_EMAIL}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--certificatesresolvers.le.acme.httpchallenge.entrypoint=web&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./letsencrypt:/letsencrypt&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;proxy&lt;/span&gt;

  &lt;span class="na"&gt;langflow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;langflowai/langflow:1.11.3&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;LANGFLOW_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;LANGFLOW_CONFIG_DIR=${LANGFLOW_CONFIG_DIR}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;LANGFLOW_KNOWLEDGE_BASES_DIR=${LANGFLOW_KNOWLEDGE_BASES_DIR}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;LANGFLOW_AUTO_LOGIN=${LANGFLOW_AUTO_LOGIN}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;LANGFLOW_SUPERUSER=${LANGFLOW_SUPERUSER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;LANGFLOW_SUPERUSER_PASSWORD=${LANGFLOW_SUPERUSER_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;LANGFLOW_SECRET_KEY=${LANGFLOW_SECRET_KEY}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;LANGFLOW_NEW_USER_IS_ACTIVE=${LANGFLOW_NEW_USER_IS_ACTIVE}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;LANGFLOW_ENABLE_SUPERUSER_CLI=${LANGFLOW_ENABLE_SUPERUSER_CLI}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;OPENAI_API_KEY=${OPENAI_API_KEY}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;langflow-data:/app/langflow&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;proxy&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.langflow.rule=Host(`${LANGFLOW_HOSTNAME}`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.langflow.entrypoints=websecure&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.langflow.tls.certresolver=le&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.langflow.loadbalancer.server.port=7860&lt;/span&gt;

  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-trixie&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=${POSTGRES_DB}&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_DB}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;langflow-postgres:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proxy&lt;/span&gt;
  &lt;span class="na"&gt;internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;langflow-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;langflow-postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;traefik&lt;/code&gt; service publishes ports &lt;code&gt;80&lt;/code&gt; and &lt;code&gt;443&lt;/code&gt;, discovers only explicitly labeled containers through the read-only Docker socket, and registers a certificate resolver named &lt;code&gt;le&lt;/code&gt; that completes the ACME challenge on port &lt;code&gt;80&lt;/code&gt; and redirects plain HTTP to HTTPS. The &lt;code&gt;langflow&lt;/code&gt; service pins the &lt;code&gt;langflowai/langflow:1.11.3&lt;/code&gt; image; the Traefik labels route your domain to port &lt;code&gt;7860&lt;/code&gt; inside the container, and the &lt;code&gt;langflow-data&lt;/code&gt; volume persists &lt;code&gt;LANGFLOW_CONFIG_DIR&lt;/code&gt; across restarts. The &lt;code&gt;postgres&lt;/code&gt; service pins &lt;code&gt;postgres:16-trixie&lt;/code&gt;, joins only the &lt;code&gt;internal&lt;/code&gt; network, and its &lt;code&gt;pg_isready&lt;/code&gt; health check gates the Langflow start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Start the stack in detached mode:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Verify that all containers are running:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output displays three running containers, with Traefik listening on ports 80 and 443 and PostgreSQL reporting a healthy status.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Check the Langflow logs to verify that the application started:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; langflow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first start takes a few minutes because Langflow runs its database migrations against PostgreSQL. The log stream ends with a startup banner when the application is ready.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open Langflow → http://localhost:7860
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press Ctrl+C to stop following the logs. The &lt;code&gt;localhost&lt;/code&gt; address applies inside the container only, and Traefik forwards your domain traffic to the same listener.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Access and Configure Langflow
&lt;/h2&gt;

&lt;p&gt;The stack now runs behind HTTPS, so the remaining configuration happens in the browser.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a web browser and visit your Langflow domain, such as &lt;code&gt;https://langflow.example.com&lt;/code&gt;. Traefik requests a Let's Encrypt certificate after the stack starts — if the browser shows a certificate warning, wait a minute and reload. Because automatic login is off, Langflow redirects you to the &lt;code&gt;/login&lt;/code&gt; page.&lt;/li&gt;
&lt;li&gt;Log in with the values you set for &lt;code&gt;LANGFLOW_SUPERUSER&lt;/code&gt; and &lt;code&gt;LANGFLOW_SUPERUSER_PASSWORD&lt;/code&gt;. The Langflow &lt;strong&gt;Projects&lt;/strong&gt; page opens.&lt;/li&gt;
&lt;li&gt;Verify the model provider: click your profile icon in the header, select &lt;strong&gt;Settings&lt;/strong&gt;, then &lt;strong&gt;Model Providers&lt;/strong&gt;. &lt;strong&gt;OpenAI&lt;/strong&gt; appears as configured because Langflow detects the &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; variable at startup. Enable the models you plan to use under &lt;strong&gt;Language Models&lt;/strong&gt; and &lt;strong&gt;Embedding Models&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Verify the stored credential: in &lt;strong&gt;Settings&lt;/strong&gt;, click &lt;strong&gt;Global Variables&lt;/strong&gt;. A variable named &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; appears with the &lt;strong&gt;Credential&lt;/strong&gt; type, which masks its value in the visual editor.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  4. Build a RAG Chatbot to Validate the Deployment
&lt;/h2&gt;

&lt;p&gt;A RAG chatbot answers questions from your own documents instead of relying only on the model's training data. Langflow ships a &lt;strong&gt;Vector Store RAG&lt;/strong&gt; template that pairs a retrieval flow with a knowledge base, which chunks a document, embeds it, and stores the vectors locally. A grounded answer in the Playground proves that ingestion, retrieval, and generation all work on the deployed stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a Knowledge Base from a Sample Document
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;On the &lt;strong&gt;Projects&lt;/strong&gt; page, click &lt;strong&gt;Knowledge&lt;/strong&gt; below the list of projects, then click &lt;strong&gt;Add Knowledge&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Create Knowledge Base&lt;/strong&gt; pane, enter a name such as &lt;code&gt;langflow_demo&lt;/code&gt;, select an OpenAI embedding model, and keep &lt;strong&gt;Chroma Local&lt;/strong&gt; as the &lt;strong&gt;DB Provider&lt;/strong&gt; — it stores vectors on the server, so the knowledge base requires no external database account.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add Files&lt;/strong&gt; and select a document from your local machine, such as a product manual or a policy document.&lt;/li&gt;
&lt;li&gt;Keep the default values for &lt;strong&gt;Chunk Size&lt;/strong&gt;, &lt;strong&gt;Chunk Overlap&lt;/strong&gt;, and &lt;strong&gt;Separator&lt;/strong&gt;, then click &lt;strong&gt;Next Step&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Review the sample chunk in the &lt;strong&gt;Review &amp;amp; Build&lt;/strong&gt; pane, then click &lt;strong&gt;Create&lt;/strong&gt;. Langflow splits the document into chunks, converts each chunk into a vector, and indexes the results.&lt;/li&gt;
&lt;li&gt;Wait until the knowledge base &lt;strong&gt;Status&lt;/strong&gt; changes to &lt;strong&gt;Ready&lt;/strong&gt;, which means the ingestion pipeline completed without errors.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Create the Flow from the Vector Store RAG Template
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;On the &lt;strong&gt;Projects&lt;/strong&gt; page, click &lt;strong&gt;New Flow&lt;/strong&gt;, then select the &lt;strong&gt;Vector Store RAG&lt;/strong&gt; template.&lt;/li&gt;
&lt;li&gt;Review how the components connect: &lt;strong&gt;Chat Input&lt;/strong&gt; sends each question to &lt;strong&gt;Knowledge&lt;/strong&gt; as the &lt;strong&gt;Search Query&lt;/strong&gt; and to &lt;strong&gt;Prompt&lt;/strong&gt; as the &lt;code&gt;{question}&lt;/code&gt; variable. &lt;strong&gt;Knowledge&lt;/strong&gt; returns matching chunks, &lt;strong&gt;Parser&lt;/strong&gt; extracts their text, and &lt;strong&gt;Prompt&lt;/strong&gt; inserts that text as &lt;code&gt;{context}&lt;/code&gt;. &lt;strong&gt;Agent&lt;/strong&gt; generates the answer, and &lt;strong&gt;Chat Output&lt;/strong&gt; returns it.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Knowledge&lt;/strong&gt; component, keep &lt;strong&gt;Retrieve&lt;/strong&gt; as the &lt;strong&gt;Mode&lt;/strong&gt;, and select your &lt;code&gt;langflow_demo&lt;/code&gt; knowledge base. Retrieval reuses the embedding model from ingestion, so query vectors and stored vectors stay comparable.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Agent&lt;/strong&gt; component, select an OpenAI chat model in the &lt;strong&gt;Language Model&lt;/strong&gt; field. The template preloads &lt;strong&gt;Agent Instructions&lt;/strong&gt; with a retrieval-focused system prompt.&lt;/li&gt;
&lt;li&gt;Review the &lt;strong&gt;Prompt&lt;/strong&gt; component. Adjust the wording if you want a different tone, but keep the &lt;code&gt;{context}&lt;/code&gt; and &lt;code&gt;{question}&lt;/code&gt; placeholders intact.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Test the Chatbot in the Playground
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Playground&lt;/strong&gt;. A chat panel opens.&lt;/li&gt;
&lt;li&gt;Type a question that only your uploaded document can answer, then press Enter.&lt;/li&gt;
&lt;li&gt;Read the response — the flow runs a semantic search against the knowledge base, pulls the most similar chunks, and instructs the language model to answer from that context.&lt;/li&gt;
&lt;li&gt;Ask a follow-up question about a different part of the document to verify that retrieval covers the full file rather than a single chunk.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Add more LLM providers (Anthropic, Gemini, and others) as global variables and swap models per flow.&lt;/li&gt;
&lt;li&gt;Explore other Langflow templates for agents and multi-step pipelines.&lt;/li&gt;
&lt;li&gt;Publish a flow as an API endpoint or MCP server to power a downstream application.&lt;/li&gt;
&lt;li&gt;Set up scheduled PostgreSQL backups to protect your flow and knowledge base data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full guide with additional tips, visit the original article on &lt;strong&gt;&lt;a href="https://docs.vultr.com/how-to-deploy-langflow-open-source-visual-framework-for-building-ai-applications" rel="noopener noreferrer"&gt;Vultr Docs&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>langflow</category>
      <category>docker</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Deploying Metabase - Open-Source Business Intelligence Tool</title>
      <dc:creator>Sanskriti Harmukh</dc:creator>
      <pubDate>Wed, 23 Sep 2026 19:12:08 +0000</pubDate>
      <link>https://dev.to/vultr/deploying-metabase-open-source-business-intelligence-tool-2ell</link>
      <guid>https://dev.to/vultr/deploying-metabase-open-source-business-intelligence-tool-2ell</guid>
      <description>&lt;p&gt;Metabase is an open-source business intelligence (BI) tool that lets you explore data, build dashboards, and share reports from a web browser. Teams often self-host it because it connects to common databases such as PostgreSQL, MySQL, MariaDB, and SQL Server. This guide deploys Metabase on a Linux server using Docker Compose with a PostgreSQL backend and Traefik providing automatic HTTPS, covering directory and environment setup, deployment, and the initial administrator setup through the web interface. By the end, you'll have Metabase running securely at your domain, ready to connect data sources and build dashboards.&lt;/p&gt;




&lt;h2&gt;
  
  
  Set Up the Project Directory and Environment Variables
&lt;/h2&gt;

&lt;p&gt;Metabase stores its application data, such as users, dashboards, and saved questions, in PostgreSQL. This section creates the project directory and the environment file that the Compose stack shares.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the project directory and move into it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; ~/metabase
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/metabase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Generate a random key that Metabase uses to encrypt stored data-source credentials:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the output for use in the environment file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Create the environment file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following configuration. Replace &lt;code&gt;YOUR_DB_PASSWORD&lt;/code&gt; with a strong password, &lt;code&gt;YOUR_ENCRYPTION_KEY&lt;/code&gt; with the output from the previous &lt;code&gt;openssl&lt;/code&gt; command, &lt;code&gt;metabase.example.com&lt;/code&gt; with your domain name, and &lt;code&gt;ADMIN_EMAIL&lt;/code&gt; with a deliverable email address for Let's Encrypt renewal notices.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;metabaseappdb&lt;/span&gt;
&lt;span class="py"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;metabase&lt;/span&gt;
&lt;span class="py"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;YOUR_DB_PASSWORD&lt;/span&gt;
&lt;span class="py"&gt;MB_ENCRYPTION_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;YOUR_ENCRYPTION_KEY&lt;/span&gt;
&lt;span class="py"&gt;MB_SITE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://metabase.example.com&lt;/span&gt;
&lt;span class="py"&gt;DOMAIN&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;metabase.example.com&lt;/span&gt;
&lt;span class="py"&gt;ACME_EMAIL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ADMIN_EMAIL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and close the file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deploy with Docker Compose
&lt;/h2&gt;

&lt;p&gt;The deployment stack consists of Traefik for reverse proxy and certificate management, plus the Metabase and PostgreSQL containers on a shared bridge network. This configuration uses the &lt;a href="https://hub.docker.com/r/metabase/metabase" rel="noopener noreferrer"&gt;official Metabase container image&lt;/a&gt; version &lt;code&gt;v0.50.21&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the Docker Compose manifest:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metabase-postgres&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres-data:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_DB}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;mem_limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1g&lt;/span&gt;
    &lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json-file"&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;max-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10m"&lt;/span&gt;
        &lt;span class="na"&gt;max-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;metabase-network&lt;/span&gt;

  &lt;span class="na"&gt;metabase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metabase/metabase:v0.50.21&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metabase&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MB_DB_TYPE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;MB_DB_DBNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="na"&gt;MB_DB_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
      &lt;span class="na"&gt;MB_DB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;MB_DB_PASS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;MB_DB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;MB_ENCRYPTION_SECRET_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MB_ENCRYPTION_SECRET_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;MB_SITE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MB_SITE_URL}&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--fail"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000/api/health"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;120s&lt;/span&gt;
    &lt;span class="na"&gt;mem_limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2g&lt;/span&gt;
    &lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json-file"&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;max-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10m"&lt;/span&gt;
        &lt;span class="na"&gt;max-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;metabase-network&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.metabase.rule=Host(`${DOMAIN}`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.metabase.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.metabase.tls.certresolver=letsencrypt"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.metabase.loadbalancer.server.port=3000"&lt;/span&gt;

  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik:v3.7.10&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker.exposedbydefault=false"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.address=:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.to=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.scheme=https"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.websecure.address=:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.letsencrypt.acme.tlschallenge=true"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./letsencrypt:/letsencrypt&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;metabase-network&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Save and close the file. &lt;code&gt;postgres&lt;/code&gt; runs PostgreSQL 16 as the database storing Metabase's users, dashboards, and saved questions. &lt;code&gt;metabase&lt;/code&gt; runs the application server, starting only after PostgreSQL reports healthy, with Traefik labels registering it for automatic HTTPS on the configured &lt;code&gt;DOMAIN&lt;/code&gt;. &lt;code&gt;traefik&lt;/code&gt; handles reverse proxying and TLS termination on ports 80 and 443. &lt;code&gt;mem_limit&lt;/code&gt; and &lt;code&gt;logging&lt;/code&gt; cap each container's memory and rotate its logs, preventing either from exhausting the server's resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Launch the containers:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Verify that the services are running:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose ps &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output displays all three containers in an &lt;code&gt;Up&lt;/code&gt; state, with a healthy status shown for &lt;code&gt;metabase-postgres&lt;/code&gt; and &lt;code&gt;metabase&lt;/code&gt; once their health checks pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Query the Metabase health endpoint through your domain:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl https://metabase.example.com/api/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your output should be similar to the one below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. View the service logs to confirm Metabase loaded the configuration successfully:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; metabase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output displays PostgreSQL accepting connections and Metabase completing its startup migrations, which take one to two minutes. Press Ctrl+C to stop following the logs once the service reports that it has started.&lt;/p&gt;




&lt;h2&gt;
  
  
  Access Metabase and Complete the Initial Setup
&lt;/h2&gt;

&lt;p&gt;Open Metabase in a browser and complete the first-run wizard.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your domain over HTTPS in a browser. Replace &lt;code&gt;metabase.example.com&lt;/code&gt; with your domain:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   https://metabase.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Select your preferred language.&lt;/li&gt;
&lt;li&gt;Create the initial administrator account by entering your name, email address, and password.&lt;/li&gt;
&lt;li&gt;When prompted to add your first data source, configure one now or skip and add it later.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Setup is now complete, and Metabase is ready to use. Any databases you connect through the interface stay separate from the PostgreSQL container, which stores only Metabase's own metadata.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;Metabase is live with Traefik-managed HTTPS and a PostgreSQL-backed metadata store. From here you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connect your production databases and start building dashboards and saved questions&lt;/li&gt;
&lt;li&gt;Configure user groups and permissions to control who can see which data&lt;/li&gt;
&lt;li&gt;Set up scheduled email or Slack reports (pulses) so dashboards reach stakeholders automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full guide with additional tips, visit the original article on &lt;strong&gt;&lt;a href="https://docs.vultr.com/how-to-deploy-metabase-open-source-business-intelligence-tool" rel="noopener noreferrer"&gt;Vultr Docs&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>docker</category>
      <category>devops</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Installing Composer with PHP</title>
      <dc:creator>Sanskriti Harmukh</dc:creator>
      <pubDate>Wed, 16 Sep 2026 17:13:04 +0000</pubDate>
      <link>https://dev.to/vultr/installing-composer-with-php-5744</link>
      <guid>https://dev.to/vultr/installing-composer-with-php-5744</guid>
      <description>&lt;p&gt;Composer is the standard tool for managing PHP dependencies. It automates library installation, enforces version constraints, and helps organize autoloading, making it an essential part of modern PHP development. Whether you're building with frameworks like Laravel, Symfony, or Slim, Composer ensures consistency across environments and simplifies dependency resolution. This guide covers installing Composer with a specific PHP version on a Linux system — useful when working across multiple PHP projects or servers that require different runtime versions. By the end, you'll have Composer installed safely, bound to a specific PHP version such as PHP 8.4, and a reusable alias set up for scripting or interactive use.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Access to a Linux instance.&lt;/li&gt;
&lt;li&gt;SSH access to the server.&lt;/li&gt;
&lt;li&gt;A non-root user with &lt;code&gt;sudo&lt;/code&gt; privileges.&lt;/li&gt;
&lt;li&gt;The server's package index already updated.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Install PHP
&lt;/h2&gt;

&lt;p&gt;Install PHP, the command-line interface (CLI), and required tools such as &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;unzip&lt;/code&gt; using your distribution's package manager.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install PHP on Ubuntu and Debian
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Install the default PHP version and required utilities on APT-based systems:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; php php-cli curl unzip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To install a specific PHP version (such as 8.3 or 8.4), add a third-party repository — the Ondřej Surý PPA on Ubuntu, or the SURY repository on Debian. These repositories provide maintained versions of PHP not available in the default APT sources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install PHP on Rocky Linux and AlmaLinux
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Install PHP and required tools on DNF-based distributions:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; php php-cli curl unzip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To install a specific PHP version, enable the EPEL and Remi repositories. These repositories offer multiple PHP versions for RHEL-based systems including Rocky Linux and AlmaLinux.&lt;/p&gt;




&lt;h2&gt;
  
  
  Install Composer Using a Specific PHP Version
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This guide uses PHP 8.4 as the example version. On DNF-based distributions (like Rocky Linux or AlmaLinux), the system uses &lt;code&gt;php&lt;/code&gt; as the command unless &lt;code&gt;update-alternatives&lt;/code&gt; is configured. If &lt;code&gt;php8.4&lt;/code&gt; doesn't work, use &lt;code&gt;php&lt;/code&gt; instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;1. Download the Composer installer using PHP 8.4:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;php8.4 &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"copy('https://getcomposer.org/installer', 'composer-setup.php');"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Get the installer signature.&lt;/strong&gt; This ensures the installer is authentic and has not been tampered with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://composer.github.io/installer.sig
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the hash from the output. You'll need it for verification in the next step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Verify the installer.&lt;/strong&gt; Replace &lt;code&gt;your_hash_here&lt;/code&gt; with the hash you copied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;php8.4 &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"if (hash_file('sha384', 'composer-setup.php') === 'your_hash_here') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Run the Composer installer using PHP 8.4 and install it system-wide:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;php8.4 composer-setup.php &lt;span class="nt"&gt;--install-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/bin &lt;span class="nt"&gt;--filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;composer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Clean up the installer file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;rm &lt;/span&gt;composer-setup.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Verify the Composer installation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;composer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ______
   / ____/___  ____ ___  ____  ____  ________  _____
  / /    / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
 / /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
 \____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                     /_/
Composer version 2.8.8 2025-04-04 16:56:46
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Run Composer with a Specific PHP Version
&lt;/h2&gt;

&lt;p&gt;To run Composer with a specific PHP version (like PHP 8.4), configure your environment as follows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Confirm the path of the PHP 8.4 binary:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;which php8.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/bin/php8.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Run Composer using the PHP 8.4 binary:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;php8.4 /usr/local/bin/composer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Create an alias so &lt;code&gt;composer&lt;/code&gt; always runs with PHP 8.4 (optional):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'alias composer="php8.4 /usr/local/bin/composer"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For portability in scripts or cron jobs, consider using a shebang like &lt;code&gt;#!/usr/bin/env php&lt;/code&gt; instead of hardcoding a version path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Reload your shell configuration to apply the alias:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're using Zsh, replace &lt;code&gt;~/.bashrc&lt;/code&gt; with &lt;code&gt;~/.zshrc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Verify that Composer now runs with PHP 8.4:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;composer &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Composer version 2.8.8 2025-04-04 16:56:46
PHP version 8.4.0 (/usr/bin/php8.4)
Run the "diagnose" command to get more detailed diagnostics output.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For managing multiple Composer versions or isolating global dependencies per PHP version, consider tools like &lt;code&gt;cgr&lt;/code&gt;, &lt;code&gt;composer-bin&lt;/code&gt;, or &lt;code&gt;asdf&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Composer Functionality (Optional)
&lt;/h3&gt;

&lt;p&gt;To confirm that Composer is functional and can generate project scaffolding:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Run &lt;code&gt;composer init&lt;/code&gt; in non-interactive mode to generate a minimal &lt;code&gt;composer.json&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;composer init &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt;/app &lt;span class="nt"&gt;--description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Test app"&lt;/span&gt; &lt;span class="nt"&gt;--author&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Your Name"&lt;/span&gt; &lt;span class="nt"&gt;--require&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;php:^8.4 &lt;span class="nt"&gt;--no-interaction&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. View the generated file to confirm success:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;composer.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test/app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Test app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"php"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^8.4"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can now run &lt;code&gt;composer install&lt;/code&gt; or add additional dependencies to begin building your PHP project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;You now have Composer installed on your Linux system, bound to a specific PHP version such as PHP 8.4, with a reusable alias for convenience. From here, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;composer require&lt;/code&gt; to add your first dependencies to a real project&lt;/li&gt;
&lt;li&gt;Set up global tool isolation or multi-version workflows with &lt;code&gt;cgr&lt;/code&gt;, &lt;code&gt;composer-bin&lt;/code&gt;, or &lt;a href="https://asdf-vm.com/" rel="noopener noreferrer"&gt;asdf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Add Composer's &lt;code&gt;vendor/&lt;/code&gt; directory to your &lt;code&gt;.gitignore&lt;/code&gt; and commit &lt;code&gt;composer.lock&lt;/code&gt; for reproducible installs&lt;/li&gt;
&lt;li&gt;Read the &lt;a href="https://getcomposer.org/doc/" rel="noopener noreferrer"&gt;official Composer documentation&lt;/a&gt; to learn more about autoloading and dependency constraints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full guide with additional tips, visit the original article on &lt;strong&gt;&lt;a href="https://docs.vultr.com/how-to-install-composer-with-php" rel="noopener noreferrer"&gt;Vultr Docs&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>php</category>
      <category>composer</category>
      <category>linux</category>
      <category>devops</category>
    </item>
    <item>
      <title>Installing Ruby on Ubuntu 24.04</title>
      <dc:creator>Sanskriti Harmukh</dc:creator>
      <pubDate>Wed, 16 Sep 2026 17:12:37 +0000</pubDate>
      <link>https://dev.to/vultr/installing-ruby-on-ubuntu-2404-nnm</link>
      <guid>https://dev.to/vultr/installing-ruby-on-ubuntu-2404-nnm</guid>
      <description>&lt;p&gt;Ruby is a dynamic, object-oriented programming language perfect for creating basic scripts to complex web applications. It offers a modern syntax for productivity, runs with an interpreter instead of a compiler, and provides a wide range of frameworks for various projects. This guide covers installing Ruby on Ubuntu 24.04 using rbenv, RVM, and APT. By the end, you'll have a working Ruby installation, with the option to manage and switch between multiple Ruby versions on the same server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Install Ruby Using rbenv
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;rbenv&lt;/code&gt; is a Ruby version manager that lets you install multiple versions of Ruby and switch between them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Install all required dependencies for the &lt;code&gt;ruby-build&lt;/code&gt; plugin to download and compile Ruby:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;autoconf patch build-essential rustc libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libgmp-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev libdb-dev libtool uuid-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Install the latest rbenv installation script:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://github.com/rbenv/rbenv-installer/raw/HEAD/bin/rbenv-installer | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Add the rbenv path to the &lt;code&gt;.bashrc&lt;/code&gt; file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'export PATH="$HOME/.rbenv/bin:$PATH"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Reload the &lt;code&gt;.bashrc&lt;/code&gt; shell configuration to apply the path changes in your active session:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. View the installed rbenv version:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rbenv &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your output should be similar to the one below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;rbenv 1.3.0-9-gefeab7f
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. List all available Ruby versions you can install:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rbenv &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the target Ruby version in your output similar to the one below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3.1.6
3.2.6
3.3.6
jruby-9.4.9.0
mruby-3.3.0
picoruby-3.0.0
truffleruby-24.1.1
truffleruby+graalvm-24.1.1

Only latest stable releases for each Ruby implementation are shown.
Use `rbenv install --list-all' to show all local versions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. Install your target Ruby version.&lt;/strong&gt; For example, &lt;code&gt;3.3.6&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rbenv &lt;span class="nb"&gt;install &lt;/span&gt;3.3.6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installation process may take &lt;code&gt;3-5&lt;/code&gt; minutes to complete depending on the available server resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Set the installed Ruby version as the default if the installation is successful:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rbenv global 3.3.6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;9. View the installed Ruby version:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ruby &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your output should be similar to the one below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;ruby 3.3.6 (2024-11-05 revision 75015d4c1f) [x86_64-linux]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Install Ruby Using RVM
&lt;/h2&gt;

&lt;p&gt;Ruby Version Manager (RVM), also known as Ruby environment manager, is a command-line tool used to install, manage, and work with multiple Ruby environments on a server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Install the required GPG keys for RVM:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gpg &lt;span class="nt"&gt;--keyserver&lt;/span&gt; keyserver.ubuntu.com &lt;span class="nt"&gt;--recv-keys&lt;/span&gt; 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Install RVM:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://get.rvm.io | bash &lt;span class="nt"&gt;-s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Load RVM into your shell environment:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.rvm/scripts/rvm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. List and install all the RVM system requirements:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rvm requirements
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter your sudo user password when prompted to update the server's package index. Your output should be similar to the one below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Checking requirements for ubuntu.
Installing requirements for ubuntu.
.....................
Installing required packages: autoconf, automake, bison, libffi-dev, libgdbm-dev, libsqlite3-dev, libtool, libyaml-dev, sqlite3, libgmp-dev, libncurses-dev, libreadline-dev, libssl-dev...............
Requirements installation successful.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. View the installed RVM version:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rvm &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your output should be similar to the one below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;rvm 1.29.12-next (master) by Michal Papis, Piotr Kuczynski, Wayne E. Seguin [https://rvm.io]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. List the available Ruby versions you can install:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rvm list known
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the target Ruby version in your output similar to the one below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# MRI Rubies
&lt;/span&gt;&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;1.8.6&lt;/span&gt;&lt;span class="nn"&gt;[-p420]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;1.8.7&lt;/span&gt;&lt;span class="nn"&gt;[-head]&lt;/span&gt; &lt;span class="c"&gt;# security released on head
&lt;/span&gt;&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;1.9.1&lt;/span&gt;&lt;span class="nn"&gt;[-p431]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;1.9.2&lt;/span&gt;&lt;span class="nn"&gt;[-p330]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;1.9.3&lt;/span&gt;&lt;span class="nn"&gt;[-p551]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;2.0.0&lt;/span&gt;&lt;span class="nn"&gt;[-p648]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;2.1&lt;/span&gt;&lt;span class="nn"&gt;[.10]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;2.2&lt;/span&gt;&lt;span class="nn"&gt;[.10]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;2.3&lt;/span&gt;&lt;span class="nn"&gt;[.8]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;2.4&lt;/span&gt;&lt;span class="nn"&gt;[.10]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;2.5&lt;/span&gt;&lt;span class="nn"&gt;[.9]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;2.6&lt;/span&gt;&lt;span class="nn"&gt;[.10]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;2.7&lt;/span&gt;&lt;span class="nn"&gt;[.8]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;3.0&lt;/span&gt;&lt;span class="nn"&gt;[.7]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;3.1&lt;/span&gt;&lt;span class="nn"&gt;[.5]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;3.2&lt;/span&gt;&lt;span class="nn"&gt;[.6]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;3&lt;/span&gt;&lt;span class="nn"&gt;[.3.6]&lt;/span&gt;
&lt;span class="nn"&gt;[ruby-]&lt;/span&gt;&lt;span class="err"&gt;3.4&lt;/span&gt;&lt;span class="nn"&gt;[.0-preview1]&lt;/span&gt;
&lt;span class="err"&gt;ruby-head&lt;/span&gt;
&lt;span class="err"&gt;............&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. Install a specific Ruby version.&lt;/strong&gt; For example, &lt;code&gt;3.3.6&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rvm &lt;span class="nb"&gt;install &lt;/span&gt;3.3.6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;8. Set the installed Ruby version as the default:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rvm &lt;span class="nt"&gt;--default&lt;/span&gt; use 3.3.6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;9. View all installed Ruby versions:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rvm list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your output should be similar to the one below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;=*&lt;/span&gt; &lt;span class="err"&gt;ruby-3.3.6&lt;/span&gt; &lt;span class="nn"&gt;[ x86_64 ]&lt;/span&gt;

&lt;span class="c"&gt;# =&amp;gt; - current
# =* - current &amp;amp;&amp;amp; default
#  * - default
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Install Ruby Using APT
&lt;/h2&gt;

&lt;p&gt;Ruby is available in the default APT package manager repositories, but the included version is not the latest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Update the server's package index:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Install Ruby:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ruby-full
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. View the installed Ruby version:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ruby &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your output should be similar to the one below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;ruby 3.2.3 (2024-01-18 revision 52bb2ac0a6) [x86_64-linux-gnu]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Uninstall Ruby
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Uninstall a specific Ruby version using rbenv.&lt;/strong&gt; For example, &lt;code&gt;3.3.6&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rbenv uninstall 3.3.6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Remove a specific Ruby version using RVM:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rvm remove 3.3.6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Uninstall Ruby installed using APT:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt autoremove ruby-full
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;You now have Ruby installed on Ubuntu 24.04 using rbenv, RVM, and/or APT, with the ability to install and switch between multiple Ruby versions. From here, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install a Ruby framework such as Rails or Sinatra to start building web applications&lt;/li&gt;
&lt;li&gt;Set a per-project Ruby version with a &lt;code&gt;.ruby-version&lt;/code&gt; file when using rbenv or RVM&lt;/li&gt;
&lt;li&gt;Set up &lt;code&gt;bundler&lt;/code&gt; to manage gem dependencies for your projects&lt;/li&gt;
&lt;li&gt;Review the &lt;a href="https://rvm.io/" rel="noopener noreferrer"&gt;RVM Documentation&lt;/a&gt; or &lt;a href="https://rbenv.org/man/rbenv.1" rel="noopener noreferrer"&gt;rbenv Manual Page&lt;/a&gt; for advanced version management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full guide with additional tips, visit the original article on &lt;strong&gt;&lt;a href="https://docs.vultr.com/how-to-install-ruby-on-ubuntu-24-04" rel="noopener noreferrer"&gt;Vultr Docs&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>ubuntu</category>
      <category>linux</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
