<?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: Amit Shukla</title>
    <description>The latest articles on DEV Community by Amit Shukla (@amitshuklabag).</description>
    <link>https://dev.to/amitshuklabag</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%2F4119780%2Fb3b9f2eb-017a-4107-b6fa-83139df41a29.png</url>
      <title>DEV Community: Amit Shukla</title>
      <link>https://dev.to/amitshuklabag</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amitshuklabag"/>
    <language>en</language>
    <item>
      <title>Self-hosting open source is easy. Running it in production is not.</title>
      <dc:creator>Amit Shukla</dc:creator>
      <pubDate>Thu, 10 Sep 2026 19:24:53 +0000</pubDate>
      <link>https://dev.to/amitshuklabag/self-hosting-open-source-is-easy-running-it-in-production-is-not-i72</link>
      <guid>https://dev.to/amitshuklabag/self-hosting-open-source-is-easy-running-it-in-production-is-not-i72</guid>
      <description>&lt;p&gt;You picked an open source tool on purpose. Maybe it was n8n instead of a per-task automation SaaS, Keycloak instead of paying per active user for auth, Postgres and Grafana and Mattermost instead of three separate subscriptions that each want a seat license. The pitch is good: own your data, no per-seat bill, no vendor deciding your roadmap.&lt;/p&gt;

&lt;p&gt;You run &lt;code&gt;docker compose up&lt;/code&gt;. It works. You show the team. Everyone is happy.&lt;/p&gt;

&lt;p&gt;Then it goes to production, and the actual job starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The work that is not in the README
&lt;/h2&gt;

&lt;p&gt;The README gets you a running process. Keeping that process healthy, safe, and recoverable for the next two years is a different list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sizing and provisioning.&lt;/strong&gt; How much CPU, memory, and disk does this actually need under real load, and what happens when it grows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OS patching.&lt;/strong&gt; The host needs security updates on a schedule, and someone has to apply them without taking the service down at a bad time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application updates.&lt;/strong&gt; The upstream project ships a release with a security fix. Now you need to update without breaking your config, your database schema, or your integrations, and you need a way back if it goes wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS.&lt;/strong&gt; Certificates that issue and renew automatically, and do not silently expire on a Sunday.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backups.&lt;/strong&gt; Not a cron job that writes to the same disk. Off-host, encrypted, with a retention policy, and actually tested by restoring them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring and alerts.&lt;/strong&gt; You want to know the disk is 80 percent full before it is 100 percent full and the database has stopped accepting writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email.&lt;/strong&gt; Most of these apps need to send mail for invites, resets, and notifications. Getting that delivered instead of dropped into spam is its own project: SPF, DKIM, DMARC, a warmed sending IP or a relay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network security.&lt;/strong&gt; A firewall that exposes only what should be exposed, and some answer for volumetric attacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets and log hygiene.&lt;/strong&gt; Rotating credentials, rotating logs before they eat the disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracking disclosures.&lt;/strong&gt; Every app you self-host is now something you have to watch for CVEs, per project, forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is hard in isolation. All of it together, across five or six self-hosted apps, is a part time job. And it is usually a part time job that lives in one engineer's head.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two options most teams settle for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do it yourself on a VPS.&lt;/strong&gt; This works. It works right up until the person who set it up is on leave during an incident, or the one undocumented step in the upgrade runbook gets skipped, or a restore is needed for the first time and nobody has ever tested one. The software is free. The operational knowledge is expensive and fragile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pay for the vendor's hosted version.&lt;/strong&gt; Faster to start, but you are often back to per-seat pricing, your data sits on their infrastructure, and you have given up the control that made you choose open source in the first place. For some tools the managed tier costs several times the raw compute.&lt;/p&gt;

&lt;p&gt;There is a third option, which is to treat "operating open source software" as a solved, boring layer that you rent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a managed layer looks like: Elestio
&lt;/h2&gt;

&lt;p&gt;One option in this space is &lt;a href="https://elest.io/" rel="noopener noreferrer"&gt;Elestio&lt;/a&gt;. It runs a managed layer on top of open source software that you still own.&lt;/p&gt;

