<?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>Forem: Nadiar Syaripul</title>
    <description>The latest articles on Forem by Nadiar Syaripul (@codxse).</description>
    <link>https://forem.com/codxse</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3908845%2F1ec6345f-5523-4dc0-9951-0dcdb43aa1d5.jpeg</url>
      <title>Forem: Nadiar Syaripul</title>
      <link>https://forem.com/codxse</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/codxse"/>
    <language>en</language>
    <item>
      <title>How to Deploy Any Public Docker Image (like wg-easy) to a VPS Using Kamal</title>
      <dc:creator>Nadiar Syaripul</dc:creator>
      <pubDate>Sat, 02 May 2026 10:56:09 +0000</pubDate>
      <link>https://forem.com/codxse/how-to-deploy-any-public-docker-image-like-wg-easy-to-a-vps-using-kamal-jmn</link>
      <guid>https://forem.com/codxse/how-to-deploy-any-public-docker-image-like-wg-easy-to-a-vps-using-kamal-jmn</guid>
      <description>&lt;p&gt;Kamal is my go-to deployment tool for Rails apps — it handles SSL, zero-downtime deploys, and the proxy with a single kamal deploy command. But officially, Kamal expects you to build and push your own image. It can't deploy an arbitrary public image directly from a registry like Docker Hub or GHCR.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Or so I thought.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this post, I'll show you a small trick using a placeholder Dockerfile + &lt;a href="https://kamal-deploy.org/docs/configuration/accessories/" rel="noopener noreferrer"&gt;Kamal accessories&lt;/a&gt; to deploy any publicly available Docker image to a VPS — no custom build required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Sometimes you just want to run a pre-built public image on your server. In my case, I needed to deploy &lt;a href="https://github.com/wg-easy/wg-easy" rel="noopener noreferrer"&gt;wg-easy&lt;/a&gt; — a WireGuard UI — on a VPS where I already have SSH access.&lt;br&gt;
Kamal's normal flow assumes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a Dockerfile&lt;/li&gt;
&lt;li&gt;You build and push your own image to a registry&lt;/li&gt;
&lt;li&gt;Kamal pulls and deploys that image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But for third-party images, step 1 and 2 are unnecessary overhead.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Trick: Accessories + Placeholder Dockerfile
&lt;/h2&gt;

&lt;p&gt;Kamal has a feature called accessories — long-running companion services (think databases, sidekiq workers) deployed alongside your main app. Crucially, accessories can pull any image directly from a public registry.&lt;/p&gt;

&lt;p&gt;The workaround:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy the real app (e.g. &lt;code&gt;wg-easy&lt;/code&gt;) as an accessory&lt;/li&gt;
&lt;li&gt;Use a minimal placeholder Dockerfile (just an nginx image) as the "main" app to satisfy Kamal's build requirement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the full setup:&lt;/p&gt;

&lt;p&gt;File structure&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── config
│   └── deploy.yml
└── Dockerfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dockerfile (placeholder)&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; nginx:alpine&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; nginx.conf /etc/nginx/conf.d/default.conf&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's literally it. This exists only to satisfy Kamal's builder step.&lt;/p&gt;

&lt;p&gt;config/deploy.yml&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="c1"&gt;# Name of your application.&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;wg-easy&lt;/span&gt;

&lt;span class="c1"&gt;# Placeholder image name (for the proxy app, not the real one).&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;wg-easy-proxy&lt;/span&gt;

&lt;span class="na"&gt;servers&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;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;111.222.333.444&lt;/span&gt; &lt;span class="c1"&gt;# your VPS IP&lt;/span&gt;

&lt;span class="na"&gt;ssh&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu&lt;/span&gt;
  &lt;span class="na"&gt;keys&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;~/.ssh/id_ed25519"&lt;/span&gt;

&lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ssl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example.yourdomain.com&lt;/span&gt;
  &lt;span class="na"&gt;app_port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&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;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;

&lt;span class="c1"&gt;# Image registry (AWS ECR shown here, Docker Hub works too).&lt;/span&gt;
&lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;xxxx.dkr.ecr.us-west-1.amazonaws.com&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;AWS_ECR_CREDENTIALS&lt;/span&gt;

&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;arch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;amd64&lt;/span&gt;
  &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;

&lt;span class="c1"&gt;# wg-easy runs as an accessory — this is where the real image is pulled.&lt;/span&gt;
&lt;span class="na"&gt;accessories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;wg-easy&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/wg-easy/wg-easy:15&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;111.222.333.444&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;clear&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;51821"&lt;/span&gt;
        &lt;span class="na"&gt;HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0"&lt;/span&gt;
        &lt;span class="na"&gt;INSECURE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false"&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cap-add"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NET_ADMIN&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SYS_MODULE&lt;/span&gt;
      &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publish"&lt;/span&gt;&lt;span class="err"&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;51820:51820/udp"&lt;/span&gt;
    &lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sysctl"&lt;/span&gt;&lt;span class="err"&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;net.ipv4.ip_forward=1"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;net.ipv4.conf.all.src_valid_mark=1"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;net.ipv6.conf.all.disable_ipv6=0"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;net.ipv6.conf.all.forwarding=1"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;net.ipv6.conf.default.forwarding=1"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wg_data:/etc/wireguard"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/lib/modules:/lib/modules:ro"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kamal deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Kamal builds and pushes the placeholder nginx image, then pulls and runs wg-easy as an accessory on your VPS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Result
&lt;/h3&gt;

&lt;p&gt;Once deployed, wg-easy gives you a clean web UI to manage WireGuard configurations — no terminal needed.&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.amazonaws.com%2Fuploads%2Farticles%2Fp2m68pb4m9sb125x0u43.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.amazonaws.com%2Fuploads%2Farticles%2Fp2m68pb4m9sb125x0u43.png" alt="Easy WireGuard. We can easily create any WireGuard configuration without touching the terminal."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This approach works well when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want to self-host a public Docker image (Plausible, Uptime Kuma, wg-easy, etc.)&lt;/li&gt;
&lt;li&gt;You already use Kamal and want a consistent deployment workflow&lt;/li&gt;
&lt;li&gt;You want SSL and zero-downtime handling without setting up Kubernetes&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If you found this useful, I share more Rails and DevOps tips on X: &lt;a href="https://x.com/codxse" rel="noopener noreferrer"&gt;@codxse&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>ruby</category>
      <category>kamal</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
