DEV Community

Nadiar Syaripul
Nadiar Syaripul

Posted on

How to Deploy Any Public Docker Image (like wg-easy) to a VPS Using Kamal

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.

Or so I thought.

In this post, I'll show you a small trick using a placeholder Dockerfile + Kamal accessories to deploy any publicly available Docker image to a VPS — no custom build required.

The Problem

Sometimes you just want to run a pre-built public image on your server. In my case, I needed to deploy wg-easy — a WireGuard UI — on a VPS where I already have SSH access.
Kamal's normal flow assumes:

  • You have a Dockerfile
  • You build and push your own image to a registry
  • Kamal pulls and deploys that image

But for third-party images, step 1 and 2 are unnecessary overhead.

The Trick: Accessories + Placeholder Dockerfile

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.

The workaround:

  • Deploy the real app (e.g. wg-easy) as an accessory
  • Use a minimal placeholder Dockerfile (just an nginx image) as the "main" app to satisfy Kamal's build requirement

Here's the full setup:

File structure

.
├── config
│   └── deploy.yml
└── Dockerfile
Enter fullscreen mode Exit fullscreen mode

Dockerfile (placeholder)

FROM nginx:alpine

COPY nginx.conf /etc/nginx/conf.d/default.conf
Enter fullscreen mode Exit fullscreen mode

That's literally it. This exists only to satisfy Kamal's builder step.

config/deploy.yml

# Name of your application.
service: wg-easy

# Placeholder image name (for the proxy app, not the real one).
image: wg-easy-proxy

servers:
  web:
    hosts:
      - 111.222.333.444 # your VPS IP

ssh:
  user: ubuntu
  keys:
    - "~/.ssh/id_ed25519"

proxy:
  ssl: true
  host: example.yourdomain.com
  app_port: 80
  healthcheck:
    path: /

# Image registry (AWS ECR shown here, Docker Hub works too).
registry:
  server: xxxx.dkr.ecr.us-west-1.amazonaws.com
  username: AWS
  password:
    - AWS_ECR_CREDENTIALS

builder:
  arch: amd64
  dockerfile: Dockerfile

# wg-easy runs as an accessory — this is where the real image is pulled.
accessories:
  wg-easy:
    image: ghcr.io/wg-easy/wg-easy:15
    host: 111.222.333.444
    env:
      clear:
        PORT: "51821"
        HOST: "0.0.0.0"
        INSECURE: "false"
    options:
      "cap-add":
        - NET_ADMIN
        - SYS_MODULE
      "publish":
        - "51820:51820/udp"
      "sysctl":
        - "net.ipv4.ip_forward=1"
        - "net.ipv4.conf.all.src_valid_mark=1"
        - "net.ipv6.conf.all.disable_ipv6=0"
        - "net.ipv6.conf.all.forwarding=1"
        - "net.ipv6.conf.default.forwarding=1"
    volumes:
      - "wg_data:/etc/wireguard"
      - "/lib/modules:/lib/modules:ro"
Enter fullscreen mode Exit fullscreen mode

Deploy

kamal deploy
Enter fullscreen mode Exit fullscreen mode

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

Result

Once deployed, wg-easy gives you a clean web UI to manage WireGuard configurations — no terminal needed.

Easy WireGuard. We can easily create any WireGuard configuration without touching the terminal.

This approach works well when:

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

If you found this useful, I share more Rails and DevOps tips on X: @codxse

Top comments (0)