<?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: Prince</title>
    <description>The latest articles on DEV Community by Prince (@offpage_prince).</description>
    <link>https://dev.to/offpage_prince</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3904148%2F08be118a-2043-4f34-aeeb-decac6008f37.png</url>
      <title>DEV Community: Prince</title>
      <link>https://dev.to/offpage_prince</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/offpage_prince"/>
    <language>en</language>
    <item>
      <title>Deploying FastAPI on a VPS with Docker Compose: The Production Checklist</title>
      <dc:creator>Prince</dc:creator>
      <pubDate>Mon, 28 Sep 2026 11:13:03 +0000</pubDate>
      <link>https://dev.to/offpage_prince/deploying-fastapi-on-a-vps-with-docker-compose-the-production-checklist-2edi</link>
      <guid>https://dev.to/offpage_prince/deploying-fastapi-on-a-vps-with-docker-compose-the-production-checklist-2edi</guid>
      <description>&lt;p&gt;Getting FastAPI running in Docker takes five minutes. Getting it to behave well on a real server takes a little longer, because the things that bite you in production rarely show up in the "hello world" tutorial: client IPs that all look the same, migrations racing the app on startup, containers running as root, and logs slowly filling the disk.&lt;/p&gt;

&lt;p&gt;This post is the checklist I work through when moving a FastAPI service from a laptop to a single Linux VPS with Docker Compose. It assumes you already have a working app and a server with Docker installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A Dockerfile that is boring on purpose
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.13-slim&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONDONTWRITEBYTECODE=1 \&lt;/span&gt;
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; app ./app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; alembic.ini .&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; migrations ./migrations&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;--create-home&lt;/span&gt; &lt;span class="nt"&gt;--uid&lt;/span&gt; 10001 appuser
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", \&lt;/span&gt;
     "--workers", "2", "--forwarded-allow-ips", "*"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few choices worth explaining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Copy &lt;code&gt;requirements.txt&lt;/code&gt; first.&lt;/strong&gt; Dependency installation is cached until that file changes, so code-only changes rebuild in seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin versions&lt;/strong&gt; in &lt;code&gt;requirements.txt&lt;/code&gt; (&lt;code&gt;fastapi==0.115.6&lt;/code&gt;, not &lt;code&gt;fastapi&lt;/code&gt;). An unpinned rebuild months later is a different application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run as a non-root user.&lt;/strong&gt; If something in your dependency tree is ever exploited, the attacker lands as an unprivileged user inside the container instead of root.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the exec form of &lt;code&gt;CMD&lt;/code&gt;.&lt;/strong&gt; With the JSON array form, Uvicorn is PID 1 and receives &lt;code&gt;SIGTERM&lt;/code&gt; directly, so in-flight requests finish during &lt;code&gt;docker compose down&lt;/code&gt; or a redeploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PYTHONUNBUFFERED=1&lt;/code&gt;&lt;/strong&gt; makes log lines appear immediately in &lt;code&gt;docker logs&lt;/code&gt; instead of arriving in bursts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On workers: recent Uvicorn versions can supervise several worker processes themselves with &lt;code&gt;--workers&lt;/code&gt;, so a separate Gunicorn layer is no longer required for most apps. Start with one or two workers per vCPU and measure. If your endpoints are mostly async and I/O bound, fewer workers than you expect often perform fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Trust the proxy, and only the proxy
&lt;/h2&gt;

&lt;p&gt;Your app will sit behind a reverse proxy that terminates HTTPS. Without extra configuration, FastAPI sees every request as plain HTTP coming from the proxy container's IP. That breaks redirects (they point to &lt;code&gt;http://&lt;/code&gt;), rate limiting, and any audit logging of client IPs.&lt;/p&gt;

&lt;p&gt;Uvicorn reads &lt;code&gt;X-Forwarded-For&lt;/code&gt; and &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; headers, but only from addresses listed in &lt;code&gt;--forwarded-allow-ips&lt;/code&gt;. Setting it to &lt;code&gt;*&lt;/code&gt; is acceptable &lt;strong&gt;only because the API container publishes no ports&lt;/strong&gt;: the proxy is the only thing that can reach it. If you ever expose port 8000 directly, tighten this to the proxy's network range.&lt;/p&gt;

&lt;p&gt;If you mount the API under a path prefix such as &lt;code&gt;/api&lt;/code&gt;, also set &lt;code&gt;root_path&lt;/code&gt; so the generated OpenAPI docs use the correct URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Compose file with health checks and a migration step
&lt;/h2&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;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.4-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;app&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;app&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:?set in .env}&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;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-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;app&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;app"&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;3s&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;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;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;ghcr.io/acme/api:${API_TAG:?set in .env}&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;alembic"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upgrade"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;head"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;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;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no"&lt;/span&gt;

  &lt;span class="na"&gt;api&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/acme/api:${API_TAG}&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;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;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;python"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;urllib.request;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;urllib.request.urlopen('http://127.0.0.1:8000/health',&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;timeout=3)"&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;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="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;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;caddy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caddy:2&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./Caddyfile:/etc/caddy/Caddyfile:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_data:/data&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="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this buys you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postgres is actually ready&lt;/strong&gt; before anything connects, not merely "started". &lt;code&gt;depends_on&lt;/code&gt; without a condition only waits for the container to exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrations run exactly once per deploy&lt;/strong&gt;, in their own short-lived container, before the API starts. If a migration fails, the API does not start on a half-migrated schema, and &lt;code&gt;docker compose logs migrate&lt;/code&gt; tells you why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The health check uses Python itself&lt;/strong&gt;, so you do not need to install &lt;code&gt;curl&lt;/code&gt; in a slim image just to probe &lt;code&gt;/health&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log rotation&lt;/strong&gt; caps each container at about 30 MB of logs. The default JSON log driver has no limit, and "disk full" is one of the most common ways a small VPS falls over.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your &lt;code&gt;/health&lt;/code&gt; endpoint should be cheap. A good pattern is a liveness check that returns immediately, plus an optional readiness check that runs &lt;code&gt;SELECT 1&lt;/code&gt; against the database with a short timeout.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Caddyfile&lt;/code&gt; for the API is two lines, and it handles certificates and HTTP to HTTPS redirects automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;api.example.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;reverse_proxy&lt;/span&gt; &lt;span class="nf"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Configuration through the environment
&lt;/h2&gt;

&lt;p&gt;Keep secrets and per-environment values in &lt;code&gt;.env&lt;/code&gt; on the server (never in Git) and read them with &lt;code&gt;pydantic-settings&lt;/code&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;pydantic_settings&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseSettings&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseSettings&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;database_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;secret_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;cors_origins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&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;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app fails fast at startup if &lt;code&gt;DATABASE_URL&lt;/code&gt; is missing, which is exactly when you want to find out. Note the host in the URL is the Compose service name: &lt;code&gt;postgresql+asyncpg://app:...@db:5432/app&lt;/code&gt;. Using &lt;code&gt;localhost&lt;/code&gt; here is the classic cause of "connection refused", because inside the API container &lt;code&gt;localhost&lt;/code&gt; is the API container.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Deploys you can roll back
&lt;/h2&gt;

&lt;p&gt;Build images in CI, tag them with the Git commit SHA, and push them to a registry. On the server, a deploy becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"s/^API_TAG=.*/API_TAG=3f9c2ab/"&lt;/span&gt; .env
docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rolling back is the same three commands with the previous SHA. Avoid the &lt;code&gt;latest&lt;/code&gt; tag in production: when something breaks, you want to know exactly which build is running. Be careful with migrations here, too. A rollback of code is easy; a rollback of a destructive schema change is not, so prefer additive migrations (add a column, backfill, remove the old one in a later release).&lt;/p&gt;

&lt;p&gt;For a longer walkthrough that also covers first-time server preparation, domain setup and HTTPS from scratch, this &lt;a href="https://peon.sh/blogs/deploy-fastapi-vps-docker" rel="noopener noreferrer"&gt;step-by-step guide to deploying FastAPI on a VPS with Docker&lt;/a&gt; is a good companion to the checklist here.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Back up the database before you need to
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;pgdata&lt;/code&gt; volume is the only irreplaceable thing on this server. Containers and images can be rebuilt in minutes; data cannot. At minimum, run a nightly &lt;code&gt;pg_dump&lt;/code&gt; to storage that lives somewhere other than the VPS, keep a few weeks of history, and actually test a restore once. If you want a concrete setup with scheduling, retention and encryption, see this walkthrough on &lt;a href="https://peon.sh/blogs/postgres-backup-s3" rel="noopener noreferrer"&gt;automating Postgres backups to S3&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pin versions, run as non-root, use exec-form &lt;code&gt;CMD&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Publish no app ports; let the reverse proxy be the only entry point, and configure forwarded headers.&lt;/li&gt;
&lt;li&gt;Gate startup on a healthy database and a successful migration container.&lt;/li&gt;
&lt;li&gt;Rotate logs so they cannot fill the disk.&lt;/li&gt;
&lt;li&gt;Tag images by commit so deploys and rollbacks are one command.&lt;/li&gt;
&lt;li&gt;Back up Postgres off the server and test the restore.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic, but together it is the difference between an API that runs and an API you can leave alone for months.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>Traefik vs Caddy on One Docker Host: Same Three Apps, Two Configs</title>
      <dc:creator>Prince</dc:creator>
      <pubDate>Fri, 25 Sep 2026 07:55:48 +0000</pubDate>
      <link>https://dev.to/offpage_prince/traefik-vs-caddy-on-one-docker-host-same-three-apps-two-configs-3afd</link>
      <guid>https://dev.to/offpage_prince/traefik-vs-caddy-on-one-docker-host-same-three-apps-two-configs-3afd</guid>
      <description>&lt;p&gt;Most "Traefik vs Caddy" debates stay abstract: one is "dynamic", the other is "simple". That is true, but it does not help much when you are staring at a fresh VPS with three containers that all want ports 80 and 443.&lt;/p&gt;

&lt;p&gt;So here is a more practical test. Take one small but realistic stack and wire it up twice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;app.example.com&lt;/code&gt;: a web frontend listening on port 3000&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;api.example.com&lt;/code&gt;: an API listening on port 8000, with a &lt;code&gt;/health&lt;/code&gt; endpoint&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;admin.example.com&lt;/code&gt;: an internal tool that should sit behind basic auth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Requirements for both versions: automatic HTTPS, HTTP to HTTPS redirects, no application ports published on the host, and a health check on the API. Then we compare what it felt like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shared starting point
&lt;/h2&gt;

&lt;p&gt;Both setups use one Docker network that the proxy and the apps share. The apps never publish ports; only the proxy binds to the host.&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;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&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;web&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DNS for all three hostnames points at the server's public IP, and ports 80 and 443 are open in the firewall. Both proxies need port 80 reachable for the HTTP-01 ACME challenge unless you switch to DNS challenges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 1: Traefik
&lt;/h2&gt;