&lt;p&gt;You pick an app from a catalog of more than 400+ open source templates. You pick where it runs: a major cloud provider and region of your choice, or your own virtual machines, or your own hardware on premise. It deploys onto a dedicated instance, not a shared multi tenant box.&lt;/p&gt;

&lt;p&gt;From there the operational list above is handled for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automated encrypted backups, off host, with retention, and restore that has been tested&lt;/li&gt;
&lt;li&gt;Monitoring with alerting on the things that actually page you&lt;/li&gt;
&lt;li&gt;Automatic OS patching and application updates, with a rollback path&lt;/li&gt;
&lt;li&gt;Managed SMTP so outbound mail is set up and deliverable&lt;/li&gt;
&lt;li&gt;TLS certificates that issue and renew on their own&lt;/li&gt;
&lt;li&gt;A firewall and DDoS protection in front of the instance&lt;/li&gt;
&lt;li&gt;Built in CI/CD if you are deploying your own code alongside&lt;/li&gt;
&lt;li&gt;Support that covers the stack, not just the VM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You keep root access. It is your instance and your data. The billing is a flat fee on top of the compute, not per user, so it does not punish you for adding people.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this makes sense, and when it does not
&lt;/h2&gt;

&lt;p&gt;It makes sense if you are a small or mid sized team without a dedicated platform or SRE function, you want the economics and control of open source, and you would rather your engineers ship product than babysit a Postgres upgrade.&lt;/p&gt;

&lt;p&gt;It makes less sense if you already have a mature platform team, infrastructure as code for all of this, tested restores, and an on call rotation. If you have built that muscle, keep using it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Open source software being free to license does not make it free to operate. Backups, patching, updates, monitoring, mail, and security tracking are the real cost, and they do not show up until you are in production.&lt;/p&gt;

&lt;p&gt;The question worth asking is whether operating this software is a competitive advantage for your business. If it is, invest in doing it well. If it is not, and for most teams it is not, hand that layer to something that does it for a living and get back to the work only you can do.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Multi-stage Docker builds: ship the artifact, not the build shop</title>
      <dc:creator>Amit Shukla</dc:creator>
      <pubDate>Thu, 10 Sep 2026 19:14:54 +0000</pubDate>
      <link>https://dev.to/amitshuklabag/multi-stage-docker-builds-ship-the-artifact-not-the-build-shop-3o6l</link>
      <guid>https://dev.to/amitshuklabag/multi-stage-docker-builds-ship-the-artifact-not-the-build-shop-3o6l</guid>
      <description>&lt;p&gt;Most Node images I inherit are somewhere north of a gigabyte. The app inside is maybe 40 MB. The other gigabyte is the build shop: compilers, dev dependencies, the npm cache, a full copy of the source, and whatever the base image shipped with.&lt;/p&gt;

&lt;p&gt;That gigabyte costs you every day. Slower pulls on every deploy and every autoscale event. More layers for your scanner to chew through. A bigger surface for a CVE to land on. And a longer gap between "push" and "running in production".&lt;/p&gt;

&lt;p&gt;Multi-stage builds fix this without touching your application code. Here is how they work and how to get the details right.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ends up in a single stage image
&lt;/h2&gt;

&lt;p&gt;When you write a normal Dockerfile, every instruction adds a layer, and every layer stays in the final image. A typical Node build looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final image now contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;node:20&lt;/code&gt;, which is Debian plus the full Node toolchain, around 1.1 GB before you add anything&lt;/li&gt;
&lt;li&gt;every dependency, including the dev ones you only need to compile&lt;/li&gt;
&lt;li&gt;the npm cache left behind by &lt;code&gt;npm ci&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;your entire source tree, not just the compiled output&lt;/li&gt;
&lt;li&gt;any build artifacts you generated on the way&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You run &lt;code&gt;node dist/server.js&lt;/code&gt;. Everything else is dead weight that ships anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The multi-stage pattern
&lt;/h2&gt;

