<?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: Oluwafemi samson </title>
    <description>The latest articles on DEV Community by Oluwafemi samson  (@daboss2003).</description>
    <link>https://dev.to/daboss2003</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%2F1157795%2F1bdf0be8-c6e6-4770-b456-ed6e82cb1166.png</url>
      <title>DEV Community: Oluwafemi samson </title>
      <link>https://dev.to/daboss2003</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/daboss2003"/>
    <language>en</language>
    <item>
      <title>The part of a PaaS you use most should have the least power — so I built Mooring</title>
      <dc:creator>Oluwafemi samson </dc:creator>
      <pubDate>Mon, 06 Jul 2026 12:46:16 +0000</pubDate>
      <link>https://dev.to/daboss2003/the-part-of-a-paas-you-use-most-should-have-the-least-power-so-i-built-mooring-5b1a</link>
      <guid>https://dev.to/daboss2003/the-part-of-a-paas-you-use-most-should-have-the-least-power-so-i-built-mooring-5b1a</guid>
      <description>&lt;p&gt;I have a folder on my laptop called &lt;code&gt;side-projects&lt;/code&gt;. Most of them are Dockerized. Most of them will never see more than a handful of users. And for years, every one of them hit the same wall: getting the thing onto a cheap VPS without losing a weekend — and copy-pasting my own past mistakes forward every single time.&lt;/p&gt;

&lt;p&gt;Here's the opinion that eventually turned into a project: &lt;strong&gt;the part of a PaaS you touch most often should hold the &lt;em&gt;least&lt;/em&gt; power over your server.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Think of a deploy tool as two planes. There's a &lt;strong&gt;read plane&lt;/strong&gt; — dashboards, logs, container health, the stuff you stare at — where you spend most of your time and which is your most exposed surface. And there's a &lt;strong&gt;write plane&lt;/strong&gt; — deploy, restart, the actions that actually change the system — which is rare and should be gated. My frustration was that the thing I looked at all day and the thing that could rewrite my box tended to sit on the same pile of privilege.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Mooring&lt;/strong&gt; to keep those two planes apart. It's an early, solo project, and this post is me showing it and asking for eyes on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;The whole thing, end to end:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install&lt;/strong&gt; it as an &lt;em&gt;unprivileged&lt;/em&gt; systemd service (not root).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connect&lt;/strong&gt; a git repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write&lt;/strong&gt; one &lt;code&gt;mooring.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Click Deploy.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the loop. Everything below is what's underneath it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;Mooring is a small, security-first, self-hosted control plane for Docker — a tiny PaaS. You point it at a git repo, describe your app once, and it deploys and runs your containers on your own server. Same territory as Coolify, Dokku, CapRover, and Kamal — all genuinely good work. The difference I care about is the posture underneath.&lt;/p&gt;

&lt;p&gt;It ships as &lt;strong&gt;one static, CGO-free Go binary&lt;/strong&gt;. It runs as an &lt;strong&gt;unprivileged systemd service&lt;/strong&gt; — not root. State lives in SQLite (the pure-Go modernc driver). Every asset is embedded with &lt;code&gt;go:embed&lt;/code&gt;, so there's no &lt;code&gt;node_modules&lt;/code&gt;, no asset pipeline, nothing to build on the box.&lt;/p&gt;

&lt;p&gt;To be honest about the neighborhood: setting up a self-hosted PaaS, these tools &lt;em&gt;tend&lt;/em&gt; to want broad access to your machine and your Docker daemon. And read-write access to the Docker socket is, for all practical purposes, root on the host. I'm running a personal VPS with my actual secrets on it, so that's the trade I wanted to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The read plane never holds a read-write Docker socket
&lt;/h2&gt;

&lt;p&gt;This is the part I actually built the project around.&lt;/p&gt;

&lt;p&gt;Mooring's &lt;strong&gt;read plane&lt;/strong&gt; — every dashboard view that shows you what your containers are doing — talks to Docker through a &lt;strong&gt;read-only docker-socket-proxy&lt;/strong&gt;. Those views never hold raw read-write access to the socket. The &lt;strong&gt;write plane&lt;/strong&gt; (the stuff that changes your system) goes through a separate, controlled path instead of being blended into every page load. The surface you use 95% of the time is the surface with the least reach.&lt;/p&gt;

&lt;p&gt;A few more things fall out of the same "assume this box matters" mindset:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The admin web UI runs under a &lt;strong&gt;strict Content-Security-Policy with no inline JavaScript&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Auth is &lt;strong&gt;password + TOTP 2FA&lt;/strong&gt;, and changing your password, username, or TOTP &lt;strong&gt;revokes every existing session&lt;/strong&gt; — an "auth-epoch" fingerprint — so a stolen session can't outlive the credential change meant to kill it.&lt;/li&gt;
&lt;li&gt;The managed images (the socket proxy, the edge, and friends) are &lt;strong&gt;digest-pinned&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Files bind-mounted into your containers get &lt;strong&gt;least-privilege permissions&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;API tokens are &lt;strong&gt;scoped and CIDR-bound&lt;/strong&gt; — restricted to the networks you name.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Deploy a Dockerized app without writing a Dockerfile
&lt;/h2&gt;

