Split View: 셀프호스팅 배포 플랫폼 고르기 2026 — Coolify·Dokploy·CapRover·Kamal·Openship을 가르는 다섯 축
셀프호스팅 배포 플랫폼 고르기 2026 — Coolify·Dokploy·CapRover·Kamal·Openship을 가르는 다섯 축
- 들어가며 — Openship이 올라온 날, 되돌아온 같은 질문
- 결정을 가르는 다섯 개의 축
- 컨트롤 플레인은 어디에 사는가
- 롤백과 무중단 배포의 실제 모양
- 시크릿 — 가장 자주 미끄러지는 곳
- 후보 비교
- 돈이 아니라 주의력을 지불한다
- 마치며 — 유지보수를 맡을 사람의 이름을 먼저 적으세요
- 참고 자료
들어가며 — Openship이 올라온 날, 되돌아온 같은 질문
GeekNews에 Openship이 올라왔습니다. 설명은 익숙하면서도 야심찹니다 — 저장소를 가리키면 스택을 감지해 컨테이너를 빌드하고, 도메인과 TLS를 설정하고, 배포까지 한다. 설정 파일도, 파이프라인도, YAML도 필요 없다는 것입니다. Apache 2.0 라이선스에 데스크톱 앱·웹 대시보드·CLI·REST API를 모두 제공합니다.
그리고 댓글은 늘 그렇듯 같은 질문으로 수렴합니다. "Coolify랑 뭐가 다른가." "그냥 compose 쓰면 안 되나." "이거 유지보수 누가 하나."
이 글은 그 질문에 답하기 위한 결정 프레임입니다. 기능 목록을 나열하는 비교는 도움이 되지 않습니다. 후보들은 대부분 같은 기능을 광고하고 — 깃 푸시 배포, 자동 TLS, 데이터베이스, 백업, 로그 — 실제로 3개월 뒤에 후회 여부를 가르는 것은 그 목록에 잘 안 적히는 다섯 가지입니다. 미리 밝혀 두면, 이 글의 결론은 "셀프호스팅이 좋다"도 "매니지드가 좋다"도 아닙니다. 셀프호스팅은 돈을 아끼는 대신 주의력을 지불하는 거래이고, 그 환율이 팀마다 다르다는 것입니다.
결정을 가르는 다섯 개의 축
기능표 대신 다음 다섯 질문으로 후보를 거르면 대개 둘 이하로 좁혀집니다.
첫째, 컨트롤 플레인이 어디에 사는가. 항상 떠 있는 대시보드 서버인가, 아니면 명령을 실행할 때만 존재하는 로컬 CLI인가. 이것이 "배포 시스템이 죽으면 무슨 일이 일어나는가"를 결정합니다.
둘째, 롤백이 몇 초에 몇 개의 명령인가. 새벽 3시에 되돌리는 경로가 UI 버튼 하나인지, 이전 이미지 태그를 찾아 손으로 다시 배포하는 것인지, 아니면 문서에도 없는지.
셋째, 시크릿이 어디에 어떤 형태로 저장되는가. 플랫폼의 데이터베이스에 들어간다면 그 데이터베이스의 백업과 접근 권한이 곧 시크릿의 보안 경계가 됩니다.
넷째, 노드가 하나에서 여럿으로 늘어날 때 무엇이 바뀌는가. 대부분의 도구가 "멀티 서버 지원"이라고 적지만, 실제로는 여러 대에 각각 배포하는 것과 클러스터로 다루는 것 사이에 큰 차이가 있습니다.
다섯째, 이 도구를 쓰는 데 매달 몇 시간이 드는가. 이게 매니지드 PaaS 요금과 실제로 비교되는 항목입니다.
컨트롤 플레인은 어디에 사는가
후보들은 이 축에서 뚜렷하게 두 진영으로 갈립니다.
상주 컨트롤 플레인 진영 — Coolify, Dokploy, CapRover, Openship(서버 모드). 서버에 대시보드가 항상 떠 있습니다. Coolify의 경우 컨트롤 플레인이 대시보드·내부 PostgreSQL·깃 웹훅 수신·배포 오케스트레이션을 담당하고, 워커 노드에는 SSH로 접속해 컨테이너를 띄웁니다. 컨트롤 플레인 자체는 2 vCPU / 4GB 정도로 충분하다는 것이 통상적인 권고입니다.
# Coolify 설치 — 스크립트를 먼저 읽고 실행하는 습관을 권합니다
curl -fsSL https://cdn.coollabs.io/coolify/install.sh -o /tmp/coolify-install.sh
less /tmp/coolify-install.sh
bash /tmp/coolify-install.sh
이 진영의 장점은 명확합니다. 배포하는 사람이 백엔드 엔지니어가 아니어도 됩니다. 로그를 보고, 환경 변수를 고치고, 재배포하는 일이 브라우저에서 끝납니다. 원클릭 앱 템플릿으로 Postgres·Redis·모니터링 스택을 몇 분 만에 올릴 수 있습니다.
단점도 그만큼 명확합니다. 컨트롤 플레인이 독립적으로 고장 날 수 있는 부품이 하나 늘어납니다. 대시보드가 죽어도 이미 떠 있는 앱은 계속 돌지만, 그 상태에서는 배포도 롤백도 못 합니다. 그리고 대시보드는 공격 표면이기도 합니다 — 2026년에 Coolify 관련 CVE가 보고되면서 "대시보드를 공개 인터넷에서 빼고 VPN이나 IP 허용 목록 뒤에 두라"는 권고가 반복해서 나왔습니다. 셀프호스팅 배포 도구의 대시보드를 0.0.0.0에 열어 두는 것은 사실상 루트 셸을 열어 두는 것과 같습니다.
무상주 진영 — Kamal 2, 그리고 순수 compose + SSH. 컨트롤 플레인이 없습니다. 개발자 노트북이나 CI 러너에서 명령을 실행하는 동안에만 존재하고, 끝나면 서버에는 컨테이너와 프록시만 남습니다.
# config/deploy.yml — Kamal 2
service: app
image: acme/app
servers:
web:
- 10.0.0.11
- 10.0.0.12
proxy:
ssl: true
host: app.example.com
healthcheck:
path: /up
interval: 3
registry:
server: ghcr.io
username: acme-ci
password:
- KAMAL_REGISTRY_PASSWORD
env:
clear:
RAILS_ENV: production
secret:
- DATABASE_URL
- SECRET_KEY_BASE
accessories:
db:
image: postgres:17
host: 10.0.0.20
env:
secret:
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
kamal setup # 최초 1회: 도커 설치, 프록시 기동, 액세서리 생성
kamal deploy # 빌드 → 푸시 → 서버별 롤링 교체
kamal app logs -f # 전체 서버의 로그 스트림
kamal rollback # 직전 정상 버전으로 되돌리기
kamal proxy reboot # 프록시만 재시작
Kamal 2는 Traefik을 걷어내고 자체 kamal-proxy를 씁니다. 무중단 배포를 위해 만들어진 프록시라, 새 컨테이너가 헬스체크를 통과할 때까지 기다렸다가 트래픽을 넘기는 동작이 기본입니다. Rails 8의 기본 배포 도구이기도 해서 Rails 생태계에서는 사실상 표준이 됐지만, 도커 이미지를 만들 수 있으면 언어는 무관합니다.
이 진영의 장점은 고장 날 컨트롤 플레인이 없다는 것입니다. 단점은 새벽 3시에 볼 UI가 없다는 것 — 로그를 보려면 SSH나 CLI를 써야 하고, 팀에 터미널이 편하지 않은 사람이 있으면 그 사람은 배포 흐름에서 배제됩니다.
Openship은 흥미롭게도 두 모드를 다 제공합니다. 데스크톱 앱이 로컬 컨트롤 플레인이 되어 SSH로 배포하는 모드와, 서버에 상주시켜 깃 푸시 배포를 받는 모드가 따로 있습니다. 다만 이 글 시점의 공개 버전은 0.1.x 대이고 저장소 스스로 "코어는 프로덕션 준비, 활발히 개발 중"이라고 표현합니다. 멀티 노드 클러스터, 사설 네트워킹, 시각적 CI/CD 파이프라인은 아직 예정 항목으로 적혀 있습니다. 신규 프로젝트에서 시험해 볼 만하지만, 이미 돌고 있는 서비스를 옮길 대상으로 삼기에는 이릅니다.
롤백과 무중단 배포의 실제 모양
배포 도구를 고를 때 사람들이 가장 적게 확인하고 가장 자주 후회하는 항목입니다.
무중단 배포는 사실상 모든 후보가 합니다. 방식도 비슷합니다 — 새 컨테이너를 띄우고, 헬스체크를 통과하면, 프록시가 트래픽을 넘기고, 옛 컨테이너를 내립니다. Kamal은 kamal-proxy가, Coolify와 Dokploy는 Traefik 계열이, CapRover는 Docker Swarm의 롤링 업데이트가, Openship은 OpenResty 기반 엣지가 그 역할을 합니다.
차이는 롤백에서 납니다. 확인해야 할 것은 세 가지입니다.
- 이전 이미지가 서버에 남아 있는가. 남아 있지 않으면 롤백은 "레지스트리에서 다시 받기"가 되고, 레지스트리가 접근 불가면 롤백 자체가 불가능해집니다. Kamal은 이전 컨테이너를 남겨 두어
kamal rollback이 즉시 동작합니다. - 롤백에 필요한 정보가 UI나 CLI에서 바로 보이는가. 이전 배포의 이미지 태그를 찾기 위해 레지스트리 콘솔을 뒤져야 한다면 그건 롤백 경로가 없는 것입니다.
- 마이그레이션은 어떻게 되는가. 이건 어떤 도구도 해결해 주지 않습니다. 앞뒤 호환되는 마이그레이션을 쓰고 배포와 스키마 변경을 분리하는 것은 여전히 애플리케이션 쪽 설계 문제입니다.
세 번째가 핵심입니다. 컨테이너 롤백이 30초에 끝나도 스키마를 되돌릴 수 없으면 실제 복구 시간은 30초가 아닙니다. 도구 선택보다 앞서는 결정입니다.
한 가지 더. 롤백을 실제로 연습해 보지 않은 롤백 기능은 없는 것과 같습니다. 스테이징에서 일부러 나쁜 배포를 하고 되돌리는 훈련을 분기에 한 번은 해야 합니다. 백업 복구도 마찬가지입니다 — 백업이 돌고 있다는 사실과 복구가 된다는 사실은 별개입니다.
시크릿 — 가장 자주 미끄러지는 곳
여기서 후보들의 성격 차이가 가장 크게 드러납니다.
대시보드형 도구는 대개 시크릿을 자체 데이터베이스에 저장합니다. UI에서 입력하고, 배포 시 환경 변수로 주입합니다. 편하지만 결과적으로 그 데이터베이스가 시크릿 저장소가 됩니다. 확인할 것은 셋입니다 — 저장 시 암호화가 되는가, 그 암호화 키는 어디에 있는가(같은 서버에 있다면 실질적 보호는 제한적입니다), 대시보드 백업에 시크릿이 그대로 들어가는가.
Kamal은 반대 방향입니다. 시크릿을 자기가 보관하지 않고, 배포 시점에 로컬에서 읽어 컨테이너에 주입합니다. .kamal/secrets 파일이 그 통로인데, 이 파일 자체를 커밋하지 않고 외부 볼트에서 채우는 것이 권장 방식입니다.
# .kamal/secrets — 값이 아니라 "가져오는 방법"을 적는다
KAMAL_REGISTRY_PASSWORD=$(op read "op://infra/ghcr/token")
DATABASE_URL=$(op read "op://infra/app/database-url")
SECRET_KEY_BASE=$(op read "op://infra/app/secret-key-base")
# CI에서는 같은 자리를 CI 시크릿으로 채운다
# .kamal/secrets.production
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
DATABASE_URL=$DATABASE_URL
이 방식의 장점은 시크릿의 단일 출처가 배포 도구 바깥에 남는다는 것입니다. 배포 플랫폼을 갈아치워도 시크릿 관리 체계는 그대로입니다. 단점은 한 겹 더 설정해야 한다는 것과, 볼트가 죽으면 배포가 막힌다는 것입니다.
어느 쪽을 고르든 지켜야 할 최소선은 같습니다. 시크릿을 이미지에 굽지 말고, 빌드 로그에 찍히지 않게 하고, 로테이션 절차를 문서로 남기고, 배포 도구의 백업에 시크릿이 평문으로 들어가는지 한 번은 직접 확인하는 것입니다.
후보 비교
| 도구 | 컨트롤 플레인 | 롤백 | 시크릿 저장 위치 | 멀티 노드 | 상태 / 라이선스 |
|---|---|---|---|---|---|
| Coolify | 상주 대시보드 + SSH 워커 | UI에서 이전 배포 선택 | 플랫폼 DB | 컨트롤 플레인 1대 + SSH 워커 N대 | 성숙, 커뮤니티 큼 / Apache 2.0 |
| Dokploy | 상주 대시보드(경량) | UI에서 이전 배포 선택 | 플랫폼 DB | Docker Swarm 기반 | 활발, Coolify보다 가벼움 / Apache 2.0 |
| CapRover | 상주 대시보드 | 이전 이미지 태그로 재배포 | 플랫폼 설정 | Docker Swarm 네이티브 | 오래됨, 안정적 / Apache 2.0 |
| Kamal 2 | 없음(CLI 실행 시에만) | kamal rollback 즉시 | 외부 볼트에서 배포 시 주입 | 서버 목록에 나열, 롤링 배포 | 성숙, Rails 8 기본 / MIT |
| Openship | 데스크톱·서버·클라우드 선택 | 문서상 지원 | 플랫폼 관리 | 예정 항목 | 0.1.x, 초기 / Apache 2.0 |
| compose + 러너 | 없음(CI가 실행) | 직접 작성해야 함 | CI 시크릿 또는 볼트 | 서버마다 별도 구성 | 도구 없음, 전부 자작 |
표를 한 줄로 요약하면 이렇습니다. 팀에 터미널이 편하지 않은 사람이 있으면 대시보드형, 전원이 백엔드 엔지니어면 Kamal이나 compose. 이 기준 하나가 실제 만족도의 대부분을 설명합니다.
순수 compose 조합도 진지한 선택지입니다. 서버에 docker compose와 systemd 유닛을 두고, GitHub Actions 셀프호스티드 러너나 웹훅 수신기가 git pull && docker compose up -d를 실행하는 구성입니다. 서비스가 한두 개고 배포가 주 1회라면 이 구성이 가장 적은 개념으로 가장 오래 갑니다. 다만 무중단 배포와 롤백은 직접 만들어야 하고, 그 부분을 안 만들면 배포할 때마다 몇 초씩 끊깁니다. 그 몇 초를 감당할 수 있는지가 판단 기준입니다.
돈이 아니라 주의력을 지불한다
셀프호스팅 논의에서 가장 흔한 왜곡은 비용 비교를 요금만으로 하는 것입니다. 매니지드 PaaS 월 200달러와 VPS 월 40달러를 비교하면 결론은 뻔합니다. 그런데 그 200달러에는 우리가 하지 않던 일이 포함돼 있습니다.
셀프호스팅으로 넘어오면 다음이 우리 일이 됩니다.
- OS 보안 패치와 재부팅 일정
- 도커 엔진과 배포 플랫폼 자체의 업그레이드(그리고 업그레이드가 깨졌을 때의 복구)
- 디스크 사용량 관리 — 오래된 이미지와 빌드 캐시가 디스크를 채워 배포가 멈추는 일은 정말 흔합니다
- 백업 스케줄과 복구 훈련
- 대시보드 접근 통제, VPN 또는 허용 목록 유지
- 인증서 갱신 실패 시 대응(자동화돼 있어도 실패는 납니다)
- 그리고 이 모든 것에 대한 온콜
시간으로 환산하면 팀마다 다르지만, 서비스 몇 개 규모에서 정상 상태일 때 월 2~4시간, 무언가 깨진 달에는 하루가 통째로 들어가는 것이 흔한 범위입니다. 엔지니어 시간을 시간당 얼마로 잡느냐에 따라 이 계산은 쉽게 뒤집힙니다.
그래서 정직한 결론은 이렇습니다.
- 셀프호스팅이 유리한 경우: 인프라를 다룰 줄 아는 사람이 이미 팀에 있고, 서버가 어차피 필요하고(백그라운드 워커, 상시 실행 프로세스, GPU), 데이터 위치에 대한 요구가 있고, 트래픽 대비 매니지드 요금이 비대해진 경우.
- 매니지드가 유리한 경우: 팀이 작고 제품에 집중해야 하고, 트래픽이 예측 불가능하고, 온콜을 세울 사람이 없고, 배포가 잦고, 규정 요구가 없는 경우.
- 최악의 경우: 셀프호스팅으로 옮겼는데 아무도 유지보수를 맡지 않는 경우. 이건 매니지드보다도 비쌉니다 — 6개월 뒤 아무도 업그레이드하지 못하는 플랫폼 위에서 프로덕션이 돌게 됩니다.
컨테이너 런타임을 옮기는 일 자체의 실무 이슈는 도커에서 팟맨으로 편을 참고하세요.
마치며 — 유지보수를 맡을 사람의 이름을 먼저 적으세요
Openship 같은 새 도구가 계속 나오는 이유는 이 문제가 아직 잘 풀리지 않았기 때문입니다. 매니지드 PaaS의 편의와 자기 서버의 통제권을 동시에 갖고 싶다는 요구는 정당하고, Coolify·Dokploy·Kamal은 각자 다른 지점에서 그 요구에 답합니다.
- 후보를 다섯 축으로 거르세요 — 컨트롤 플레인 위치, 롤백 경로, 시크릿 위치, 멀티 노드, 월간 운영 시간. 기능표는 그다음입니다.
- 대시보드형을 고른다면 대시보드를 공개 인터넷에서 빼세요. VPN이나 IP 허용 목록 뒤가 기본입니다.
- Kamal이나 compose를 고른다면 롤백과 로그 조회를 문서로 남기세요. UI가 없는 만큼 절차가 지식이 됩니다.
- Openship은 0.1.x 대입니다. 신규 사이드 프로젝트에서 먼저 만나 보고, 멀티 노드가 필요한 워크로드는 로드맵이 실제로 도착한 뒤에 고려하는 편이 맞습니다.
- 도입 문서 첫 줄에 이 플랫폼의 업그레이드를 책임질 사람의 이름을 적으세요. 그 이름이 비어 있으면 도구 비교는 의미가 없습니다.
셀프호스팅은 비용을 없애지 않습니다. 청구서에서 캘린더로 옮길 뿐입니다.
참고 자료
Choosing a Self-Hosted Deployment Platform in 2026 — The Five Axes That Separate Coolify, Dokploy, CapRover, Kamal, and Openship
- Introduction — The Day Openship Landed, the Same Question Came Back
- The Five Axes That Decide It
- Where the Control Plane Lives
- What Rollback and Zero-Downtime Deploys Actually Look Like
- Secrets — Where Things Slip Most Often
- Comparing the Candidates
- You're Not Paying Money — You're Paying Attention
- Conclusion — Write Down Who Owns Maintenance First
- References
Introduction — The Day Openship Landed, the Same Question Came Back
Openship landed on GeekNews. The pitch is familiar yet ambitious — point it at a repository, and it detects the stack, builds the container, sets up the domain and TLS, and deploys. No config file, no pipeline, no YAML required. It's Apache 2.0 licensed and ships a desktop app, a web dashboard, a CLI, and a REST API.
And the comments, as always, converged on the same questions: "How is this different from Coolify?" "Why not just use compose?" "Who's going to maintain this?"
This post is a decision framework meant to answer that question. A feature-list comparison won't help — most of the candidates advertise the same things: git-push deploys, automatic TLS, databases, backups, logs. What actually decides whether you regret the choice three months later is five things that rarely make it onto that list. Up front: this post's conclusion is neither "self-hosting is better" nor "managed is better." Self-hosting is a trade where you pay attention instead of money, and the exchange rate is different for every team.
The Five Axes That Decide It
Instead of a feature matrix, filtering candidates through the following five questions usually narrows the field to two or fewer.
First, where does the control plane live? Is it a dashboard server that's always up, or a local CLI that only exists while a command is running? This determines what happens when the deployment system itself goes down.
Second, how many seconds and how many commands does a rollback take? At 3 a.m., is reverting a single UI button, manually hunting down the previous image tag and redeploying by hand, or not even documented?
Third, where and in what form are secrets stored? If they land in the platform's own database, that database's backups and access controls become the secret's actual security boundary.
Fourth, what changes when you go from one node to several? Most tools say "multi-server support," but there's a large gap in practice between deploying to several boxes individually and treating them as a cluster.
Fifth, how many hours a month does running this tool cost you? This is the number that actually compares against a managed PaaS bill.
Where the Control Plane Lives
On this axis, the candidates split cleanly into two camps.
The resident-control-plane camp — Coolify, Dokploy, CapRover, and Openship (server mode). A dashboard sits on the server, always running. In Coolify's case, the control plane handles the dashboard, an internal PostgreSQL, git webhook ingestion, and deployment orchestration, then reaches worker nodes over SSH to run containers. The common recommendation is that the control plane itself is fine on roughly 2 vCPU / 4GB.
# Install Coolify — make it a habit to read the script before running it
curl -fsSL https://cdn.coollabs.io/coolify/install.sh -o /tmp/coolify-install.sh
less /tmp/coolify-install.sh
bash /tmp/coolify-install.sh
This camp's advantage is clear: whoever deploys doesn't need to be a backend engineer. Viewing logs, editing environment variables, and redeploying all happen in a browser. One-click app templates can bring up Postgres, Redis, or a monitoring stack in minutes.
The downside is just as clear. You now have one more component that can fail independently — the control plane. If the dashboard goes down, apps that are already running keep running, but you can neither deploy nor roll back in that state. The dashboard is also an attack surface — after Coolify-related CVEs were reported in 2026, the recommendation to "pull the dashboard off the public internet and put it behind a VPN or an IP allowlist" kept coming up. Leaving a self-hosted deployment tool's dashboard open on 0.0.0.0 is effectively the same as leaving a root shell open.
The no-resident camp — Kamal 2, and plain compose + SSH. There is no control plane. It exists only while a command runs on a developer's laptop or a CI runner; once the command finishes, only containers and a proxy remain on the server.
# config/deploy.yml — Kamal 2
service: app
image: acme/app
servers:
web:
- 10.0.0.11
- 10.0.0.12
proxy:
ssl: true
host: app.example.com
healthcheck:
path: /up
interval: 3
registry:
server: ghcr.io
username: acme-ci
password:
- KAMAL_REGISTRY_PASSWORD
env:
clear:
RAILS_ENV: production
secret:
- DATABASE_URL
- SECRET_KEY_BASE
accessories:
db:
image: postgres:17
host: 10.0.0.20
env:
secret:
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
kamal setup # first time only: installs Docker, starts the proxy, creates accessories
kamal deploy # build -> push -> rolling replace per server
kamal app logs -f # stream logs from every server
kamal rollback # revert to the last healthy version
kamal proxy reboot # restart only the proxy
Kamal 2 dropped Traefik in favor of its own kamal-proxy. Because it was built specifically for zero-downtime deploys, waiting for a new container to pass its health check before handing over traffic is the default behavior. It's also Rails 8's default deployment tool, which has made it a de facto standard in the Rails ecosystem, but the language doesn't matter as long as you can produce a Docker image.
This camp's advantage is that there's no control plane to fail. The downside is that there's no UI to check at 3 a.m. — viewing logs means using SSH or the CLI, and anyone on the team who isn't comfortable with a terminal is effectively excluded from the deploy flow.
Interestingly, Openship offers both modes: a desktop app that acts as a local control plane and deploys over SSH, and a separate mode where it's installed resident on the server to receive git-push deploys. That said, the public version as of this writing is in the 0.1.x range, and the repository itself describes it as "core is production-ready, actively developed." Multi-node clustering, private networking, and a visual CI/CD pipeline are all still listed as upcoming. It's worth trying on a new project, but it's premature as a target for migrating a service that's already running.
What Rollback and Zero-Downtime Deploys Actually Look Like
This is the item people check least when picking a deployment tool, and regret most often.
Virtually every candidate does zero-downtime deploys, and the mechanism is similar too — bring up a new container, and once it passes its health check, the proxy hands over traffic and the old container comes down. Kamal's kamal-proxy, Coolify and Dokploy's Traefik-based setups, CapRover's Docker Swarm rolling updates, and Openship's OpenResty-based edge all play that role.
The difference shows up in rollback. There are three things to check.
- Does the previous image stay on the server? If not, a rollback becomes "pull it again from the registry," and if the registry is unreachable, rollback becomes impossible outright. Kamal keeps the previous container around, so
kamal rollbackworks instantly. - Is the information a rollback needs visible right in the UI or CLI? If you have to dig through a registry console to find the previous deploy's image tag, you effectively have no rollback path.
- What happens to migrations? No tool solves this for you. Using backward/forward-compatible migrations and decoupling deploys from schema changes remains a design problem on the application side.
The third point is the crux. Even if a container rollback finishes in 30 seconds, if you can't revert the schema, your actual recovery time isn't 30 seconds. This is a decision that precedes tool choice.
One more thing. A rollback feature you've never actually practiced is as good as no rollback feature. You should deliberately ship a bad deploy to staging and practice reverting it at least once a quarter. The same goes for backup restores — the fact that backups are running and the fact that they can be restored are two separate claims.
Secrets — Where Things Slip Most Often
This is where the candidates' underlying character shows up most starkly.
Dashboard-style tools generally store secrets in their own database. You type them into the UI, and they're injected as environment variables at deploy time. Convenient, but the consequence is that database becomes your secret store. Three things to check: whether it's encrypted at rest, where the encryption key lives (if it's on the same server, the actual protection is limited), and whether secrets end up in dashboard backups as-is.
Kamal goes the opposite direction. It doesn't hold secrets itself; it reads them locally at deploy time and injects them into the container. The .kamal/secrets file is the conduit, and the recommended practice is to never commit that file and instead populate it from an external vault.
# .kamal/secrets — write "how to fetch it," not the value itself
KAMAL_REGISTRY_PASSWORD=$(op read "op://infra/ghcr/token")
DATABASE_URL=$(op read "op://infra/app/database-url")
SECRET_KEY_BASE=$(op read "op://infra/app/secret-key-base")
# in CI, the same slots get filled from CI secrets
# .kamal/secrets.production
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
DATABASE_URL=$DATABASE_URL
The advantage of this approach is that the single source of truth for secrets stays outside the deployment tool. Even if you swap out the deployment platform, your secret-management setup stays intact. The downside is one more layer to configure, and if the vault goes down, deploys are blocked.
Whichever side you choose, the minimum bar is the same: don't bake secrets into the image, keep them out of build logs, document your rotation procedure, and personally verify at least once whether secrets end up in plaintext inside the deployment tool's backups.
Comparing the Candidates
| Tool | Control Plane | Rollback | Where Secrets Live | Multi-Node | Status / License |
|---|---|---|---|---|---|
| Coolify | Resident dashboard + SSH workers | Pick a previous deploy in the UI | Platform DB | 1 control plane + N SSH workers | Mature, large community / Apache 2.0 |
| Dokploy | Resident dashboard (lightweight) | Pick a previous deploy in the UI | Platform DB | Docker Swarm-based | Active, lighter than Coolify / Apache 2.0 |
| CapRover | Resident dashboard | Redeploy the previous image tag | Platform config | Native Docker Swarm | Long-standing, stable / Apache 2.0 |
| Kamal 2 | None (only while the CLI runs) | kamal rollback, instant | Injected at deploy time from an external vault | Listed in the server list, rolling deploy | Mature, Rails 8 default / MIT |
| Openship | Choice of desktop, server, or cloud | Supported per the docs | Platform-managed | Upcoming | 0.1.x, early / Apache 2.0 |
| compose + runner | None (CI runs it) | You build it yourself | CI secrets or a vault | Configured separately per server | No tool, all DIY |
The table boils down to one line: if anyone on the team isn't comfortable with a terminal, go dashboard-style; if everyone is a backend engineer, go Kamal or compose. This one criterion accounts for most of the real-world satisfaction difference.
Plain compose is also a serious option: put docker compose and a systemd unit on the server, and have a GitHub Actions self-hosted runner or a webhook receiver run git pull && docker compose up -d. If you have one or two services and deploy weekly, this setup goes the longest on the fewest concepts. But you have to build zero-downtime deploys and rollback yourself, and if you skip that part, every deploy causes a few seconds of downtime. Whether you can tolerate those few seconds is the deciding factor.
You're Not Paying Money — You're Paying Attention
The most common distortion in self-hosting discussions is comparing cost by price alone. Weigh a managed PaaS at $200 a month against a VPS at $40 a month, and the conclusion looks obvious. But that $200 bill also covers work we wouldn't otherwise be doing ourselves.
Move to self-hosting, and the following becomes your job.
- OS security patches and reboot scheduling
- Upgrading the Docker engine and the deployment platform itself (and recovering when an upgrade breaks)
- Disk usage management — old images and build caches filling the disk and stalling deploys is genuinely common
- Backup schedules and restore drills
- Dashboard access control, keeping a VPN or an allowlist in place
- Handling certificate renewal failures (even automated, they still fail sometimes)
- And on-call for all of the above
Converted into hours, it varies by team, but for a handful of services, 2-4 hours a month in a normal state and a whole day in a month when something breaks is a common range. Depending on what you value an engineer-hour at, this math flips easily.
So here's the honest conclusion.
- Self-hosting wins when: someone on the team already knows infrastructure, you need servers anyway (background workers, always-on processes, GPUs), you have data-residency requirements, or your managed bill has bloated relative to your traffic.
- Managed wins when: the team is small and needs to focus on the product, traffic is unpredictable, there's no one to put on call, deploys are frequent, and there are no compliance requirements.
- The worst case: you move to self-hosting and no one actually owns the upkeep. This ends up more expensive than managed ever was — six months later, production is running on a platform nobody can upgrade anymore.
For the practical issues of migrating the container runtime itself, see the Docker to Podman migration piece.
Conclusion — Write Down Who Owns Maintenance First
New tools like Openship keep appearing because this problem still isn't well solved. Wanting both the convenience of a managed PaaS and control over your own servers is a legitimate ask, and Coolify, Dokploy, and Kamal each answer it from a different angle.
- Filter candidates through the five axes — control plane location, rollback path, where secrets live, multi-node behavior, monthly operational hours. The feature matrix comes after that.
- If you go with a dashboard-style tool, pull the dashboard off the public internet. Behind a VPN or an IP allowlist should be the default.
- If you go with Kamal or compose, document rollback and log access. With no UI, procedure becomes institutional knowledge.
- Openship is still 0.1.x. Try it on a new side project first, and hold off on workloads that need multi-node until the roadmap actually lands.
- On the first line of your adoption doc, write down the name of the person who owns upgrading this platform. If that name is blank, comparing tools is pointless.
Self-hosting doesn't eliminate the cost. It just moves it from the invoice to the calendar.
References
- Openship — self-hosted deployment platform repository (Apache 2.0)
- Coolify — official site and docs
- Dokploy — official site
- CapRover — official site
- Kamal — official docs and the 2.x upgrade guide
- basecamp/kamal-proxy — Kamal 2's own proxy
- Docker Compose file reference
- Docker to Podman migration (related post)