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
Dockerfile (placeholder)
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
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"
Deploy
kamal deploy
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.
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)