&lt;p&gt;Here's a real (trimmed) &lt;code&gt;mooring.yaml&lt;/code&gt; for one of my apps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mooring/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;App&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linkstash&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;compose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;generated&lt;/span&gt;            &lt;span class="c1"&gt;# Mooring generates &amp;amp; owns the compose&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;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                   &lt;span class="c1"&gt;# no Dockerfile to write — Mooring generates a hardened one&lt;/span&gt;
          &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node&lt;/span&gt;
          &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;dist/main&lt;/span&gt;&lt;span class="pi"&gt;]&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="nv"&gt;internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;3000&lt;/span&gt; &lt;span class="pi"&gt;}]&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
          &lt;span class="na"&gt;MONGODB_URI&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;MONGODB_URI&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# a reference — the value stays in the store&lt;/span&gt;
  &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&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;MONGODB_URI&lt;/span&gt;
  &lt;span class="na"&gt;edge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt;  &lt;span class="c1"&gt;# Mooring terminates HTTPS and routes to api:3000&lt;/span&gt;
        &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines are worth pointing at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;source: generated&lt;/code&gt; — Mooring generates and &lt;em&gt;owns&lt;/em&gt; the compose file. You don't hand-edit it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;build.language: node&lt;/code&gt; — you name the language, and Mooring generates a hardened Dockerfile for you. (It supports Node, Python, Go, Ruby, PHP, static, and generic.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MONGODB_URI: { secret: MONGODB_URI }&lt;/code&gt; — that's a &lt;em&gt;reference&lt;/em&gt;, not a value. The secret stays in the store; the plaintext never lands in the file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the part I'm actually proud of — &lt;strong&gt;what I did &lt;em&gt;not&lt;/em&gt; write&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;Dockerfile&lt;/code&gt;. Mooring generates and owns a hardened one.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;docker-compose.yml&lt;/code&gt;. Mooring generates and owns the compose.&lt;/li&gt;
&lt;li&gt;No reverse-proxy config. A managed &lt;strong&gt;Caddy&lt;/strong&gt; does automatic HTTPS and renews the certs — including internal certs for services like an MQTT broker.&lt;/li&gt;
&lt;li&gt;No cert-reload sidecar. Gone.&lt;/li&gt;
&lt;li&gt;No plaintext secret in the file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because Mooring owns compose generation, the genuinely dangerous compose keys — &lt;code&gt;privileged&lt;/code&gt;, host mounts, host namespaces — &lt;strong&gt;can't even be expressed&lt;/strong&gt; in &lt;code&gt;mooring.yaml&lt;/code&gt;. There's simply no syntax for them in the schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bits that made it pleasant to dogfood
&lt;/h2&gt;

&lt;p&gt;The build system recently learned to &lt;strong&gt;detect your package manager from your committed lockfile&lt;/strong&gt; — &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; → pnpm, &lt;code&gt;yarn.lock&lt;/code&gt; → yarn (including Yarn Berry), else npm; &lt;code&gt;poetry.lock&lt;/code&gt; → poetry, &lt;code&gt;Pipfile&lt;/code&gt; → pipenv, else pip — and it strips dev dependencies out of the shipped image.&lt;/p&gt;

&lt;p&gt;The gitops model is deliberately calm: connect a repo, and Mooring &lt;strong&gt;fetches&lt;/strong&gt; changes on its own (no webhook to configure), but it &lt;strong&gt;never auto-deploys&lt;/strong&gt;. You click Deploy. Push-to-deploy exists as an explicit opt-in. One repo can hold multiple apps via &lt;code&gt;mooring.yaml&lt;/code&gt; + &lt;code&gt;mooring.*.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On the ops side there's an auto-scaling edge load balancer, encrypted backups of Mooring's own state, a read-only &lt;strong&gt;Server&lt;/strong&gt; tab (live CPU/mem/disk, top processes, and an allow-listed file viewer that refuses to show secrets or state), optional &lt;strong&gt;Trivy CVE scanning&lt;/strong&gt; of your app images &lt;em&gt;without&lt;/em&gt; mounting the docker socket into Trivy, and a self-update/advisory checker that alerts you even when the &lt;em&gt;running&lt;/em&gt; Mooring version is itself the one affected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;Mooring is young. It's solo-built, it's around &lt;strong&gt;v0.4.x&lt;/strong&gt;, and right now its most serious user is me, dogfooding it on my own boxes. I'm not going to pretend it's battle-tested at scale — it isn't, yet. I'm also not going to overstate the guarantees: the claim is that the read plane doesn't hold a read-write socket and that dangerous compose keys can't be expressed, not that the whole thing is unbreakable.&lt;/p&gt;

&lt;p&gt;But the core idea feels right, and I'd love other eyes on it — especially from people who care about the socket-access problem the way I do. If you run Dockerized side-projects on a VPS and any of this resonates, I'd genuinely value your feedback, your "wait, but what about X," and your early-user bug reports.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/daboss2003/mooring" rel="noopener noreferrer"&gt;https://github.com/daboss2003/mooring&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Docs &amp;amp; site: &lt;strong&gt;&lt;a href="https://daboss2003.github.io/mooring/" rel="noopener noreferrer"&gt;https://daboss2003.github.io/mooring/&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you kick the tires, come tell me what broke. That's the whole reason I'm posting.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>docker</category>
      <category>selfhosted</category>
      <category>security</category>
    </item>
  </channel>
</rss>