&lt;p&gt;A multi-stage Dockerfile has more than one &lt;code&gt;FROM&lt;/code&gt;. Each &lt;code&gt;FROM&lt;/code&gt; starts a new stage with its own filesystem. You do the messy work in an early stage, then start a clean final stage and copy in only what you need with &lt;code&gt;COPY --from&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things changed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The first stage is named &lt;code&gt;build&lt;/code&gt; with &lt;code&gt;AS build&lt;/code&gt;. It installs everything and compiles. None of its layers reach the final image.&lt;/li&gt;
&lt;li&gt;The second stage starts from &lt;code&gt;node:20-slim&lt;/code&gt;, a much smaller base. It installs production dependencies only, then copies the compiled &lt;code&gt;dist&lt;/code&gt; folder out of the &lt;code&gt;build&lt;/code&gt; stage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The compilers, the dev dependencies, and the source tree never make it into the image you ship. Same build, a fraction of the size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get these four details right
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Name your stages
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;AS &amp;lt;name&amp;gt;&lt;/code&gt; on stages you copy from, then reference them in &lt;code&gt;COPY --from=&amp;lt;name&amp;gt;&lt;/code&gt;. You can also copy from an external image directly, for example &lt;code&gt;COPY --from=nginx:latest /etc/nginx/nginx.conf ./&lt;/code&gt;, which is handy for grabbing a single binary or config file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not copy node_modules across stages
&lt;/h3&gt;

&lt;p&gt;It is tempting to &lt;code&gt;COPY --from=build /app/node_modules ./node_modules&lt;/code&gt; and skip a second install. Do not. The build stage has dev dependencies mixed in, and native modules may have compiled against a different base image. Run &lt;code&gt;npm ci --omit=dev&lt;/code&gt; in the final stage so you get a clean production tree that matches the runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Order instructions so the cache survives
&lt;/h3&gt;

&lt;p&gt;Docker caches each layer and reuses it until something upstream changes. Copy your lockfiles and install before you copy the rest of the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a change to your application code invalidates only the &lt;code&gt;COPY . .&lt;/code&gt; layer and everything after it. The dependency install, which is the slow part, stays cached. Reverse those lines and every one line code change reinstalls everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Go smaller on the final base
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;node:20-slim&lt;/code&gt; drops most of the Debian extras. For a bigger cut, &lt;code&gt;gcr.io/distroless/nodejs20&lt;/code&gt; ships Node and nothing else, no shell, no package manager. It is stricter to debug but the attack surface is tiny. Whatever you pick, add a non-root user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; node&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The same feature, other uses
&lt;/h2&gt;

&lt;p&gt;Multi-stage is not only about size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One Dockerfile, several targets.&lt;/strong&gt; Add a &lt;code&gt;test&lt;/code&gt; stage and build just that stage in CI with &lt;code&gt;docker build --target test .&lt;/code&gt;. Your test image and your production image come from the same file and the same base layers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="c"&gt;# ... install and compile ...&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run lint &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;
&lt;span class="c"&gt;# ... copy artifact ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pulling a tool without installing it.&lt;/strong&gt; Need &lt;code&gt;dockerize&lt;/code&gt; or a specific CLI at runtime? Copy the single binary from its published image in a stage instead of running a package manager in your final image.&lt;/p&gt;

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

&lt;p&gt;On a plain Express and TypeScript service:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single stage on &lt;code&gt;node:20&lt;/code&gt;: about 1.1 GB&lt;/li&gt;
&lt;li&gt;Multi-stage with &lt;code&gt;node:20-slim&lt;/code&gt; and &lt;code&gt;--omit=dev&lt;/code&gt;: about 180 MB&lt;/li&gt;
&lt;li&gt;Multi-stage with distroless: about 130 MB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pull time on a cold node drops from tens of seconds to a few. Your scanner has less to report. And nothing in the image can compile code or run a shell that does not need to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Build with the full toolchain in an early stage. Start the final stage from the smallest base you can debug, install production dependencies only, and copy in just the artifact. Name your stages, keep your install layer above your source copy, and run as a non-root user. It is a ten line change to your Dockerfile and it pays off on every deploy.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>node</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