&lt;p&gt;Traefik's model is "configure the proxy once, then describe routes on each container with labels". The static configuration goes on the Traefik container itself:&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&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=web&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.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=ops@example.com&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="nv"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;]&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;Then each service declares its own routing:&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;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/acme/frontend:1.4.2&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="nv"&gt;web&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="s"&gt;traefik.enable=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.app.rule=Host(`app.example.com`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.app.entrypoints=websecure&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.app.tls.certresolver=le&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.app.loadbalancer.server.port=3000&lt;/span&gt;

  &lt;span class="na"&gt;api&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/acme/api:2.0.1&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="nv"&gt;web&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="s"&gt;traefik.enable=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.api.rule=Host(`api.example.com`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.api.entrypoints=websecure&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.api.tls.certresolver=le&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.api.loadbalancer.server.port=8000&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.api.loadbalancer.healthcheck.path=/health&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.api.loadbalancer.healthcheck.interval=10s&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/acme/admin:0.9.0&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="nv"&gt;web&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="s"&gt;traefik.enable=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.admin.rule=Host(`admin.example.com`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.admin.entrypoints=websecure&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.admin.tls.certresolver=le&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.admin.middlewares=admin-auth&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.middlewares.admin-auth.basicauth.users=admin:$$apr1$$REPLACE$$WITHHASH&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.admin.loadbalancer.server.port=8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate the basic auth hash with &lt;code&gt;htpasswd -nb admin 'your-password'&lt;/code&gt; and double every &lt;code&gt;$&lt;/code&gt; so Compose does not treat it as variable interpolation. That doubling trips up almost everyone once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stood out:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding a fourth app never touches the proxy. You add labels, run &lt;code&gt;docker compose up -d&lt;/code&gt;, and Traefik picks up the route within seconds.&lt;/li&gt;
&lt;li&gt;The labels are verbose. Router, service and middleware names must line up exactly, and a typo fails quietly: the route simply does not exist and you get a 404.&lt;/li&gt;
&lt;li&gt;Mounting the Docker socket gives Traefik broad control over the Docker daemon. On anything shared or important, put a socket proxy such as &lt;code&gt;tecnativa/docker-socket-proxy&lt;/code&gt; in between and expose only read access to containers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Version 2: Caddy
&lt;/h2&gt;

&lt;p&gt;Caddy's model is "one readable file describes every site". The container is minimal:&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;caddy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caddy:2&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443/udp"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./Caddyfile:/etc/caddy/Caddyfile:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_data:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_config:/config&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="nv"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;]&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="na"&gt;caddy_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app services keep their images and the &lt;code&gt;web&lt;/code&gt; network, but lose all labels. The routing lives in the &lt;code&gt;Caddyfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;app.example.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;reverse_proxy&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;api.example.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;reverse_proxy&lt;/span&gt; &lt;span class="nf"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;health_uri&lt;/span&gt; &lt;span class="n"&gt;/health&lt;/span&gt;
        &lt;span class="s"&gt;health_interval&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
    &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;admin.example.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;basic_auth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;admin&lt;/span&gt; &lt;span class="nv"&gt;$2a$14$REPLACE_WITH_BCRYPT_HASH&lt;/span&gt;
    &lt;span class="err"&gt;}&lt;/span&gt;
    &lt;span class="s"&gt;reverse_proxy&lt;/span&gt; &lt;span class="nf"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate the hash with &lt;code&gt;docker compose exec caddy caddy hash-password&lt;/code&gt;. HTTPS, certificate renewal, and the HTTP to HTTPS redirect all happen without a single extra line. The published &lt;code&gt;443/udp&lt;/code&gt; port lets Caddy serve HTTP/3, which it enables by default.&lt;/p&gt;

&lt;p&gt;After editing the file, reload without downtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; /etc/caddy caddy caddy reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What stood out:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The whole routing table fits on one screen. A new teammate can read it and understand the server in a minute.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;/data&lt;/code&gt; volume matters. Lose it and Caddy requests fresh certificates on every restart, which can run you into Let's Encrypt rate limits.&lt;/li&gt;
&lt;li&gt;Adding an app means editing a central file and reloading. That is a feature for small, stable servers and friction for servers where containers come and go all day.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Traefik&lt;/th&gt;
&lt;th&gt;Caddy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where routes live&lt;/td&gt;
&lt;td&gt;Labels on each container&lt;/td&gt;
&lt;td&gt;One Caddyfile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New app workflow&lt;/td&gt;
&lt;td&gt;Add labels, &lt;code&gt;up -d&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Edit file, reload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTPS defaults&lt;/td&gt;
&lt;td&gt;Needs resolver + entrypoint config&lt;/td&gt;
&lt;td&gt;On by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debugging a missing route&lt;/td&gt;
&lt;td&gt;Dashboard or logs; typos fail silently&lt;/td&gt;
&lt;td&gt;Config errors fail loudly on reload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker socket access&lt;/td&gt;
&lt;td&gt;Required for discovery&lt;/td&gt;
&lt;td&gt;Not required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic environments (Swarm, many short-lived stacks)&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;Possible via the caddy-docker-proxy plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How I would choose
&lt;/h2&gt;

&lt;p&gt;Pick &lt;strong&gt;Caddy&lt;/strong&gt; when you have a handful of long-lived services, one or two people managing the server, and you value being able to read the entire configuration at a glance. For most single-VPS setups this is the calmer option, and its error messages are friendlier when something goes wrong.&lt;/p&gt;

&lt;p&gt;Pick &lt;strong&gt;Traefik&lt;/strong&gt; when services appear and disappear often, when several people deploy independently and should not need to edit a shared file, or when you are already on Swarm or planning to move towards an orchestrator. The label model scales with the number of teams rather than the number of config files.&lt;/p&gt;

&lt;p&gt;If you want a broader breakdown that also covers load balancing strategies, middleware, and performance considerations, this &lt;a href="https://peon.sh/blogs/traefik-vs-caddy" rel="noopener noreferrer"&gt;deeper Traefik vs Caddy comparison for Docker&lt;/a&gt; goes further than the hands-on test above. And if your real question is less "which proxy" and more "how do I lay out a server that hosts many projects cleanly", this guide on &lt;a href="https://peon.sh/blogs/how-to-run-multiple-apps-on-one-vps" rel="noopener noreferrer"&gt;running multiple apps on one VPS&lt;/a&gt; covers domains, networks, and resource limits around the proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few gotchas that apply to both
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Do not publish app ports.&lt;/strong&gt; If &lt;code&gt;api&lt;/code&gt; still has &lt;code&gt;ports: ["8000:8000"]&lt;/code&gt;, it is reachable over plain HTTP on the server IP, bypassing your auth and TLS. Remove it and let the proxy be the only door.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use service names, not localhost.&lt;/strong&gt; Inside the proxy container, &lt;code&gt;localhost&lt;/code&gt; is the proxy itself. Always target &lt;code&gt;api:8000&lt;/code&gt;, never &lt;code&gt;127.0.0.1:8000&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass real client IPs.&lt;/strong&gt; Both proxies set &lt;code&gt;X-Forwarded-For&lt;/code&gt;, but your app framework must be told to trust the proxy. Otherwise rate limiting and logs see every request as coming from the proxy container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back up certificate storage.&lt;/strong&gt; That means &lt;code&gt;acme.json&lt;/code&gt; for Traefik and the &lt;code&gt;/data&lt;/code&gt; volume for Caddy. Both are small and save you from rate limit trouble after a rebuild.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Either proxy will serve you well. The better question is where you want your routing to live: next to each service, or in one file you can read top to bottom. If you would rather not maintain that layer at all, self-hosted platforms like Peon, Coolify and Dokploy generate the proxy configuration for you, but knowing what they do underneath makes debugging much easier when a certificate refuses to issue at 2 a.m.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>linux</category>
    </item>
    <item>
      <title>Docker Compose in Production on a Single VPS: A Practical Checklist</title>
      <dc:creator>Prince</dc:creator>
      <pubDate>Fri, 25 Sep 2026 07:51:13 +0000</pubDate>
      <link>https://dev.to/offpage_prince/docker-compose-in-production-on-a-single-vps-a-practical-checklist-422c</link>
      <guid>https://dev.to/offpage_prince/docker-compose-in-production-on-a-single-vps-a-practical-checklist-422c</guid>
      <description>&lt;p&gt;Kubernetes gets most of the attention, but a large share of real production workloads run on one or two virtual machines with Docker Compose. For a SaaS in its first years, an internal tool, or a content site, a single well configured VPS is cheaper, easier to reason about, and fast to recover.&lt;/p&gt;

&lt;p&gt;The catch is that the Compose file that works on a laptop is not the Compose file you want in production. This checklist covers the changes that matter most, in roughly the order I apply them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Pin image versions
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;image: postgres:latest&lt;/code&gt; is a time bomb. The next &lt;code&gt;docker compose pull&lt;/code&gt; can bring a new major version and a data format your volume does not support.&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;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.4-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pin to a specific minor or patch version, and upgrade deliberately. For your own images, tag with the Git SHA or a release version rather than &lt;code&gt;latest&lt;/code&gt;, so you always know what is running and can roll back to a known tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Always set a restart policy
&lt;/h2&gt;

&lt;p&gt;Containers crash, and hosts reboot for kernel updates. Without a restart policy, a reboot means downtime until someone notices.&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;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;unless-stopped&lt;/code&gt; survives reboots but respects a manual &lt;code&gt;docker compose stop&lt;/code&gt;, which is usually what you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Add health checks and use them
&lt;/h2&gt;

&lt;p&gt;A running container is not the same as a healthy service. Health checks let Docker know when your app is actually ready, and &lt;code&gt;depends_on&lt;/code&gt; can wait for them.&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;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;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="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;wget"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-qO-"&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/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;15s&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;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20s&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.4-alpine&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;app"&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;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the health endpoint cheap. It should confirm the process can serve requests and reach critical dependencies, not run a full test suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keep secrets out of the Compose file
&lt;/h2&gt;

&lt;p&gt;Hardcoded passwords in &lt;code&gt;docker-compose.yml&lt;/code&gt; end up in Git history. Use an &lt;code&gt;.env&lt;/code&gt; file that is excluded from version control, or Docker secrets for more sensitive values.&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;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env.production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set file permissions to &lt;code&gt;600&lt;/code&gt;, and keep a separate env file per environment. Rotate anything that was ever committed, even briefly.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Do not publish database ports
&lt;/h2&gt;

&lt;p&gt;A common mistake is &lt;code&gt;ports: - "5432:5432"&lt;/code&gt; on the database service. On a public VPS, that exposes Postgres to the internet, and Docker's iptables rules can bypass host firewalls like UFW.&lt;/p&gt;

&lt;p&gt;Services on the same Compose network can already reach each other by name. Only publish the ports your reverse proxy needs, and bind them to localhost if the proxy runs on the host:&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;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:3000:3000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Put a reverse proxy in front
&lt;/h2&gt;

&lt;p&gt;You want TLS, HTTP to HTTPS redirects, and the ability to run several apps on one machine. Caddy and Traefik both handle Let's Encrypt certificates automatically. Caddy wins on simplicity; Traefik wins when you want routing driven by container labels.&lt;/p&gt;

&lt;p&gt;Either way, the proxy is the only thing that should listen on ports 80 and 443.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Use named volumes and back them up
&lt;/h2&gt;

&lt;p&gt;Named volumes survive container recreation and are easy to find:&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;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&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="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A volume is not a backup. Schedule logical dumps (&lt;code&gt;pg_dump&lt;/code&gt;) to object storage such as S3 or a compatible provider, keep several days of history, and test a restore at least once a quarter. Untested backups are a hope, not a plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Cap log size
&lt;/h2&gt;

&lt;p&gt;Docker's default &lt;code&gt;json-file&lt;/code&gt; driver grows without limit. A chatty container can fill the disk and take down everything on the host.&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;x-logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;default-logging&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;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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*default-logging&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using a YAML anchor keeps the setting consistent across services. If you need search and retention, ship logs to a central store instead of keeping them on the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Set resource limits
&lt;/h2&gt;

&lt;p&gt;One runaway process should not starve the rest. Compose supports memory and CPU limits under &lt;code&gt;deploy.resources&lt;/code&gt;, which modern Docker Compose applies outside Swarm as well:&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;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;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;cpus&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.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start generous, watch real usage for a week, then tighten.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Plan how you deploy updates
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;docker compose up -d --build&lt;/code&gt; on the server works, but it rebuilds on the production machine and briefly stops the old container before the new one is ready. Better options, from simplest to most robust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build images in CI, push to a registry, and only &lt;code&gt;pull&lt;/code&gt; and &lt;code&gt;up -d&lt;/code&gt; on the server.&lt;/li&gt;
&lt;li&gt;Run two instances behind the proxy and replace them one at a time.&lt;/li&gt;
&lt;li&gt;Use a blue/green setup where the proxy switches traffic only after the new version passes its health check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last two are the basis of &lt;a href="https://peon.sh/blogs/zero-downtime-deployments-docker" rel="noopener noreferrer"&gt;zero downtime deployments with Docker&lt;/a&gt;, and they depend on the health checks from step 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Harden the host
&lt;/h2&gt;

&lt;p&gt;Compose settings do not help if the host is weak. At minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disable SSH password login and use keys.&lt;/li&gt;
&lt;li&gt;Enable unattended security updates.&lt;/li&gt;
&lt;li&gt;Allow only ports 22, 80, and 443 at the firewall or provider level.&lt;/li&gt;
&lt;li&gt;Run containers as a non-root user where the image supports it.&lt;/li&gt;
&lt;li&gt;Keep enough free disk: prune old images with &lt;code&gt;docker image prune&lt;/code&gt; on a schedule.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  12. Monitor from the outside
&lt;/h2&gt;

&lt;p&gt;Internal health checks tell Docker what is wrong. External monitoring tells you. A simple uptime checker hitting your public URL every minute, plus alerts on disk usage and memory, catches most incidents before users report them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;None of these steps is complicated, but skipping any one of them is how a small outage turns into a long night. The order above also works as a review checklist for an existing server: go through it top to bottom and fix whatever is missing.&lt;/p&gt;

&lt;p&gt;If you want a longer walkthrough with a complete example file, this &lt;a href="https://peon.sh/blogs/docker-compose-production-deployment" rel="noopener noreferrer"&gt;production Docker Compose guide&lt;/a&gt; goes deeper on env handling, networking, and deployment workflow.&lt;/p&gt;

&lt;p&gt;A single VPS with a disciplined Compose setup will carry you further than most people expect. Move to an orchestrator when you genuinely need multi-node scheduling, not because the Compose file on your laptop was never production ready.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Letting AI Agents Deploy to Your Own Servers With MCP (Without Handing Them Root)</title>
      <dc:creator>Prince</dc:creator>
      <pubDate>Fri, 25 Sep 2026 06:02:33 +0000</pubDate>
      <link>https://dev.to/offpage_prince/letting-ai-agents-deploy-to-your-own-servers-with-mcp-without-handing-them-root-12na</link>
      <guid>https://dev.to/offpage_prince/letting-ai-agents-deploy-to-your-own-servers-with-mcp-without-handing-them-root-12na</guid>
      <description>&lt;p&gt;Coding agents got good at writing code long before they got safe at running it. The moment you ask Cursor or Claude to "deploy this and check the logs", you hit a real question: what exactly is the agent allowed to touch?&lt;/p&gt;

&lt;p&gt;The lazy answer is an SSH key or a cloud admin token pasted into a config file. It works, and it is also how you end up with an agent that can &lt;code&gt;rm -rf&lt;/code&gt; a production volume because it misread a stack trace.&lt;/p&gt;

&lt;p&gt;This post walks through a better pattern for self-hosted infrastructure: expose deployment operations to agents through the Model Context Protocol (MCP), and let the same role based access control that governs your human team govern the agent too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MCP changes
&lt;/h2&gt;

&lt;p&gt;MCP is a standard way for an AI client to discover and call tools on a server. Instead of the agent improvising shell commands, it sees a typed catalog: &lt;code&gt;list_services&lt;/code&gt;, &lt;code&gt;deploy&lt;/code&gt;, &lt;code&gt;get_logs&lt;/code&gt;, &lt;code&gt;set_env&lt;/code&gt;, and so on. Each tool has a schema, and the server decides what each call is allowed to do.&lt;/p&gt;

&lt;p&gt;That shift matters for operations work for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The surface is explicit.&lt;/strong&gt; You can read the tool list and know what the agent can do. There is no "anything bash can do" category.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization lives on the server.&lt;/strong&gt; The client sends a token; the server checks it on every call. The agent cannot talk its way past a permission check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actions are auditable.&lt;/strong&gt; Tool calls go through the same API as your dashboard, so they land in the same logs.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The threat model in plain terms
&lt;/h2&gt;

&lt;p&gt;Before wiring anything up, write down what you are protecting against. For most small teams it looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent misunderstands an instruction and changes the wrong service.&lt;/li&gt;
&lt;li&gt;The agent leaks a credential into a chat transcript or a commit.&lt;/li&gt;
&lt;li&gt;A prompt injection in a README, issue, or log line convinces the agent to do something you never asked for.&lt;/li&gt;
&lt;li&gt;A token issued "just for testing" never gets revoked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are solved by a smarter model. They are solved by scoping.&lt;/p&gt;

&lt;h2&gt;
  
  
  A scoping checklist that works
&lt;/h2&gt;

&lt;p&gt;Whatever platform you use, these rules hold up well:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a per-person, per-workspace token.&lt;/strong&gt; Never share one token across a team, and never reuse your own admin token for an agent that works on a single project. If your platform supports it, create a separate user or membership for automation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inherit roles, do not invent them.&lt;/strong&gt; The agent should get exactly the permissions of the human who issued the token, or less. If a developer can only deploy to the &lt;code&gt;staging&lt;/code&gt; project, their agent should hit the same wall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leave out interactive shells.&lt;/strong&gt; Shell access is the escape hatch that makes every other control pointless. Deployments, restarts, env changes, and log reads cover nearly everything an agent needs. Keep terminals for humans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat tokens like passwords.&lt;/strong&gt; Keep them in your MCP client config or a secret manager, not in repos. Rotate by revoking and reissuing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer reversible actions.&lt;/strong&gt; Rollback should be one tool call. If an agent can deploy, it should also be able to undo its own deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;I have been testing this pattern with &lt;a href="https://peon.sh/docs/mcp" rel="noopener noreferrer"&gt;Peon's MCP server docs&lt;/a&gt; as the reference setup, because it follows the checklist closely. Peon is an open source, self-hostable deployment platform, and its MCP endpoint is part of the app itself rather than a side script.&lt;/p&gt;

&lt;p&gt;The client configuration is short. In Cursor or Claude Desktop you add a streamable HTTP server with a bearer token:&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;"mcpServers"&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;"peon"&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://app.peon.sh/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&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;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer peon_xxxxxxxx"&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;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;If you self-host, the URL becomes &lt;code&gt;https://your-domain/mcp&lt;/code&gt;. The token is created in the dashboard under Keys and Tokens, is scoped to one workspace, and inherits the creator's role. An owner or admin token can manage servers; a project member token only reaches the projects that member belongs to. The details are in the docs on &lt;a href="https://peon.sh/docs/workspaces-and-roles" rel="noopener noreferrer"&gt;workspace and project roles&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two design choices are worth copying even if you build your own server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shell exec tools are not registered on MCP.&lt;/strong&gt; The agent can deploy, restart, roll back, read logs, and manage env vars, but it cannot open a shell on a server or inside a container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same RBAC applies to the API, the MCP server, and the in-app assistant.&lt;/strong&gt; The UI is not the security boundary; the API is.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A realistic agent session
&lt;/h2&gt;

&lt;p&gt;Here is the kind of loop that works well once scoping is in place:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"List services in the &lt;code&gt;blog&lt;/code&gt; project and tell me which one failed its last deploy."&lt;/li&gt;
&lt;li&gt;The agent calls the list and deployment tools, finds a failed build, and pulls the build logs.&lt;/li&gt;
&lt;li&gt;It spots a missing &lt;code&gt;DATABASE_URL&lt;/code&gt;, explains the fix, and asks before setting it.&lt;/li&gt;
&lt;li&gt;After you confirm, it sets the variable, triggers a redeploy, and watches the status.&lt;/li&gt;
&lt;li&gt;If the new deployment fails health checks, it rolls back and reports what happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nothing in that loop needed root. Every step is a typed call that your platform can log and deny. If you want a full example, there is a &lt;a href="https://peon.sh/blogs/ai-agents-deploy-apps-mcp-peon" rel="noopener noreferrer"&gt;walkthrough of an agent shipping an app end to end&lt;/a&gt; that covers the same flow with screenshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrails on the client side
&lt;/h2&gt;

&lt;p&gt;Server side scoping is the foundation, but a few client habits help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Require confirmation for writes.&lt;/strong&gt; Most MCP clients can ask before running a tool. Leave that on for anything that deploys, deletes, or changes env.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep read-only tokens for exploration.&lt;/strong&gt; If your role model supports a viewer role, issue a viewer token for "what is running and why is it slow" sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be suspicious of instructions found in data.&lt;/strong&gt; Logs, READMEs, and issue text can contain text that looks like instructions. Tell the agent explicitly that tool output is data, not a command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review the audit trail weekly.&lt;/strong&gt; It takes five minutes and catches forgotten tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When not to use an agent for deploys
&lt;/h2&gt;

&lt;p&gt;Agents are great at the boring middle of operations: reading logs, correlating a failed deploy with a config change, and doing the obvious fix. They are a poor fit for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First-time infrastructure setup where you need to understand every choice.&lt;/li&gt;
&lt;li&gt;Database migrations with data loss risk.&lt;/li&gt;
&lt;li&gt;Anything involving billing, DNS for your primary domain, or deleting backups.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep those human, or at least human-approved step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;MCP gives you a clean contract between an AI client and your infrastructure. The contract is only as safe as the permissions behind it, so start from the rules above: personal scoped tokens, inherited roles, no shell tools, reversible actions, and an audit trail.&lt;/p&gt;

&lt;p&gt;If you run your own servers, this is one of the few places where self-hosting makes AI tooling safer rather than riskier, because you control both the tool catalog and the permission model. I would love to hear how others are scoping agent access in their own stacks.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>ai</category>
    </item>
    <item>
      <title>Deploy Peon on a DigitalOcean Droplet (self-hosted Docker PaaS)</title>
      <dc:creator>Prince</dc:creator>
      <pubDate>Fri, 18 Sep 2026 12:30:50 +0000</pubDate>
      <link>https://dev.to/offpage_prince/deploy-peon-on-a-digitalocean-droplet-self-hosted-docker-paas-bkj</link>
      <guid>https://dev.to/offpage_prince/deploy-peon-on-a-digitalocean-droplet-self-hosted-docker-paas-bkj</guid>
      <description>&lt;p&gt;Peon is an open-source, self-hostable deployment platform. Think Vercel or Heroku workflows on a server you control: Git deploys, Docker Compose, databases, workspaces, and RBAC.&lt;/p&gt;

&lt;p&gt;This guide walks through a clean DigitalOcean droplet setup. Same pattern works on Hetzner, Vultr, and Contabo.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Site: &lt;a href="https://peon.sh/" rel="noopener noreferrer"&gt;https://peon.sh/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/Peon-sh/Peon" rel="noopener noreferrer"&gt;https://github.com/Peon-sh/Peon&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you will build
&lt;/h2&gt;

&lt;p&gt;A DigitalOcean Ubuntu droplet running Peon so you can connect the server over SSH and deploy apps from Git.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;DigitalOcean account&lt;/li&gt;
&lt;li&gt;SSH key added to DigitalOcean&lt;/li&gt;
&lt;li&gt;Domain name (optional, recommended for TLS)&lt;/li&gt;
&lt;li&gt;Basic SSH comfort&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Create the droplet
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an Ubuntu 24.04 LTS droplet (2 GB RAM minimum; 4 GB preferred).&lt;/li&gt;
&lt;li&gt;Choose your SSH key.&lt;/li&gt;
&lt;li&gt;Note the public IPv4 address.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  2. Point DNS (optional)
&lt;/h2&gt;

&lt;p&gt;Create an A record for peon.example.com to the droplet IP.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Harden basics
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@YOUR_DROPLET_IP
apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com | sh
docker &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Install Peon
&lt;/h2&gt;

&lt;p&gt;Follow current official docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;README: &lt;a href="https://github.com/Peon-sh/Peon" rel="noopener noreferrer"&gt;https://github.com/Peon-sh/Peon&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://peon.sh/docs" rel="noopener noreferrer"&gt;https://peon.sh/docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Typical flow: clone repo, configure .env secrets, start Postgres/queue, migrate, run app + worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Open Peon and add the droplet
&lt;/h2&gt;

&lt;p&gt;Add the server over SSH, create a workspace/project, deploy a sample Git repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Verify
&lt;/h2&gt;

&lt;p&gt;App URL responds, deploy logs clean, optional second project for RBAC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Peon vs Coolify / Dokploy
&lt;/h2&gt;

&lt;p&gt;All three are strong self-hosted PaaS options. Peon focuses on workspace/project tenancy, audit logs, and MCP hooks for AI agents, while staying MIT and self-host free.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>docker</category>
      <category>devops</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Peon vs Coolify: picking a self-hosted PaaS in 2026</title>
      <dc:creator>Prince</dc:creator>
      <pubDate>Fri, 04 Sep 2026 12:19:56 +0000</pubDate>
      <link>https://dev.to/offpage_prince/peon-vs-coolify-picking-a-self-hosted-paas-in-2026-4m7a</link>
      <guid>https://dev.to/offpage_prince/peon-vs-coolify-picking-a-self-hosted-paas-in-2026-4m7a</guid>
      <description>&lt;p&gt;Managed platforms are excellent until the invoice and the lock-in start arguing with your roadmap. Seat pricing, usage cliffs, and "you can leave anytime" exports that are never quite painless push a lot of teams toward a different model: keep the git-push UX, run the apps on hardware you control.&lt;/p&gt;

&lt;p&gt;That is the promise of a self-hosted PaaS. Two names that keep showing up in that conversation are [Coolify] and [&lt;a href="https://peon.sh/" rel="noopener noreferrer"&gt;Peon&lt;/a&gt;]. Both turn a Linux box into something closer to Heroku or Vercel, without giving up the server. This piece is a practical comparison for developers, DevOps folks, and SaaS founders who are deciding which one fits.&lt;/p&gt;

&lt;p&gt;Why people leave managed PaaS&lt;/p&gt;

&lt;p&gt;The usual triggers are boring and expensive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Per-seat bills that grow faster than headcount value. Inviting a contractor or a designer should not feel like a budget meeting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Vendor gravity. Preview deploys, edge features, and proprietary build tooling are sticky. Moving later is harder than starting elsewhere.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wanting a real VPS. Many teams already pay for Hetzner, DigitalOcean, AWS EC2, or a spare bare-metal machine. Paying again for someone else's orchestration on top feels redundant.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Data and networking control. Some products need private networking, unusual databases, or compliance stories that managed PaaS plans treat as enterprise upsells.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that means managed PaaS is bad. It means the tradeoff stopped matching the team.&lt;/p&gt;

&lt;p&gt;What "self-hosted PaaS" actually means&lt;/p&gt;

&lt;p&gt;A self-hosted PaaS is not Kubernetes-by-default and it is not "SSH and pray." In this category, a control plane talks to your servers (usually over SSH), runs Docker for you, wires a reverse proxy, issues Let's Encrypt certificates, and deploys from Git, images, or Compose files.&lt;/p&gt;

&lt;p&gt;You still own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The VPS or machines where containers run&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DNS for your domains&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Backups and capacity planning&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The platform owns the workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Connect a repo or image&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Build and roll out with fewer manual Docker steps&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Manage databases, env vars, logs, and rollbacks from a UI&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Coolify and Peon both live in that lane. They are closer to each other than either is to "just Docker Compose on a VPS" or to a full cluster platform.&lt;/p&gt;

&lt;p&gt;Coolify, fairly summarized&lt;/p&gt;

&lt;p&gt;&lt;a href="https://coolify.io/" rel="noopener noreferrer"&gt;Coolify&lt;/a&gt; is a mature open-source, self-hostable alternative to Vercel, Heroku, Netlify, and Railway. You point it at servers you can reach over SSH and deploy websites, APIs, databases, and a large catalog of one-click services.&lt;/p&gt;

&lt;p&gt;In practice, Coolify is strong when you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A well-known community and lots of tutorials&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A wide one-click service marketplace&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Git push deploys, Docker and Compose, automatic HTTPS&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multi-server management from one dashboard&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;PR preview style workflows and solid day-to-day Docker hosting&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Self-hosting Coolify itself is free; you pay for the machine it runs on. There is also a hosted control-plane option if you prefer not to operate that layer. For solo builders and teams that mainly need "Docker hosting with a good UI," Coolify is a legitimate default recommendation, not a straw man.&lt;/p&gt;

&lt;p&gt;Where Peon aims differently&lt;/p&gt;

&lt;p&gt;&lt;a href="https://peon.sh/" rel="noopener noreferrer"&gt;Peon&lt;/a&gt; sits in the same self-hosted PaaS category: your servers, Docker under the hood, git-based deploys, Compose, databases, custom domains, and automatic HTTPS. The product bet is that once more than one person touches production, the hard problems become permissions, accountability, and how AI tools interact with deploys.&lt;/p&gt;

&lt;p&gt;Project-level RBAC&lt;/p&gt;

&lt;p&gt;Peon models work as workspaces and projects, with roles at both levels. That matters when you want to invite someone to one app without handing them every server in the workspace. Agencies, multi-product startups, and teams with contractors feel this gap quickly on platforms that only think in "team on the whole instance."&lt;/p&gt;

&lt;p&gt;Audit logs on standard plans&lt;/p&gt;

&lt;p&gt;Owner-visible audit history for who changed what across projects, services, servers, and deploys is included on Peon's self-hosted and Cloud offerings, rather than treated as a distant enterprise checkbox. If you care about "who restarted production at 2 a.m.," that is the feature, not a nice-to-have slide.&lt;/p&gt;

&lt;p&gt;MCP for AI agents&lt;/p&gt;

&lt;p&gt;Peon ships a hosted Model Context Protocol (MCP) server so tools like Cursor or Claude can operate against your projects with the same permission model as a human user. The point is not novelty. It is giving agents a governed path to deploys and config instead of pasting raw SSH into a chat window.&lt;/p&gt;

&lt;p&gt;In-app AI assistant&lt;/p&gt;

&lt;p&gt;There is also an in-app assistant that uses your own OpenAI or Anthropic keys, shares the MCP tool surface, and expects approval before mutating production. That is useful for "how do I wire this domain" questions and for proposed actions you can accept or reject in the UI.&lt;/p&gt;

&lt;p&gt;Pricing shape: $3 per project Cloud, unlimited seats&lt;/p&gt;

&lt;p&gt;Peon Cloud is priced per project (published at $3/month or $30/year), with unlimited servers and unlimited team members on that model. Self-hosting the control plane is free and open source. Apps still run on infrastructure you bring. The intent is predictable cost that tracks how you organize products, not seat count or every extra connected box.&lt;/p&gt;

&lt;p&gt;For a side-by-side feature view maintained by the Peon team, see &lt;a href="https://peon.sh/compare/peon-vs-coolify" rel="noopener noreferrer"&gt;Peon vs Coolify&lt;/a&gt;. Treat any comparison table as a starting point and verify current plan details on both sites before you buy.&lt;/p&gt;

&lt;p&gt;Who should pick which&lt;/p&gt;

&lt;p&gt;Lean Coolify if you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The larger community gravity and template ecosystem&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A battle-tested Docker PaaS for personal projects or a small shop that mostly needs deploys, SSL, and one-click services&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To follow abundant third-party guides and stay close to a widely documented stack&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lean Peon if you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Workspace and project RBAC as a first-class concern&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Audit logs without waiting for an enterprise conversation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;MCP and an in-app assistant wired into the same permissions model&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Flat per-project Cloud pricing with unlimited seats, or a free self-hosted control plane with those team features included&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A path that feels closer to "platform for a growing engineering team" than "excellent solo Docker dashboard"&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many people will be happy on either. The wrong choice is usually picking on vibes, then discovering you needed project isolation or that you did not need AI agent hooks at all.&lt;/p&gt;

&lt;p&gt;Quick start (without the sales pitch)&lt;/p&gt;

&lt;p&gt;If you try Peon, the boring successful path is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Bring a Linux server with SSH and Docker (a small Hetzner or DigitalOcean VPS is fine for experiments).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use Peon Cloud or self-host the control plane.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Connect the server, link a Git source, create a project and service, deploy.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The official walkthroughs live in the &lt;a href="https://peon.sh/docs" rel="noopener noreferrer"&gt;Peon docs&lt;/a&gt; (introduction, self-hosting, first deployment, roles). The code is on &lt;a href="https://github.com/Peon-sh/Peon" rel="noopener noreferrer"&gt;GitHub at Peon-sh/Peon&lt;/a&gt; if you want to read it before you install anything.&lt;/p&gt;

&lt;p&gt;For Coolify, start from their site and docs the same way: one server, one app, prove the loop, then add teammates.&lt;/p&gt;

&lt;p&gt;Honest caveats&lt;/p&gt;

&lt;p&gt;Self-hosted PaaS removes platform markup. It does not remove infrastructure cost or ops ownership.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You still pay for the VPS (or several). A $3 control plane or a free self-hosted panel does not include CPU, RAM, disk, or bandwidth.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You own failure modes. Disk full, bad Docker upgrade, mis-pointed DNS, and backup restore drills are yours whether the UI is pretty or not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Migration is work. Moving from Vercel or from Coolify to Peon (or the reverse) is incremental at best: non-prod first, same Git repo, then cut over when you trust the path.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AI features need keys and judgment. MCP and chat assistants are powerful; treat production mutations like you would treat a junior with root, with approvals on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Community size differs. Coolify currently has more ambient internet knowledge around it. Peon is newer in that social sense; weigh docs quality and your willingness to file issues.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simple decision rule&lt;/p&gt;

&lt;p&gt;If your main job is "get containers on my VPS with git push and HTTPS," both Coolify and Peon can do that job. Prefer Coolify when community breadth and one-click catalog depth matter most. Prefer Peon when project isolation, auditability, and governed AI/agent workflows are part of how your team already works, or how you expect to work in 2026.&lt;/p&gt;

&lt;p&gt;Either way, keep the apps on servers you can SSH into, keep backups off-box, and pick the panel your future teammates will not hate. That is the whole self-hosted PaaS bet in one sentence.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Deploy your apps on your server in clicks</title>
      <dc:creator>Prince</dc:creator>
      <pubDate>Wed, 26 Aug 2026 07:20:51 +0000</pubDate>
      <link>https://dev.to/offpage_prince/deploy-your-appson-your-server-in-clicks-4fdn</link>
      <guid>https://dev.to/offpage_prince/deploy-your-appson-your-server-in-clicks-4fdn</guid>
      <description>&lt;p&gt;Peon is an open-source application deployment platform you can run yourself: a practical Vercel alternative, and a stand-in for Heroku, Netlify and Railway, when you want to push websites, databases, apps and one-click templates onto hardware you control.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Startup Boilerplate: What It Is and How to Pick One</title>
      <dc:creator>Prince</dc:creator>
      <pubDate>Fri, 17 Jul 2026 06:24:21 +0000</pubDate>
      <link>https://dev.to/offpage_prince/startup-boilerplate-what-it-is-and-how-to-pick-one-5aj0</link>
      <guid>https://dev.to/offpage_prince/startup-boilerplate-what-it-is-and-how-to-pick-one-5aj0</guid>
      <description>&lt;p&gt;A startup boilerplate is essentially a pre-written codebase you clone or purchase to avoid redoing everything from scratch each time you launch a company. User registration, payment gateway integration, database, and emails – you will get all of them done for you. This way you can focus on the code that actually brings something special to your startup.&lt;/p&gt;

&lt;p&gt;The term itself comes from journalism and newspaper production. In order to reduce costs, newspapers used standardised metal plates to print articles and stories that would run the same way in every issue. Software adopted the term for similar reasons – code that is written once and reused instead of being rewritten each time.&lt;/p&gt;

&lt;p&gt;What a Startup Boilerplate Actually Includes&lt;br&gt;
Depending on the particular boilerplate you use, its feature set may vary. However, some common features include:&lt;/p&gt;

&lt;p&gt;User sign up, login, and password reset&lt;/p&gt;

&lt;p&gt;Payment processing, usually through Stripe&lt;/p&gt;

&lt;p&gt;A database already connected and structured&lt;/p&gt;

&lt;p&gt;Basic UI components for your dashboard and landing page&lt;/p&gt;

&lt;p&gt;Deployment configuration so you can ship without guessing&lt;/p&gt;

&lt;p&gt;More elaborate versions have team accounts, roles and permissions, email templates, file uploads. The more there is included in the boilerplate, the less you'll have to develop yourself. However, it also means that more decisions were made for you by default, which is always a compromise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fye14byc0lzid2my7hxyl.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%2Fye14byc0lzid2my7hxyl.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why It's More Important for Startups than for Enterprises&lt;br&gt;
While an enterprise could afford a whole month for setting things up, a startup could not. Every week invested in building authentication screens and Stripe webhooks is a week that could be used for interacting with customers and adding that extra feature that would make people actually pay for it.&lt;/p&gt;

&lt;p&gt;The founders who miss this math will end up developing the same authentication flow that every other startup developed in that year. This work adds absolutely no value to your business. The only person for whom it does have value is a creator of the boilerplate you did not use.&lt;/p&gt;

&lt;p&gt;Free Boilerplate vs Paid Boilerplate&lt;br&gt;
And now we get to the point where you actually have to make a choice.&lt;/p&gt;

&lt;p&gt;While free boilerplates offer access to full codebase without license fees, paid boilerplates allow you to leverage someone else's work and expertise on that. In other words, you can see every single line of code for free boilerplate but you won't have much support after the initial setup. If something goes wrong – nobody will explain you what has happened, especially if the developer behind the boilerplate project is inactive at the moment.&lt;/p&gt;

&lt;p&gt;Paid boilerplates usually cost you once or monthly and come with additional functionality pre-installed – multi-tenancy, admin panels, billing flows. Also you pay for someone else's time spent on solving those problems.&lt;/p&gt;

&lt;p&gt;How Do You Actually Choose One?&lt;br&gt;
4 questions, in order of importance:&lt;/p&gt;

&lt;p&gt;What do you need your product to actually do on day one? Don't overpay for multi-tenancy if you're writing a single-user app. Don't underpay for it if you're writing something for teams.&lt;/p&gt;

&lt;p&gt;What stack are you familiar with? A boilerplate on a framework you know saves you a lot more time than one on a framework you'll be learning in a month.&lt;/p&gt;

&lt;p&gt;Is it maintained? Check the date of the last update and the changelog. An unmaintained boilerplate, even if its demo looked nice, is an unnecessary risk to take.&lt;/p&gt;

&lt;p&gt;Can you look into the code yourself or do you have to trust reviews? Open-source boilerplates give you the ability to evaluate their quality. With a closed-source one, you have to trust the authors instead.&lt;/p&gt;

&lt;p&gt;A Word of Caution on AI-Generated Boilerplate&lt;br&gt;
Nowadays, it is possible to generate a working application out of a simple English description using an AI tool. And while it's incredibly fast and useful for prototyping, research up to 2026 points at one drawback – the AI-generated code tends to accumulate more technical debt per line than a professional developer's code.&lt;/p&gt;

&lt;p&gt;And that is because there is no architectural review done to AI-generated applications, and no maintenance of any kind. While a boilerplate made by a human and maintained by a human has passed all the tests needed in the real world environment multiple times.&lt;/p&gt;

&lt;p&gt;Startup Boilerplate Comparison: What the Top Options Actually Include&lt;br&gt;
Talk is cheap. Here is what real startup boilerplates include, based on their own documentation and pricing pages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk8z107uq609xe4cla107.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%2Fk8z107uq609xe4cla107.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
A few honest notes on that table, since a comparison only earns trust if it admits tradeoffs.&lt;/p&gt;

&lt;p&gt;Kostra's authentication is done internally. This allows avoiding the per-user fees of using some third-party auth provider and does not require you to depend on another company's uptime. However, the disadvantage here is that all the security is now on you.&lt;/p&gt;

&lt;p&gt;ShipFast's advantage is in its affordable price and large community of users. The product does not offer multi-tenancy and admin panel, so in case of development for companies, these features should be added.&lt;/p&gt;

&lt;p&gt;Both MakerKit and Supastarter are more expensive options than the others on the list but also deserve their price because of having a more complicated and advanced multi-tenancy solution. Moreover, both frameworks have already been used by many projects, which means that their architecture should be studied before work.&lt;/p&gt;

&lt;p&gt;Finally, an open-source product is totally free, and you can see every code of it without paying anything. Of course, you will need to install it manually and will have no access to a support hotline in case of any troubles at night.&lt;/p&gt;

&lt;p&gt;All of the products above are great in their own way. They help founders of different kinds of apps to cope with some challenges in development.&lt;/p&gt;

&lt;p&gt;Why Founders Use a Self-Hosted and One-Time Payment Approach&lt;br&gt;
In this section, I will explain why Kostra has been built the way it was designed. In other words, I will tell what is the real reason for the above-mentioned claims of the founder.&lt;/p&gt;

&lt;p&gt;There are no subscription taxes. Some boilerplates in this market have changed their prices to annual ones. In other words, your product becomes a source of constant income for your company.&lt;/p&gt;

&lt;p&gt;No dependency on the third-party auth providers. As soon as you have some login system based on the third party, it is obvious that you become dependent on them. Thus, you inherit all their decisions in terms of price, outages, etc.&lt;/p&gt;

&lt;p&gt;Stack known by developers. Both PostgreSQL and Prisma do not require your development team to learn any new technologies to start working.&lt;/p&gt;

&lt;p&gt;These are the real reasons for choosing this model for the product. Of course, it is not the only way of development, and Kostra may not suit everyone.&lt;/p&gt;

&lt;p&gt;Common Mistakes Founders Make When Picking a Boilerplate&lt;br&gt;
There are some common mistakes I've seen many founders doing while selecting the boilerplate. Let me name them explicitly.&lt;/p&gt;

&lt;p&gt;Choosing it based on marketing, rather than a demo. Marketing page doesn't say anything about the code. Clone the repository, or at least, visit the demo site, before buying the tool.&lt;/p&gt;

&lt;p&gt;Not looking into the maintenance schedule. Visit the changelog of the product. No updates within eight months? This means you will need to maintain the boilerplate yourself in the nearest future.&lt;/p&gt;

&lt;p&gt;Overbuying based on current needs. Multi-tenancy, role-based access control, and audit logging is great for a B2B application aimed at enterprise clients. It's absolutely useless if you're trying to validate your basic B2C application.&lt;/p&gt;

&lt;p&gt;Underbuying and then hitting a wall in the third month. Just the opposite situation. Basic, single-tenant boilerplate will work perfectly for fast prototyping, but the first request for another user's account in your product will force you to do the refactoring yourself.&lt;/p&gt;

&lt;p&gt;Forgetting about licensing terms. There are boilerplates that limit your usage to one project only. And there are tools that can be used for any number of projects. If you're an agency or a serial entrepreneur, it's a difference in the price.&lt;/p&gt;

&lt;p&gt;How to Actually Test a Boilerplate Before You Commit&lt;br&gt;
Here is a list of actions that will tell you more about the product than general advices about how to do research.&lt;/p&gt;

&lt;p&gt;Clone it and run the code locally. Fifteen minutes will give you much more information than an hour of reading about the boilerplate.&lt;/p&gt;

&lt;p&gt;Study the authentication code. It's usually the most critical part of the boilerplate because of security reasons. Unclear code here means a warning sign.&lt;/p&gt;

&lt;p&gt;Visit the Stripe integration and check if it handles all edge cases. Do they process failed transactions, subscription cancellations, and plan upgrades? Or do they handle only the happy case of the purchase?&lt;/p&gt;

&lt;p&gt;Visit their community/support channel. One-day reply will show you that someone still supports this project. Lack of replies says the opposite.&lt;/p&gt;

&lt;p&gt;See what kind of applications were built using it. Every boilerplate provides the list of sample projects. Go to several of them and see if they work.&lt;/p&gt;

&lt;p&gt;Signs a Boilerplate Is Overselling Itself&lt;br&gt;
All marketing pages for boilerplates look the same. "Shipped in days, not months." "Saved 40 hours." Some of that is accurate. Some of that is pure copywriting working harder than code.&lt;/p&gt;

&lt;p&gt;A few red flags worth looking out for that will help you discern between honest and misleading boilerplates: &lt;/p&gt;

&lt;p&gt;List of features that don't show any code. If the marketing page says it does some fancy things, but never once demonstrates how, there is a reason. A decent boilerplate maker shows the code behind their claims.&lt;/p&gt;

&lt;p&gt;Testimonials that have no product. Saying "this saved me weeks" is nice, but you should be able to find the actual product that used it. Find boilers that name the product they made.&lt;/p&gt;

&lt;p&gt;Ambiguous pricing pages. If you can't find the exact one-off or subscription price without writing an email to the company, then that is unnecessary friction. Good boilerplate marketing pages, either paid or free, will tell you the price on the first screen.&lt;/p&gt;

&lt;p&gt;Lack of changelog/roadmap. An active boilerplate maker releases regular changes and shares updates publicly. Lack of those is an indicator of a project becoming inactive, although its marketing pages might not reflect that.&lt;/p&gt;

&lt;p&gt;None of this guarantees that a slick-looking marketing page is not a good boilerplate, but it proves that slickness is not proof of quality. Code, changelog, and real-world examples prove that.&lt;/p&gt;

&lt;p&gt;What startup boilerplate does is bet on saving developers time on non-profit-generating features. Authentication, billing, and initialization are common problems for almost all businesses. Someone has found an optimal solution for those common problems multiple times.&lt;/p&gt;

&lt;p&gt;The boilerplate I would recommend to founders who want self-hosting, a one-time fee, a Postgres and Prisma stack they already know is Kostra. If this isn't your cup of tea and you lean towards another solution from the list above, it's still good news.&lt;/p&gt;

&lt;p&gt;What is the difference between a startup boilerplate and a SaaS starter kit?&lt;/p&gt;

&lt;p&gt;No difference at all! The terms are equivalent. Both mean a pre-built codebase with auth, payments and database integrated so that you don't have to spend time setting things up.&lt;/p&gt;

&lt;p&gt;Do I own the code in a startup boilerplate?&lt;/p&gt;

&lt;p&gt;Yes. Most of the boilerplates provide you with the whole source code. You can modify it, change it and deploy it as your own application. What you buy is a base which you will be able to transform into your product.&lt;/p&gt;

&lt;p&gt;Is it possible to use a boilerplate for more than one project?&lt;/p&gt;

&lt;p&gt;This depends on the license type. The majority of the boilerplates offer per-developer licenses, meaning that once you purchase a license, you may use it for all projects you develop. Team licenses might cost more but allow for sharing the codebase across the development team.&lt;/p&gt;

&lt;p&gt;How much time does it take to start working on a boilerplate?&lt;/p&gt;

&lt;p&gt;Assuming that you are talking about a paid boilerplate with reasonable documentation, in less than an hour you will be ready to work locally and in one day you will be able to deploy it.&lt;/p&gt;

&lt;p&gt;Are free open source boilerplates sufficient for production?&lt;/p&gt;

&lt;p&gt;Yes, they are. Our free open source boilerplate, BoxyHQ, is completely production-ready for enterprise identity use cases. The only tradeoff would be in documentation quality and support.&lt;/p&gt;

&lt;p&gt;What would happen if Next.js or Stripe would release a new major version?&lt;/p&gt;

&lt;p&gt;In case of paid boilerplates with an active author, the author would usually update his/her boilerplate. Otherwise, you would have to update everything by yourself. This is one of the reasons why people pay for their boilerplates.&lt;/p&gt;

&lt;p&gt;How can I tell if the boilerplate is still maintained?&lt;/p&gt;

&lt;p&gt;The best way is to see the activity in GitHub. If commits were made in the last 30–60 days, then the project is alive. You should also pay attention to the response rate in case of open issues. It means that the author is willing to help but just doesn't make changes often enough.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Prisma vs Drizzle: Which ORM Should You Use in 2026?</title>
      <dc:creator>Prince</dc:creator>
      <pubDate>Mon, 13 Jul 2026 09:20:40 +0000</pubDate>
      <link>https://dev.to/offpage_prince/prisma-vs-drizzle-which-orm-should-you-use-in-2026-bcf</link>
      <guid>https://dev.to/offpage_prince/prisma-vs-drizzle-which-orm-should-you-use-in-2026-bcf</guid>
      <description>&lt;p&gt;Kostra's whole data layer was built using Prisma. However, prior to settling on that solution, we had run real-world migrations and write operations via Prisma and Drizzle on a single PostgreSQL database.&lt;/p&gt;

&lt;p&gt;Here's what the benchmark revealed about the two ORMs, along with the developments since 2026 in each of them that may be helpful in making a decision which one to choose for your projects.&lt;/p&gt;

&lt;p&gt;TL;DR if you're in a rush: Choose Prisma when a mature ecosystem, visual data browser, and SQL abstraction are what you need. Choose Drizzle when you prefer to stick closer to SQL, need a minimal bundle size, or deploy to the edge&lt;/p&gt;

&lt;p&gt;Both solutions are production-ready. There's no wrong choice, just the one that fits your project more.&lt;/p&gt;

&lt;p&gt;What Each ORM Actually Is&lt;br&gt;
Prisma is schema-first. You design your data model in a .prisma file by using Prisma's schema language and after that run prisma generate to get a fully typed client for querying. Prisma Migrate will automatically migrate your schema changes into SQL migrations.&lt;/p&gt;

&lt;p&gt;Drizzle is code-first. You create your tables natively in TypeScript using the query builder which is similar in structure to SQL. There's no separate schema file and no generation step. Change a table definition, save the file, and your types update immediately.&lt;/p&gt;

&lt;p&gt;That single difference, schema file plus generation step versus TypeScript-native with none, is the root of almost every other difference between them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwfuv0t7lxqv0tfl0st8n.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%2Fwfuv0t7lxqv0tfl0st8n.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Schema Design and Developer Experience&lt;br&gt;
The Prisma schema looks almost like normal English. Type definitions, relationships, and constraints are all defined in one declarative code block. The Prisma VSCode plugin further provides features like autocomplete, quick fixes, and definition navigation above this. However, the problem is the generation process. Any changes to the field require a separate prisma generate command for the application to reflect those changes.&lt;/p&gt;

&lt;p&gt;With Drizzle, the generation step is skipped entirely. You can work with your schema directly as a TypeScript module, and types will be updated instantly when you save your file. The catch is reversed as well: table definitions with Drizzle feel more like SQL-DDL than data modelling, which may suit some teams just fine but not others.&lt;/p&gt;

&lt;p&gt;Performance and Bundle Size&lt;br&gt;
Here was where the story changed in 2026. For years, the main problem was the Rust engine for Prisma's queries; it was a big, heavy binary that caused extra cold-start latency when deployed to serverless and edge systems. In contrast, Drizzle's lack of engine layer meant it was always faster and had a much smaller payload.&lt;/p&gt;

&lt;p&gt;Prisma 7, which was launched at the end of 2025, had done away with its Rust engine and adopted a new implementation in TypeScript and WebAssembly. This meant smaller bundles and native edge runtime support was now available. Thus, the gap in cold start performance that made Drizzle such a clear winner was significantly reduced.&lt;/p&gt;

&lt;p&gt;The gap never closed. Drizzle remains the smallest payload, with its bundles measuring in kilobytes compared to megabytes of Prisma bundles. Drizzle also has slightly better cold-start performance than Prisma in benchmarks up until 2026. However, for typical backend systems that run on a persistent server or container and not an edge system, database round-trip times completely overshadow the overheads of ORMs.&lt;/p&gt;

&lt;p&gt;Migrations and Tooling&lt;br&gt;
Prisma Migrate creates SQL migrations automatically out of changes to your schema, while Prisma Studio allows you to browse your data visually without any configuration. This tooling is superior compared to Drizzle's similar offerings, drizzle-kit generate and drizzle-kit migrate. While those are very powerful and easy to use, they sometimes require manual SQL migrations in case of changes that cannot be done automatically, such as renaming columns.&lt;/p&gt;

&lt;p&gt;However, if migration tooling and database graphical user interface are important parts of your development workflow, then Prisma wins in 2026 in that regard. As long as you're okay with manually writing SQL migrations in case of generator inability, Drizzle should not hold you back.&lt;/p&gt;

&lt;p&gt;Ecosystem, Maturity, and Community&lt;br&gt;
Prisma comes with more history and a richer ecosystem - more answers on StackOverflow, more tutorials, more people who have hired someone who knows it. Prisma is the ORM that is included in SvelteKit and Remix in their official documentation, and it is the default suggestion for a fast onboarding of a new team member.&lt;/p&gt;

&lt;p&gt;The adoption of Drizzle quickly accelerated during 2025 and 2026. Drizzle is currently used in production at companies like Replit, Sentry, and Figma; Astro created its own Astro DB based on Drizzle; and the Hono.js framework recommends it as a default ORM. By mid-2026, various reports noted that the weekly downloads of Drizzle from npm were getting closer to Prisma’s and even surpassed them in some reports, although each of the ORMs was counted under a different package name (drizzle-orm versus @prisma/client and prisma CLI). In any case, the general trend seems to be switching from Prisma to Drizzle rather than vice versa.&lt;/p&gt;

&lt;p&gt;One important development to note: PlanetScale employed the core team behind Drizzle in 2026 to develop the ORM. While Drizzle itself is still open-source, it was an important decision regarding sustainability since it addressed the question of where the money for development would come from. Prisma, in turn, is a venture-backed company with a free ORM that helps people adopt other paid tools from their ecosystem such as Prisma Accelerate and Prisma Postgres.&lt;/p&gt;

&lt;p&gt;Comparison Table&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqvk9749cctyhqgjl3ux7.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%2Fqvk9749cctyhqgjl3ux7.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Which One Did We Pick for Kostra, and Why&lt;br&gt;
Kostra is built on top of Prisma using PostgreSQL. There are three particular reasons why we chose it specifically for our boilerplate:&lt;/p&gt;

&lt;p&gt;Prisma Migrate guarantees consistency in all projects based on the boilerplate concerning schema changes. Second, the schema itself is the quickest way for any new developer to grasp the whole data model at once. The fact is significant because Kostra is a boilerplate, meaning others will build upon it. Third, our layered architecture separates the ORM from the rest of the application through a repository pattern, It means that in case Kostra was going to provide an alternative driver such as Drizzle in some future release, there will be changes only in one layer.&lt;/p&gt;

&lt;p&gt;The straightforward bargain: The benefits provided by the smaller package size and quicker warm-up time of Drizzle will be more significant when deploying Kostra for edge environments. It doesn't. Kostra ships with Docker and Docker Compose for running on their own and in containers, which becomes a valid choice for which the “cold start” factor is not much of an issue anymore. If your own product is built with the edge in mind, then the logic behind that may lead you to Drizzle even within the Prisma boilerplate code.&lt;/p&gt;

&lt;p&gt;Kostra isn't the only Next.js SaaS boilerplate built this way. The official Vercel starter and the ixartz SaaS Boilerplate both ship with Drizzle instead, which is worth knowing if ORM choice is a hard requirement for your stack.&lt;/p&gt;

&lt;p&gt;How to Choose: A Simple Framework&lt;br&gt;
Pick Prisma if:&lt;/p&gt;

&lt;p&gt;Your developers lack comfort using SQL and you need abstraction that helps write the correct query&lt;/p&gt;

&lt;p&gt;You need Prisma Studio for visualization of your database without extra configuration steps&lt;/p&gt;

&lt;p&gt;You are looking to hire people who are proficient with the ORM of choice&lt;/p&gt;

&lt;p&gt;Your application runs in a persistent environment like server/container where there is no such thing as cold starts&lt;/p&gt;

&lt;p&gt;Pick Drizzle if:&lt;/p&gt;

&lt;p&gt;Your developers prefer working with SQL and you don’t want the ORM to get in their way&lt;/p&gt;

&lt;p&gt;You deploy your app to an edge runtime like Vercel Edge Functions or Cloudflare Workers where bundle size matters&lt;/p&gt;

&lt;p&gt;You don’t want any generation step in your development workflow&lt;/p&gt;

&lt;p&gt;You are running your database on Neon, Turso or PlanetScale all of which provide excellent support for Drizzle driver&lt;/p&gt;

&lt;p&gt;If none of that settles it, look at your database requirements against your actual stack before the ORM choice, not after. The ORM should follow from your deployment target and team's SQL comfort, not the other way around.&lt;/p&gt;

&lt;p&gt;Frequently Asked Questions&lt;br&gt;
Is Drizzle faster than Prisma? &lt;br&gt;
In terms of bare ORM overhead and startup time, yes, Drizzle is still faster because it does not have an extra layer of query engine to start up. Prisma 7 brought down that margin substantially by switching from Rust engine to a more lightweight TypeScript and WASM engine. For applications that use server-side persistence rather than serverless functions, the difference becomes negligible, as the execution time of the database queries always will be dominant.&lt;/p&gt;

&lt;p&gt;Can I switch from Prisma to Drizzle later, or the other way around?&lt;br&gt;
Yes. It is possible to switch between Drizzle and Prisma after a while? Yes, but allocate some time for it. Ultimately, both ORMs produce the same SQL migrations to the same PostgreSQL tables, so your data will be unlocked. The thing that consumes time is rewriting every query in the application layer to the different ORM API. It becomes easier when teams follow a repository pattern and have all database queries under one single internal interface.&lt;/p&gt;

&lt;p&gt;Does Prisma work on the edge now? &lt;br&gt;
Yes, since Prisma 7, which came out later in 2025. Prisma versions before then required a Rust binary that did not support edge runtimes such as Cloudflare Workers, making this the clearest strength of Drizzle for several years until Prisma 7 introduced its TypeScript/WASM engine. However, Drizzle’s compactness remains an advantage where size is critical.&lt;/p&gt;

&lt;p&gt;Which ORM should a solo founder pick for a new SaaS project? &lt;br&gt;
If you haven’t used either before, then you should start with Prisma’s schema file and auto-generated client because of how much easier it is to use and because there are many more tutorials and SO answers. If you know how to use SQL well, then Drizzle will not waste your time.&lt;/p&gt;

&lt;p&gt;Do Prisma and Drizzle both support PostgreSQL? &lt;br&gt;
Absolutely, as both are fully compatible with PostgreSQL, MySQL, and SQLite. Prisma also supports SQL Server and MongoDB, something that Drizzle doesn’t support. If your project requires you to use MongoDB, then the decision becomes pretty clear on its own.&lt;/p&gt;

&lt;p&gt;Bottom Line&lt;br&gt;
Both ORMs are truly production-ready in 2026, and the difference between them has been narrowed from both sides: Prisma became lighter and edge-enabled, and Drizzle became more feature-rich. The better choice is based on how comfortable your team is with SQL and where your application really runs, not which ORM comes out ahead in the benchmarks.&lt;/p&gt;

&lt;p&gt;Kostra Ships with Prisma and PostgreSQL baked in to a layered architecture right out of the box, alongside auth, Stripe billing, and file storage, so you’re not cobbling any of that together yourself. Compare it against other Next.js boilerplates to see how the rest of the stack holds up.&lt;/p&gt;

</description>
      <category>prisma</category>
      <category>drizzle</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Vercel Starter Kit: Best Templates to Deploy Your SaaS Fast</title>
      <dc:creator>Prince</dc:creator>
      <pubDate>Fri, 10 Jul 2026 11:55:33 +0000</pubDate>
      <link>https://dev.to/offpage_prince/vercel-starter-kit-best-templates-to-deploy-your-saas-fast-3l6k</link>
      <guid>https://dev.to/offpage_prince/vercel-starter-kit-best-templates-to-deploy-your-saas-fast-3l6k</guid>
      <description>&lt;p&gt;I looked at Vercel's template page myself and will give you exactly what it includes, be it free or not.&lt;/p&gt;

&lt;p&gt;Vercel operates the marketplace of templates located on vercel.com/templates. One can deploy any template there in one click. Some of them are officially supported builds from Vercel, the others are community contributions. The gap between them in terms of quality is very big, so one has to know what is what before deploying.&lt;/p&gt;

&lt;p&gt;What Are the Actual Features Included in Official Vercel Starter Kits&lt;br&gt;
Vercel develops the "Next.js SaaS Starter" template itself. It has more than 15,500 stars on Github. It includes:&lt;/p&gt;

&lt;p&gt;JWT authentication manually implemented, without using any third-party provider for that purpose&lt;/p&gt;

&lt;p&gt;Stripe Checkout integration for subscription billing&lt;/p&gt;

&lt;p&gt;Access control based on role with Owner and Member roles&lt;/p&gt;

&lt;p&gt;Dashboard for managing teams&lt;/p&gt;

&lt;p&gt;Activity logging&lt;/p&gt;

&lt;p&gt;Middleware for protecting routes&lt;/p&gt;

&lt;p&gt;Underlying tech stack includes Next.js, PostgreSQL, Drizzle ORM, shadcn/ui and Tailwind CSS. These technologies form the stack recommended by Vercel and therefore they stay pretty close to the recommendations of the Next.js team.&lt;/p&gt;

&lt;p&gt;There is one more template provided by Vercel, called the "Minimal Next.js SaaS Website Starter". As the name implies, it includes only Tailwind and dark theme support and nothing more. It can be used for building landing pages, but not for whole SaaS application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpute9dtf2mgg0wo8nqiw.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%2Fpute9dtf2mgg0wo8nqiw.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What Is Missing in Official Templates&lt;br&gt;
This part is often omitted in reviews of Vercel starter kits, but I will discuss it because Vercel's official SaaS starter clearly mentions this on its own page saying that it is a learning resource and not a production toolkit with other paid and more featureful alternatives available in the community.&lt;/p&gt;

&lt;p&gt;It is an honest thing to say, so take it into account when picking up the starter. The official Vercel SaaS starter template does not provide:&lt;/p&gt;

&lt;p&gt;Multi-tenant functionality or organization management, only basic Owner/Member roles&lt;/p&gt;

&lt;p&gt;E-mail sending either transactional or bulk&lt;/p&gt;

&lt;p&gt;File storage and uploads&lt;/p&gt;

&lt;p&gt;CMS/blog module&lt;/p&gt;

&lt;p&gt;Additional admin tools except those basic&lt;/p&gt;

&lt;p&gt;If your SaaS requires any of these features then you will have to build them yourself on top of the starter.&lt;/p&gt;

&lt;p&gt;How to Actually Deploy the Official Vercel SaaS Starter&lt;br&gt;
And now comes the real step-by-step guide on how to deploy the starter from Vercel. It is different from the marketing version.&lt;/p&gt;

&lt;p&gt;Clone the template repository. Deploy on Vercel templates page or clone the repository and use it locally first.&lt;/p&gt;

&lt;p&gt;Set up your database. The starter relies on Postgres with Drizzle ORM. So you will need the connection string from Vercel's own Postgres offering, Neon, or Supabase.&lt;/p&gt;

&lt;p&gt;Configure Stripe locally. Stripe webhooks will be listened locally using the Stripe CLI for testing purposes until deployment.&lt;/p&gt;

&lt;p&gt;Create a production webhook. When everything is ready for deployment, you should go to your Stripe dashboard and create a production webhook pointing to the URL of your API route. For example, it may be something like yourdomain.com/api/stripe/webhook. Choose the events you want to process there. Usually, it is checkout.session.completed and customer.subscription.updated.&lt;/p&gt;

&lt;p&gt;Deploy to GitHub. Just push your code and link the repository to Vercel.&lt;/p&gt;

&lt;p&gt;Test everything. Register a test user, go through a test subscription process in Stripe's test mode, and ensure that the webhook correctly updates your database.&lt;/p&gt;

&lt;p&gt;All these steps are quite easy if you are familiar with them, but for beginners the process of creating a Stripe webhook will take some real time. This is the place where people usually get stuck, since local testing is different from production behavior.&lt;/p&gt;

&lt;p&gt;Community Templates in Vercel Marketplace&lt;br&gt;
Except for the official starter there are dozens of community templates listed in the Vercel marketplace. Let's consider three of them in more details.&lt;/p&gt;

&lt;p&gt;Saasfly&lt;br&gt;
It is a full-fledged open-source SaaS solution that is more featureful than the minimal official starter. This is a template for developing the actual product and not just a reference implementation, so it contains more elements that will have to be built from scratch otherwise.&lt;/p&gt;

&lt;p&gt;Stripe &amp;amp; Supabase SaaS Starter Kit&lt;br&gt;
This template uses Supabase for the authentication layer and Drizzle ORM with Postgres for database and Stripe for subscriptions. The authentication layer supports OAuth and password reset out-of-the-box.&lt;/p&gt;

&lt;p&gt;Setting up Google or GitHub OAuth will require creating credentials in Google Cloud and GitHub developer settings and adding client ID and client secret to the environment variables. This template is perfect for those who already use Supabase as the base for their infrastructure.&lt;/p&gt;

&lt;p&gt;Auth0 SaaS Starter Kit&lt;br&gt;
This template is designed for B2B SaaS solutions that require the advanced authentication layer provided by Auth0 by Okta instead of the manually built system. The tradeoff here is that while setting up the starter becomes slightly harder, the authentication layer gains such enterprise-grade features as SSO, which become important when selling the product to the companies and not to the individuals.&lt;/p&gt;

&lt;p&gt;Community templates may differ greatly in terms of freshness and maintenance. Some of them are one-man projects and were abandoned some months ago. Check the last commit date on GitHub before starting development based on it. A template with no commits during half a year is a risky choice, even if the demo looks great.&lt;/p&gt;

&lt;p&gt;Vercel Starter Kit Comparison Table&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2z0cxu8jgkyrfsar58i4.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%2F2z0cxu8jgkyrfsar58i4.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Free templates will help you get started quickly. The price you pay for that is the need to implement additional components from scratch after some time, when you will require team accounts, file storage or e-mails.&lt;/p&gt;

&lt;p&gt;Common Questions About Vercel Starter Kits&lt;br&gt;
Are there open-source SaaS templates that combine Prisma, Postgres, and Stripe under a permissive license? Most of Vercel's own templates use Drizzle ORM, not Prisma. If Prisma specifically matters to your stack, you'll find more Prisma-based options outside Vercel's official marketplace, including several free GitHub boilerplates and paid kits like Kostra.&lt;/p&gt;

&lt;p&gt;What are the most active Next.js SaaS starter repos right now? Activity changes fast enough that "most active" is a moving target. The safest check is the GitHub commit history and issue response time on whichever repo you're considering, not a ranked list someone wrote months ago.&lt;/p&gt;

&lt;p&gt;Can I use any database with the official Vercel starter? Yes. The template is built around Postgres, so any Postgres provider works, including Vercel's own offering, Neon, or Supabase's Postgres service.&lt;/p&gt;

&lt;p&gt;Vercel Starter Kit vs a Full SaaS Boilerplate&lt;br&gt;
This is the real decision. A Vercel starter kit gets you a working pattern fast, usually free. A paid SaaS boilerplate like MakerKit, ShipFast, Supastarter, or Kostra gets you more of the business logic already built: multi-tenancy, email, file storage, and ongoing updates from a maintainer whose job is keeping that codebase current.&lt;/p&gt;

&lt;p&gt;Neither is wrong. It depends on how much you want handed to you versus how much you want to build yourself.&lt;/p&gt;

&lt;p&gt;Pick a free Vercel template if:&lt;/p&gt;

&lt;p&gt;You're learning the Next.js App Router patterns&lt;/p&gt;

&lt;p&gt;Your SaaS is simple, no teams, no complex billing&lt;/p&gt;

&lt;p&gt;You have time to build auth, email, and storage yourself&lt;/p&gt;

&lt;p&gt;Pick a paid boilerplate if:&lt;/p&gt;

&lt;p&gt;You need multi-tenancy or team accounts on day one&lt;/p&gt;

&lt;p&gt;You want email, file storage, and billing already wired together&lt;/p&gt;

&lt;p&gt;You'd rather pay once than spend weeks on setup&lt;/p&gt;

&lt;p&gt;Where This Leaves You&lt;br&gt;
Vercel's own starter is a solid, honest reference. It even tells you when you've outgrown it. If you hit that point, that's exactly the gap a full boilerplate like Kostra is built to close, with auth, Stripe billing, and file storage already connected in one Next.js codebase.&lt;/p&gt;

&lt;p&gt;Start free. Upgrade when the missing pieces start costing you more time than a boilerplate would.&lt;/p&gt;

&lt;p&gt;Frequently Asked Questions&lt;br&gt;
Are Vercel starter kits free?&lt;br&gt;
Yes. All official Vercel templates are free and open source. You pay for Vercel hosting separately, but most small projects can fit the Vercel free tier.&lt;/p&gt;

&lt;p&gt;What is the difference between Vercel template and Vercel starter kit?&lt;br&gt;
Vercel uses both terms interchangeably on the same page. Starter kits usually contain more configuration (auth, database, payments) than basic templates, which are mostly UI shells. But the names are not consistent, so you should look at the feature list.&lt;/p&gt;

&lt;p&gt;Does the Vercel SaaS starter contain a database?&lt;br&gt;
Yes. The official Next.js SaaS Starter is built around Postgres via Drizzle ORM. It isn't locked to a single provider — Vercel's own Postgres offering, Neon, Supabase, or any other Postgres host all work.&lt;/p&gt;

&lt;p&gt;Can I deploy a Vercel starter kit to a non-Vercel host?&lt;br&gt;
Yes. The code is just standard Next.js. You can deploy it to Railway, Render, Fly.io or any other host that runs Node.js. One-click deployment button is specific to Vercel only, but the codebase is fully portable.&lt;/p&gt;

&lt;p&gt;Is the Vercel Platforms Starter Kit suitable for regular SaaS?&lt;br&gt;
No. It is designed for multi-tenant applications, where users need subdomains or custom domains. For a regular single-tenant SaaS application, Platforms kit adds unnecessary complexity.&lt;/p&gt;

&lt;p&gt;What is the best Vercel starter kit for a solo founder deploying a SaaS?&lt;br&gt;
For a simple product, the Next.js SaaS Starter will be a perfect fit to implement authentication and Stripe billing in the App Router. For a more complete production-ready solution, a boilerplate will cover the gaps left by Vercel starter.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>React SaaS Boilerplate: Best Options in 2026</title>
      <dc:creator>Prince</dc:creator>
      <pubDate>Thu, 09 Jul 2026 10:39:54 +0000</pubDate>
      <link>https://dev.to/offpage_prince/react-saas-boilerplate-best-options-in-2026-31ml</link>
      <guid>https://dev.to/offpage_prince/react-saas-boilerplate-best-options-in-2026-31ml</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo7c1o4xc1p9uxu307l7x.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%2Fo7c1o4xc1p9uxu307l7x.png" alt=" " width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've built SaaS products from scratch and from boilerplates. The from-scratch approach always costs the same thing: three to six weeks wiring auth, Stripe, email, and a database layer before you even start building product features.&lt;/p&gt;

&lt;p&gt;A good React SaaS boilerplate cuts that down to a day or two. A bad one costs you three weeks, but spent in debugging someone else's hacks.&lt;/p&gt;

&lt;p&gt;This article talks about the best React SaaS boilerplates in 2026. Almost all of them use Next.js under the hood, because Next.js is where all the production React happens nowadays. I'll talk about what each boilerplate offers, what it costs, and who it suits. No affiliate links. Just straight-up trade-offs.&lt;/p&gt;

&lt;p&gt;What a React SaaS Boilerplate Should Have in 2026&lt;br&gt;
First, here's how to immediately filter boilerplates out. The bar has been raised. In 2026, if a boilerplate offers just auth and Stripe checkout forget it, you're not saving much. &lt;/p&gt;

&lt;p&gt;Here's what needs to come bundled with a boilerplate today: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Authentication with multiple methods. email and password, Google OAuth, magic links. HTTP-only JWT cookie based session handling as default&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Payments beyond checkout. Subscriptions, one-time payments, customer portal. With webhook handling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A real database layer. Typed ORMs such as Prisma or Drizzle running on PostgreSQL. No JSON files. No "bring your own".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Role-based access control. If you're building a B2B app, you'll need roles right away. Trying to implement RBAC retroactively is extremely painful.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Transactional email. Password reset emails and invoices, integrated into a dedicated service such as Resend or AWS SES.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;File storage. Uploading files to S3 or R2, abstracted in a reusable way.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A maintainable structure. Layered architecture beats dumping everything in a bunch of files. You'll be living in this codebase for years.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And there's one hidden metric: the date of the last commit. The Next.js ecosystem moves quickly. If you see a boilerplate that hasn't seen an update in 8 months   it will have a hard time keeping up with every dependency you have to include. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8phojxopk48hpyocf21.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%2Fj8phojxopk48hpyocf21.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Best React SaaS Boilerplates in 2026&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Kostra - Best overall for production-grade architecture
Price: $150 one-time | Stack: Next.js App Router, Prisma + PostgreSQL, Stripe, Tailwind CSS&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Full disclosure: Kostra is our product. But the reason why we made it is the same reason why it stands above everyone else. Most boilerplates are demos wrapped in price tags. They work until your first real feature request comes along and then fall apart architecturally.&lt;/p&gt;

&lt;p&gt;Kostra is architected as the kind of software a team would maintain over years:&lt;/p&gt;

&lt;p&gt;Layered architecture. Services and repositories are decoupled. The business logic isn't mixed with the route handlers. Swapping a provider or adding a new feature only requires changing a single layer, not twelve files.&lt;/p&gt;

&lt;p&gt;Auth implemented properly. HTTP-only JWT cookies (not localStorage tokens), Google OAuth, and magic links included.&lt;/p&gt;

&lt;p&gt;Stripe covered in its entirety. Subscriptions, one-time payments, customer portal, webhook handling.&lt;/p&gt;

&lt;p&gt;Drivers configured. Storage works out of the box with AWS S3 or Cloudflare R2. Email with Resend or AWS SES. Change providers with configs, not rewrite.&lt;/p&gt;

&lt;p&gt;Centralized role-based access control (RBAC). Roles and permissions enforced in a consistent way.&lt;/p&gt;

&lt;p&gt;Everything else in the stack: Docker configuration, Zustand as a state manager, Atomic Design components based on Tailwind CSS, blogging system, and an admin dashboard.&lt;/p&gt;

&lt;p&gt;For: Developers and founders who intend to work with this codebase in year two. If you want a foundation, not a demo, this is the one.&lt;/p&gt;

&lt;p&gt;Against: Kostra is opinionated. If you want to remove Prisma and replace it with Drizzle, or JWT with a hosted auth service, you're swimming against the tide.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ShipFast - Best for weekend launches
Cost: ~$199-$299 | Stack: Next.js, MongoDB or Supabase, Stripe or Lemon Squeezy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;ShipFast is the most famous name in this space, and it's honestly marketed. Its goal is shipping in a weekend. It's quick to set up, lightweight, and backed by a large community of indie hackers.&lt;/p&gt;

&lt;p&gt;The downside is depth. ShipFast optimizes for time to revenue, not maintainability. There's no multi-tenancy, no RBAC, no layered architecture, which is fine if all you need is a landing page with a checkout. Once the product becomes more complex it gets problematic.&lt;/p&gt;

&lt;p&gt;Best for: Indie hackers validating ideas quickly.&lt;/p&gt;

&lt;p&gt;Skip if: You're building B2B or anything with teams and roles.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;MakerKit - Best for multi-tenant B2B
Cost: ~$299+ | Stack: Next.js, Supabase, React 19, TypeScript, Tailwind&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;MakerKit is one of the most comprehensive kits for a team-based SaaS application. It comes with organization support, invitation handling, tenant billing, and role management right off the bat. Documentation is very strong and it's actively maintained.&lt;/p&gt;

&lt;p&gt;The cost is complexity. MakerKit packs quite a bit of abstraction and has a tight coupling with Supabase. If Supabase is what you're planning to use, that's great. Otherwise, migrating from it could be a challenge.&lt;/p&gt;

&lt;p&gt;Best for: B2B SaaS with teams and workspaces from day one.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ixartz SaaS Boilerplate - Best free open source option
Cost: Free (Pro/Max tiers) | Stack: Next.js, Clerk, Drizzle ORM, Shadcn UI&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most starred SaaS boilerplate on GitHub for a good reason. Comes with auth via Clerk, multi-tenancy, roles and permissions, i18n, testing suite with Vitest and Playwright, and dependencies getting updates once a month.&lt;/p&gt;

&lt;p&gt;There are two things to consider. First, Clerk is a hosted authentication service that charges per usage. So, if you're looking for a "free boilerplate", you shouldn't expect it to be cost-free. Second, the most useful features (Stripe integration, self-hosted auth) are tucked behind Pro and Max tiers.&lt;/p&gt;

&lt;p&gt;Best for: Developers who want to read excellent code and aren't bothered by Clerk's pricing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Supastarter - Best for the Supabase ecosystem
Cost: ~$349+ | Stack: Next.js (also Nuxt/SvelteKit), Supabase, TypeScript&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Supastarter comes with auth, billing, i18n, and AI-powered features across multiple frameworks. Quality is high, and updates are frequent. Pricing is at the top of the market, and like with MakerKit, it presumes you're committed to Supabase.&lt;/p&gt;

&lt;p&gt;Best for: Teams who already picked Supabase and want polish and support.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Vercel saas-starter   Best Minimal Official Starter
Price: Free | Stack: Next.js, Postgres, Drizzle, Stripe&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Vercel's own open source starter. It's minimal by design: auth, a Stripe subscription flow, and a dashboard skeleton. Think of it as a reference implementation, not a product foundation. You'll build most of your SaaS yourself, but you'll build it on patterns the Next.js team endorses.&lt;/p&gt;

&lt;p&gt;Best for: Learning current Next.js patterns, or teams that want to own every line.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open SaaS (Wasp) - Best free full-stack alternative
Cost: Free | Stack: React, Node.js, Wasp, Prisma, Stripe&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Free full-stack solution including auth, Stripe, admin dashboard, email and some AI examples. The drawback is Wasp itself: a full-stack framework with a custom config format. It eliminates boilerplate but adds a framework dependency most React developers aren't familiar with.&lt;/p&gt;

&lt;p&gt;Best for: Build zero-budget project with the willingness to use Wasp.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Gravity - Best Non-Next.js Option
Cost: ~$400+ | Stack: Node.js + Express, React (separate client)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Not every React SaaS is built with Next.js. Gravity implements classic separation of concerns: Express API server, React frontend with billing, auth, organizations, and mobile app templates.&lt;/p&gt;

&lt;p&gt;It's mature and battle-tested, with years of updates behind it.&lt;/p&gt;

&lt;p&gt;Best for: Teams who want a separated API and client, or need a React Native mobile app companion.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;T3 Stack - Best Scaffolding for Full Control
Cost: Free | Stack: Next.js, tRPC, Prisma or Drizzle, Tailwind, NextAuth&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;T3 is not a boilerplate itself. It's a type-safe scaffolding that's widely used among React developers for building various types of software. People create their SaaS applications based on it. You get a fully-typed backend-to-front end communication and nothing else: no billing, no multi-tenancy, no RBAC. You implement that yourself.&lt;/p&gt;

&lt;p&gt;Best for: Developers who need full control and don't mind implementing the SaaS part themselves.&lt;/p&gt;

&lt;p&gt;Comparison Table&lt;br&gt;
Comparison table of the best React SaaS boilerplates for 2026, comparing Kostra, ShipFast, MakerKit, ixartz, Supastarter, Vercel Starter, Open SaaS, Gravity, and T3 Stack by price, authentication, payments, multi-tenancy, RBAC, and ideal use cases.&lt;br&gt;
How to Choose the Right One&lt;br&gt;
There are three questions that clear everything up:&lt;/p&gt;

&lt;p&gt;Are you validating or building your SaaS?&lt;/p&gt;

&lt;p&gt;Validating the idea? Speed wins. ShipFast or even free boilerplate will suffice.&lt;/p&gt;

&lt;p&gt;Building something you're going to maintain? Architectural soundness wins. Then this article shows why Kostra and MakerKit are worth their price.&lt;/p&gt;

&lt;p&gt;Do you need teams and roles?&lt;/p&gt;

&lt;p&gt;B2B implies multi-tenancy and RBAC right away. Very few options here can deliver that out-of-the-box. Implementing it retroactively is one of the hardest things to do in a SaaS development process.&lt;/p&gt;

&lt;p&gt;How locked-in you're willing to be?&lt;/p&gt;

&lt;p&gt;Clerk, Supabase, and Wasp are all great tools, but they're also dependencies with their own pricing models and roadmaps. Choosing boilerplates that work with standard building blocks (Prisma, PostgreSQL, JWT, S3) leaves more options open.&lt;/p&gt;

&lt;p&gt;The Real Cost&lt;br&gt;
$150-$350 boilerplate seems pricey compared to a bunch of free options. Not really. Wiring auth, Stripe webhooks, email, storage, RBAC and setting up deployment will take 80-150 hours of work if you do it properly. At any reasonable rate, a paid boilerplate pays off in the first afternoon.&lt;/p&gt;

&lt;p&gt;It's not the initial price that's the real cost. It's picking a boilerplate that has poor architecture and making that decision pay off in month 6 when every addition is fighting the codebase.&lt;/p&gt;

&lt;p&gt;Frequently Asked Questions&lt;br&gt;
What's the difference between React SaaS boilerplate and Next.js SaaS boilerplate in 2026?&lt;br&gt;
Pretty much none. Virtually all React SaaS boilerplates nowadays use Next.js as a standard framework for production React applications. These two terms are interchangeable.&lt;/p&gt;

&lt;p&gt;Are free and open source SaaS boilerplates good enough?&lt;br&gt;
For learning and side projects   yes. But for products with paying customers, make sure that maintenance is regular and paid features are not needed. Plenty of "free" boilerplates charge for payment processing or auth, or require a hosted service with usage pricing.&lt;/p&gt;

&lt;p&gt;How much time does a SaaS boilerplate save?&lt;br&gt;
Three to six weeks of groundwork: auth, billing, email, storage, RBAC and deployment. The more solid the architecture, the more time it saves during maintenance.&lt;/p&gt;

&lt;p&gt;Which SaaS boilerplate is the best for solo founders?&lt;br&gt;
If you're just validating the idea, ShipFast. If you're building the actual SaaS, Kostra is the boilerplate with the most solid architecture available for the lowest price.&lt;/p&gt;

&lt;p&gt;Conclusion&lt;br&gt;
The best React SaaS boilerplate depends on what you're optimizing for. Speed to launch favors ShipFast. Team-heavy B2B favors MakerKit. Zero budget favors ixartz or Open SaaS.&lt;/p&gt;

&lt;p&gt;If you want the strongest balance of price, architecture, and completeness, Kostra delivers a production-ready Next.js foundation for $150 one-time. Auth, Stripe, RBAC, storage, email, and an admin dashboard, built on a layered architecture you won't outgrow. See how it stacks up against ten other Next.js boilerplates.&lt;/p&gt;

&lt;p&gt;Skip the setup weeks. Start on your product.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Next.js SaaS Boilerplate: Best Options for 2026</title>
      <dc:creator>Prince</dc:creator>
      <pubDate>Thu, 09 Jul 2026 10:28:20 +0000</pubDate>
      <link>https://dev.to/offpage_prince/nextjs-saas-boilerplate-best-options-for-2026-4g86</link>
      <guid>https://dev.to/offpage_prince/nextjs-saas-boilerplate-best-options-for-2026-4g86</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7fmrb927bjcrhh6ti4g2.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%2F7fmrb927bjcrhh6ti4g2.png" alt=" " width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Developing a SaaS product from scratch takes weeks of setting up, even before writing one line of the product's functionality. A Next.js SaaS boilerplate will save this time by providing you with the basics (authentication, billing, and dashboard integration) so that you can focus on writing the actual paying part of your product on the first day.&lt;/p&gt;

&lt;p&gt;In this guide, we will compare all major options available in 2026, list the criteria to distinguish a quality starter from a bad one, and show you how to choose the right one for your specific case. We will also consider what the decision to use the starter looks like six months after launching your app instead of right at the beginning, because this is when the real cost of a poor choice becomes apparent. &lt;/p&gt;

&lt;p&gt;Quick summary, if you are short on time: if you need to have multi-tenancy and billing working properly on day one and the value of your time is higher than the price of a license fee, a paid option such as Kostra would be the faster path. If you are validating your idea, feel comfortable building auth and billing by yourself, and have no budget, an open-source option such as T3 Stack or the official Vercel SaaS Starter would be the better choice. The rest of this guide explains why this way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fntc9akdrchkqqq5c1ke0.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%2Fntc9akdrchkqqq5c1ke0.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What is a Boilerplate Code (Quickly)&lt;br&gt;
Boilerplate code is reusable code that you put into every new project instead of coding it from scratch. It is the repetitive setup code that every application needs but is not related to the product-specific features at all.&lt;/p&gt;

&lt;p&gt;SaaS boilerplate is the same idea, but applied to a product. Not only configuration and structure but a ready-to-use starter template with the basics (auth, billing, multi-tenancy, and dashboard) implemented. And the main thing to add is the product itself, not the whole infrastructure around it.&lt;/p&gt;

&lt;p&gt;For the full breakdown of what boilerplate code is, as well as for the difference between boilerplate, a framework, and a library, read our guide on &lt;a href="https://kostra.io/blog/what-is-boilerplate-code" rel="noopener noreferrer"&gt;what boilerplate code &lt;/a&gt; is and &lt;a href="https://kostra.io/blog/what-is-a-saas-boilerplate" rel="noopener noreferrer"&gt;what a SaaS boilerplate actually includes&lt;/a&gt;, respectively.&lt;/p&gt;

&lt;p&gt;Why Next.js for Your SaaS Starter Kit&lt;br&gt;
Routing, API endpoints, and server rendering in one place is why Next.js became the default choice for building SaaS products based on React. In combination with the SaaS starter kit, you have a ready-to-use full-stack foundation instead of a barebones project.&lt;/p&gt;

&lt;p&gt;Speed. Auth, billing, and emails are set up before writing your first feature. You can ship on the first week instead of setting everything up.&lt;/p&gt;

&lt;p&gt;Modern tech stack. App Router, TypeScript, Tailwind CSS, and Prisma (or Drizzle) are the standard tools used in nearly all serious starters in 2026. All of those are not optional anymore.&lt;/p&gt;

&lt;p&gt;Deployable. Most options provide a starter kit that is ready to deploy on Vercel with one click, templates for environment variables, and ready-to-go API routes.&lt;/p&gt;

&lt;p&gt;Large community. Next.js has one of the largest developer communities among frameworks. Any bug is quickly fixed, and it is much easier to find a contractor/hire familiar with the stack than one familiar with the niche framework.&lt;/p&gt;

&lt;p&gt;Built to scale. A boilerplate created in the right way does not lock you inside of it. And if you are planning for the future, it is good to understand how a Next.js SaaS actually scales from starter to production before you commit to one stack over another.&lt;/p&gt;

&lt;p&gt;What to Check Before Picking One&lt;/p&gt;

&lt;p&gt;Not every starter that is called a "SaaS boilerplate" has all the required functionality. Some are landing pages with the login form included. Here is what to check before making a choice.&lt;/p&gt;

&lt;p&gt;Authentication&lt;br&gt;
Your boilerplate should support real authentication, not a demo login page. At least, the following things are needed:&lt;/p&gt;

&lt;p&gt;Email and password login&lt;/p&gt;

&lt;p&gt;OAuth (Google, GitHub, or both)&lt;/p&gt;

&lt;p&gt;Magic link login&lt;/p&gt;

&lt;p&gt;Protected routes and proper session management on the server, not just the client&lt;/p&gt;

&lt;p&gt;Skipping this and doing auth manually is one of the slowest and error-prone parts of any SaaS development. Also, auth bugs are the security bugs that should not be skipped under any circumstances. For the detailed description of what proper SaaS auth in Next.js should look like, read our guide on how to build authentication in a SaaS.&lt;/p&gt;

&lt;p&gt;Billing&lt;br&gt;
Billing is the most complex part of any SaaS application. And poor boilerplate shows its flaws here the quickest. A SaaS starter should include:&lt;/p&gt;

&lt;p&gt;Stripe integration that goes beyond a checkout button&lt;/p&gt;

&lt;p&gt;Subscription plans and tiers, including upgrades and downgrades&lt;/p&gt;

&lt;p&gt;A customer billing portal so users can manage their own subscription&lt;/p&gt;

&lt;p&gt;Webhook handling for payment events like failed charges, cancellations, and renewals&lt;/p&gt;

&lt;p&gt;Most free starters provide a simple Stripe checkout example but completely skip the webhook handler. This means that the state of subscription could fall out of sync with what Stripe charges users. And if there is some missing or poorly-written billing logic, you would spend weeks to implement it correctly. For more details on what is needed for a production-level billing setup, read on Stripe billing integration for Next.js SaaS and our broader Stripe SaaS billing guide.&lt;/p&gt;

&lt;p&gt;Multi-Tenancy&lt;br&gt;
Multi-tenant SaaS boilerplate allows one codebase for serving many different customers, each of whom has isolated data. &lt;/p&gt;

&lt;p&gt;There are a few common patterns for this:&lt;/p&gt;

&lt;p&gt;Shared database, tenant ID column. Simplest to build, cheapest to run, requires careful query discipline to avoid data leaks between tenants.&lt;/p&gt;

&lt;p&gt;Schema-per-tenant. Better isolation, more operational overhead as your tenant count grows.&lt;/p&gt;

&lt;p&gt;Database-per-tenant. Strongest isolation, usually reserved for enterprise contracts with strict compliance needs.&lt;/p&gt;

&lt;p&gt;Once you sell to companies or teams, implementing multi-tenancy would mean rebuilding your data layer, your auth flow, and billing logic at the same time. Choose this option beforehand if there is any chance you would need this functionality. For more information on architectural tradeoffs, see our guide on Next.js multi-tenant architecture patterns.&lt;/p&gt;

&lt;p&gt;TypeScript&lt;br&gt;
TypeScript prevents bugs on the deployment and makes a codebase more maintainable because it is easier to hand over a project written in TypeScript to another developer than to teach him/her the codebase written in vanilla JS. All serious starters use TypeScript as a default language in 2026, so the boilerplate that uses JS instead of TS could be considered inactive. For some tips on how to use TypeScript in a SaaS codebase, see our TypeScript best practices for Next.js SaaS apps.&lt;/p&gt;

&lt;p&gt;Database and Email&lt;br&gt;
Prisma with PostgreSQL is the most common default database among serious starters, although Drizzle is becoming popular too for the teams preferring an SQL-like ORM. As for the emails, Resend, Postmark, or SendGrid should be supported because all SaaS applications send transactional emails (welcome message, password reset, billing receipt, invoices, etc.). Leaving it aside would mean wiring up an email provider by yourself.&lt;/p&gt;

&lt;p&gt;Performance&lt;br&gt;
The starter that loads slowly at the very beginning would become even more sluggish with time. Make sure that the starter includes proper defaults for image optimisation, caching, and bundle size. Our guide on Next.js performance optimisation for SaaS apps covers what good defaults actually look like.&lt;/p&gt;

&lt;p&gt;Security and Route Protection&lt;br&gt;
A surprisingly large amount of starters protect routes only at the client-side level, which means that it is possible for a determined user to bypass the check completely by calling an API directly. The proper route protection should be done at the server side, not in the client-side component.&lt;/p&gt;

&lt;p&gt;Also, it is worth checking if passwords are hashed with a modern algorithm, if there is any login attempts' rate limiting, and if the session management does not allow session fixation attacks. None of this is a complex security task, but this is exactly the kind of thing that is often left aside in a starter created just to make a good demo video.&lt;/p&gt;

&lt;p&gt;API Rate Limiting&lt;br&gt;
Once your SaaS has real users, unprotected API routes become a liability really fast because of misbehaving clients and possible abuses. And the starter built for production should provide some starting point for rate limiting rather than leaving all of the endpoints unprotected. If your starter does not include this, you can read our guide on API rate limiting for Next.js SaaS apps walks through how to add it properly.&lt;/p&gt;

&lt;p&gt;Comparison Table&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvqkooija9ebar9m8ffeo.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%2Fvqkooija9ebar9m8ffeo.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Frequently, the pricing, feature set and maintenance status change in this area. Be sure to verify all the details directly on the website/repository of each particular product you consider before making a decision, and do not rely solely on any comparison article, including this one. &lt;/p&gt;

&lt;p&gt;The Options, Compared Fairly&lt;br&gt;
Kostra&lt;br&gt;
Paid Next.js boilerplate with authentication, Stripe billing, multi-tenancy, and dashboard that comes with pre-set connections. Designed specifically for the startup founders who are interested in starting with product features development on day one by skipping the setup phase completely. The trade-off here is pretty obvious - you invest upfront money in order to save time, but you are losing community flexibility and the zero upfront cost of open source projects.&lt;/p&gt;

&lt;p&gt;You should go with Kostra if you already know that you definitely need multi-tenancy, you do not want to wire billing webhooks yourself, and your time is worth more than the license. Otherwise, Kostra will make less sense for you, especially if you are in the validation phase of your idea and are not sure if you need any of these features. For a detailed feature-by-feature comparison with an open-source alternative, see T3 Stack vs. Kostra.&lt;/p&gt;

&lt;p&gt;T3 Stack&lt;br&gt;
Popular open-source SaaS boilerplate consisting of Next.js, tRPC, Prisma, Tailwind, and NextAuth. One of the best TypeScript stacks of all the starters listed on this comparison, as it ensures end-to-end type safety from your database to the frontend of your project and has very strong community backing.&lt;/p&gt;

&lt;p&gt;The thing is that T3 Stack does not provide you with billing and multi-tenancy capabilities. So, you implement these yourself and depending on your experience with Stripe webhook and multi-tenancy design, it may take you from a few days to a few weeks. It may sound discouraging for some developers, but for others, T3 Stack is the perfect option.&lt;/p&gt;

&lt;p&gt;Vercel SaaS Starter&lt;br&gt;
Official Vercel starter package. It is free, simple and minimalistic boilerplate with a basic Stripe billing implemented. Good choice for solo projects and anyone who wants to implement the proper architectural patterns of Next.js SaaS.&lt;/p&gt;

&lt;p&gt;It does not provide multi-tenancy, role-based permissions and complex billing implementation that paid starters and more feature-complete open source projects offer. Consider it not as a ready-to-use solution, but rather as a clean documentation example you can start from.&lt;/p&gt;

&lt;p&gt;Node.js SaaS Boilerplate (Backend-First)&lt;br&gt;
Good choice for projects where you need a separate backend on Express or Fastify along with a Next.js frontend instead of using Next.js API routes for everything. In some situations, such a backend-first approach gives you more control over the back-end architecture of your project, especially if you plan to develop a service layer, background jobs processing or even microservices in the future.&lt;/p&gt;

&lt;p&gt;However, you have to put more effort into the initial setup compared to other options and the quality of various open-source Node.js SaaS boilerplates differs dramatically. So, be careful and check the history of commits and issues on GitHub very carefully before choosing any generic option from this category.&lt;/p&gt;

&lt;p&gt;How Much Time Does a Boilerplate Actually Save?&lt;br&gt;
This is the question that defines whether it is really profitable for you to pay for a boilerplate or not. It is always good to be clear about the benefits of "saves you time".&lt;/p&gt;

&lt;p&gt;Proper implementation of auth (including OAuth, session management and password reset flows) takes about three to five days for an experienced developer. Implementation of billing with Stripe (subscription tiers, customer portal, and properly implemented webhook handler) takes an additional week at least and even longer if you are implementing it for the first time and need to understand the Stripe events mechanism. If you build multi-tenancy from the beginning, it will add an additional few days to a week, depending on the isolation pattern you choose.&lt;/p&gt;

&lt;p&gt;So, the careful implementation of auth, billing, and multi-tenancy from scratch takes you about two to four weeks before you can start with product features development. Boilerplate that implements all these things correctly saves you a whole this time, and whether it is worth it or not depends only on your own hourly rate and your runway. But, as a rule, it is more beneficial to pay for the boilerplate in case of funded/revenue generating teams.&lt;/p&gt;

&lt;p&gt;Total Cost of Ownership: Free vs. Paid&lt;br&gt;
The license fee is not the only factor defining the total cost of the solution. Free boilerplate does not have a license fee, but it transfers the cost into development time, maintenance and risks.&lt;/p&gt;

&lt;p&gt;Setup time. Free boilerplates require you to fill the gaps by yourself, which is an additional time investment.&lt;/p&gt;

&lt;p&gt;Maintenance burden. Open source projects require continuous maintainers' interest. In case of paid boilerplates, there is always a commercial interest to update the boilerplate with the Next.js versions in a timely. In the case of free projects, updates will happen when the maintainer finds enough time for them.&lt;/p&gt;

&lt;p&gt;Documentation quality. As a rule, paid boilerplates provide the documentation describing why particular decisions were made by the author, not only how to set up the boilerplate. It becomes crucial in case of debugging problems which were created by someone else six months ago, and there are no comments explaining why a particular architectural decision was made.&lt;/p&gt;

&lt;p&gt;Support. When something goes wrong in the middle of the night in production, paid boilerplate with support channel or community Discord will give you the answer faster than searching GitHub issues on a free repository with a small maintainer base.&lt;/p&gt;

&lt;p&gt;But it does not mean that free boilerplates are bad options for you. In the early stage of validation, it is better to choose a free boilerplate with a smaller feature set, since you are not sure yet if you need all these complex features. Things will be different if you already have paying customers and your downtime has financial consequences.&lt;/p&gt;

&lt;p&gt;How to Choose&lt;br&gt;
Go through these five questions before choosing any starter. &lt;/p&gt;

&lt;p&gt;Do you need multi-tenancy? If the answer is yes or probably you will need it, do not choose the boilerplate which requires you to implement multi-tenancy later. Addition of tenant isolation to existing data model is one of the most painful refactorings in SaaS development. &lt;/p&gt;

&lt;p&gt;What does your team already know? Boilerplate developed with stack that your team never uses before increases the learning curve on top of the saved setup time. Match the stack to the actual strengths of your team, not to the most popular one.&lt;/p&gt;

&lt;p&gt;How fast do you need to ship? Paid boilerplates save calendar time but cost you license fee. Open source boilerplates save money but cost you engineering hours. Choose the constraint that you are actually struggling with.&lt;/p&gt;

&lt;p&gt;Are you deploying to Vercel? Make sure that the starter was developed with it in mind (edge runtime compatibility and environment variables convention) instead of assuming that any Next.js project will deploy with no adjustments.&lt;/p&gt;

&lt;p&gt;Is it still maintained? Before choosing the open source boilerplate, check the GitHub commit history. Repository which does not have any commits during last few months is risky when Next.js, React and your auth provider will ship breaking changes.&lt;/p&gt;

&lt;p&gt;If you want the detailed overview of the decision process, including the description of how to balance all these aspects in order to choose the starter that suits your needs, see our full guide on how to choose the right Next.js boilerplate for your SaaS.&lt;/p&gt;

&lt;p&gt;Common Mistakes&lt;br&gt;
Adding multi-tenancy after launch. Usually, it requires a partial rebuild of your data model, auth flow and billing logic. If there is any possibility that you will be selling to teams or companies, you should implement the multi-tenancy from day one.&lt;/p&gt;

&lt;p&gt;Choosing the dead project. No updates mean no security updates and a lack of maintainer's interest in case your dependency breaks. Always check the recent activity of the project, not only the number of stars, before using the free boilerplate.&lt;/p&gt;

&lt;p&gt;Picking a bloated starter. Some boilerplates come with a lot of features that you will never use in your project: internationalisation you will not use, a role system with five permission tiers for your two-man team and an admin panel for features you have not yet implemented. Instead of working with real features, you spend the time stripping the unnecessary things. Choose boilerplate according to your scope, not the most feature-complete one.&lt;/p&gt;

&lt;p&gt;Skipping TypeScript. It is much faster on the initial stage of development, but costs you much more in the debugging process once your project grows bigger and has more than a few files, especially in case a second developer joins your project.&lt;/p&gt;

&lt;p&gt;Underestimating billing complexity. Many founders think that "Stripe integration" equals to the presence of a checkout button on your page. Actually, the main thing in Stripe billing is handling the webhooks: synchronisation of your database with subscription upgrades/downgrades, failed payments and cancellations. Boilerplate that skips this step will give you a gap, which usually appears in case of a real customer's card problem.&lt;/p&gt;

&lt;p&gt;What Happens Six Months After Launch&lt;br&gt;
Comparison of SaaS boilerplates ends with the moment of setup in many cases. The more important question is what your codebase will look like in six months after launch, after you have implemented some features that the starter does not have.&lt;/p&gt;

&lt;p&gt;Dependency drift. Next.js, your auth library and Stripe's SDK are updated separately. A starter that was up-to-date at the time of your launch will become outdated very soon if nobody maintains it. And this is the main reason why it is always good to check the GitHub history of commits before choosing the starter, not only when you will clone the repository, but in order to make sure that it will stay safe to use a year later.&lt;/p&gt;

&lt;p&gt;Schema migrations. Your data model will change: additional columns, tables for unimplemented features, tenant-level settings. Starting with a clear and documented migration approach will help you in this process; starting with a messy schema or without documentation will turn every change into an investigation.&lt;/p&gt;

&lt;p&gt;Onboarding of the second developer. The first developer who started working on the starter understands all the decisions that were made in the starter because he/she made most of these decisions himself/herself while implementing the starter. The second developer does not have this advantage. And in this case, documentation becomes a crucial thing instead of nice to have: it becomes the difference between a smooth onboarding process and week of confusion in Slack. And it becomes one of the biggest differences between free and paid starters: paid starters usually contain documentation with architectural reasons, and free ones have only setup documentation.&lt;/p&gt;

&lt;p&gt;Scaling beyond the starter's assumptions. Every starter has assumptions about your tenant model, billing, traffic, etc. It will work perfectly for the first thousand users, and sometimes it will start to struggle after that. It is always good to know which assumptions your chosen starter has and how hard it will be to change them later. See our guide on scaling a Next.js SaaS from starter to production, which walks through exactly where these strains tend to show up first.&lt;/p&gt;

&lt;p&gt;Final Thoughts&lt;br&gt;
Right Next.js SaaS boilerplate depends on your timeline, the stack of your team and the need for multi-tenancy from the beginning. Open-source starters like T3 and Vercel starter boilerplate give you flexibility and zero upfront cost, but require more setup time and continuous maintenance from your side. Paid starters like Kostra save you setup time and reduce maintenance risk, but have a higher upfront cost and less flexibility in changing architecture.&lt;/p&gt;

&lt;p&gt;It is always good to make your own choice instead of looking for the universally correct answer. A solo founder who validates an early idea has different needs from a funded team that builds for an enterprise contract.&lt;/p&gt;

&lt;p&gt;Frequently Asked Questions&lt;br&gt;
Is a Next.js SaaS boilerplate worth it for a solo founder?&lt;br&gt;
It all depends on what do you know how to implement in two days. If you can correctly build auth and billing with Stripe, a free boilerplate like T3 or Vercel SaaS Starter may be enough for you, because you just know how to extend the project. However, if this is your first experience of creating a SaaS or you want to validate an idea in a week, a paid boilerplate with billing and multi-tenancy implementation always saves you time. &lt;/p&gt;

&lt;p&gt;Can I switch boilerplates after I have already started building?&lt;br&gt;
Yes, but it will become more difficult the further you move along in your project. Changing the auth provider when you already have real user accounts requires the plan of moving from one set of credentials to another. Changing the billing system when you already have paying customers requires careful migration of active subscriptions so that nobody's billing cycle is broken. It is easier to spend several more days choosing a boilerplate from the very beginning than to migrate after six months of development. &lt;/p&gt;

&lt;p&gt;Does a multi-tenant SaaS boilerplate cost more to run than a single-tenant one?&lt;br&gt;
Not necessarily. A boilerplate with a shared database and a tenant-ID model of multi-tenancy costs approximately the same as a single-tenant SaaS project, because you don't duplicate your infrastructure for each tenant. Additional costs appear in case of more isolated approaches like schema-per-tenant or database-per-tenant. They provide higher security of data but at higher costs of operation, justified by the necessity of using them when you get enterprise customers.&lt;/p&gt;

&lt;p&gt;Do I need TypeScript for a Next.js SaaS, or is JavaScript fine?&lt;br&gt;
If you use JavaScript, your project will work properly. But when your team grows larger or you create a SaaS with a complex data model where typos in property names should be detected beforehand, TypeScript will save your time in debugging. In the case of SaaS that you are going to maintain for several years, the initial time spent on learning it is insignificant compared to the time during debugging.&lt;/p&gt;

&lt;p&gt;What is the real difference between a "boilerplate" and a "starter kit"?&lt;br&gt;
In practice, there is no big difference between them, and both terms are interchangeable for almost everyone in the industry, even in this guide. Some people mean a bit more minimal foundation in case of "boilerplate" and something closer to a ready solution with UI in case of "starter kit". There is no exact industry standard dividing the two terms, so when choosing your boilerplate, look at its features instead of the name.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
