How to Deploy CapRover on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Install CapRover on an Ubuntu VPS — replace the default captain42 password before anything else, set up wildcard DNS and HTTPS, deploy a first app, and back up both halves of the platform.
- A VPS with at least 1 GB RAM (2 GB+ if you build apps from source on it)
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access, dedicated to CapRover
- A domain where you can create a wildcard A record
- Docker Engine installed (see the base guide below)
What CapRover is
CapRover is a self-hosted platform-as-a-service: a dashboard and CLI on top of Docker Swarm that builds and runs your apps, routes a subdomain to each through its own nginx, and gets Let's Encrypt certificates for them. You deploy from a Git push, a tarball, a Dockerfile or a prebuilt image, and there is a catalogue of one-click apps (databases, WordPress and many others). It is written in TypeScript on Node.js, and the licence is Apache-2.0 with a CapRover-specific appendix.
CapRover takes over the whole box: it owns ports 80, 443 and 3000, turns the
server into a Swarm manager, and keeps its state in /captain. Give it a
dedicated server rather than installing it next to other things.
If you are choosing a platform, Dokku vs CapRover and CapRover vs Coolify cover the main alternatives, and Dokploy is another Swarm-based option with a more modern UI.
Server sizing
CapRover's docs ask for at least 1 GB of RAM, and add that more memory helps when building applications from source. That is the realistic split:
- 1 GB runs CapRover plus a few small apps deployed as prebuilt images.
- 2–4 GB is the comfortable range if the server builds your apps from source, since builds (npm, Maven, Docker multi-stage) are the memory spikes.
- Disk fills with old image versions. Start with 40 GB+ and turn on the scheduled image cleanup described below.
CapRover publishes AMD64 and ARM64 images, so ARM VPS plans work too.
Prepare the server
This guide assumes Docker Engine is installed. If not, work through Docker & Compose on Ubuntu first — CapRover's docs advise against the snap package of Docker.
Open the ports CapRover needs. 80/tcp and 443/tcp carry app traffic and the
Let's Encrypt challenge, 443/udp is for HTTP/3, and 3000/tcp is the initial
dashboard port:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw allow 3000/tcp
sudo ufw --force enable
sudo ufw status
Check your provider's own firewall as well; it can block traffic that ufw
allows. And keep in mind that ports published by Docker bypass ufw rules, a
point CapRover's firewall page makes too — so an app you later map to a public
port is reachable whatever ufw says.
Paid link — we earn a commission if you shop through it.
Install CapRover
The installer is one container. It initialises Docker Swarm, creates the CapRover services and exits:
sudo docker run -p 80:80 -p 443:443 -p 3000:3000 \
-e ACCEPTED_TERMS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /captain:/captain \
caprover/caprover
ACCEPTED_TERMS=true accepts CapRover's terms non-interactively. Before it
does anything, the installer checks that port 3000 on the server's public
IP is reachable; if your provider's firewall blocks it, the install stops
with Port timed out: 3000 and suggests the ports to open. Setup
continues for a little while after the installer exits; wait for the
dashboard's API to answer:
sudo apt-get install -y jq >/dev/null
for i in $(seq 1 60); do
curl -s http://127.0.0.1:3000/api/v2/login -H 'x-namespace: captain' \
-H 'content-type: application/json' --data '{"password":"-"}' \
| jq -e '.status != 1001' >/dev/null 2>&1 && break
sleep 5
done
sudo docker service ls
On first boot CapRover restarts itself once to load a generated secret, and
until it is done the API answers Captain is not ready yet (status 1001) or
a plain 500. The loop waits for a real answer, which took under a minute on
our test box.
You should see the core Swarm services, including captain-captain (the
dashboard and API) and captain-nginx (the router).
Replace the default password — now
A fresh CapRover dashboard is on http://SERVER_IP:3000, over plain HTTP,
with the password captain42. Anyone who finds the port before you do
owns the server, so change it straight away. You can do it in the dashboard's
Settings, or from the shell through CapRover's documented HTTP API, which
generates a strong password and saves it somewhere only you can read:
if [ ! -f ~/caprover-password.txt ]; then
NEWPASS="$(openssl rand -hex 14)"
TOKEN=$(curl -fsS http://127.0.0.1:3000/api/v2/login \
-H 'x-namespace: captain' -H 'content-type: application/json' \
--data '{"password":"captain42"}' | jq -er '.data.token') &&
curl -fsS http://127.0.0.1:3000/api/v2/user/changepassword/ \
-H 'x-namespace: captain' -H "x-captain-auth: $TOKEN" -H 'content-type: application/json' \
--data "$(jq -n --arg o captain42 --arg n "$NEWPASS" '{oldPassword:$o,newPassword:$n}')" \
| jq -e '.status == 100' >/dev/null &&
echo "$NEWPASS" > ~/caprover-password.txt && chmod 600 ~/caprover-password.txt &&
echo "password changed"
fi
Confirm the old password is rejected and the new one works:
curl -s http://127.0.0.1:3000/api/v2/login -H 'x-namespace: captain' \
-H 'content-type: application/json' --data '{"password":"captain42"}' | jq -r '.description'
curl -s http://127.0.0.1:3000/api/v2/login -H 'x-namespace: captain' \
-H 'content-type: application/json' \
--data "$(jq -n --arg p "$(cat ~/caprover-password.txt)" '{password:$p}')" \
| jq -e '.data.token' >/dev/null && echo "new password: login OK"
The password is 28 characters on purpose. CapRover's login rejects passwords longer than 29 characters ("password is too long"), even though the change-password call accepts longer ones — set a 32-character password and you lock yourself out of the normal login. (The error message says the first 29 characters of a longer password still work.)
The API wraps every reply in its own status field (100 means success), so
an HTTP 200 alone does not mean the call worked — which is why the block above
checks .status before saving the new password.
HTTPS + domain: wildcard DNS
CapRover gives every app a subdomain of one root domain. If the root is
apps.example.com, the dashboard lives at captain.apps.example.com and an
app called api at api.apps.example.com. One wildcard record covers them
all. At your DNS provider, create:
| Type | Name (in the example.com zone) |
Value |
|---|---|---|
| A | *.apps |
your server's public IPv4 |
Check that both captain.apps.example.com and a made-up name such as
random123.apps.example.com resolve to the server:
dig +short captain.apps.example.com
dig +short random123.apps.example.com
If your DNS provider offers a proxy mode (Cloudflare's orange cloud), use DNS-only while setting up, so Let's Encrypt reaches the server directly.
Then in the dashboard at http://SERVER_IP:3000, sign in with the new
password and:
- Set the root domain to
apps.example.com— without*.orcaptain.. - Open
http://captain.apps.example.comand check you can sign in there. - Enable HTTPS for the dashboard. CapRover requests a certificate for
captain.apps.example.com, so ports 80 and 443 and the DNS must already work. - Open
https://captain.apps.example.com, confirm the certificate is valid, then turn on Force HTTPS.
Once the dashboard works over HTTPS on its domain, CapRover's docs say you may close public access to port 3000:
sudo ufw delete allow 3000/tcp
Because Docker-published ports bypass ufw, also block 3000 in your
provider's firewall to be sure.
Deploy a first app
The quickest end-to-end test uses a prebuilt image. In the dashboard:
-
Apps → create an app called
my-first-app. -
Deployment tab → paste this Captain Definition and deploy:
{ "schemaVersion": 2, "imageName": "nginx:stable-alpine" } -
In HTTP Settings, make sure the container HTTP port is
80. -
Visit
http://my-first-app.apps.example.com— the nginx welcome page means DNS, routing and the container all work. -
Click Enable HTTPS for the app, then Force HTTPS.
For your own code, commit a captain-definition file (pointing at your
Dockerfile) and deploy with caprover deploy from the CLI (npm install -g caprover) or connect the app to a Git repository with a webhook. Deploys
upload the committed branch, so uncommitted and .gitignored files are left
out.
Stateful apps need persistent directories set in the app config before they hold real data; without one, the data lives in the container and is lost on the next deploy.
Securing it
- The password change above is the big one. Also enable HTTPS and Force HTTPS for the dashboard, then close port 3000.
- Treat anything with Docker socket access as root on the host. CapRover's own security page says as much: whoever holds the dashboard can run any container.
- Use per-app deployment tokens for CI rather than the dashboard password.
- Do not expose database one-click apps on public ports. Apps reach each other
over the internal Swarm network as
srv-captain--<app-name>.
Backups
A CapRover backup and your app data are two separate things, and you need both:
- CapRover's configuration: Settings → Create Backup, then download
the
.tar. This holds CapRover's own state and settings, not the contents of your apps' volumes. The download link is temporary; keep your own copy off the server. - App data: dump each database with its own tool and archive each persistent directory. Find the volumes with:
docker volume ls
sudo du -sh /captain
To restore on a fresh server, copy the archive to /captain/backup.tar
before running the installer — it detects the file at startup — then point
DNS at the new box and restore each app's data.
Upgrades
Download a CapRover backup and back up app data first. Then in Settings,
the dashboard shows the current and available versions with a changelog; use
its update button. It pulls the new image and updates the captain-captain
service. If something goes wrong:
docker service ps captain-captain --no-trunc
docker service logs captain-captain --tail 100
Keep the host itself patched too (sudo apt upgrade, Docker included).
Troubleshooting
The dashboard on port 3000 does not load. Check sudo docker service ls
shows captain-captain running 1/1, and that port 3000 is open in both
ufw and the provider firewall.
Enabling HTTPS fails. Let's Encrypt must reach the name on port 80. Check the wildcard record resolves to this server, that no DNS proxy is in the way, and that 80/443 are open in the provider firewall.
The app shows a 502. The container HTTP port in HTTP Settings does not match the port the app listens on, or the app crashed; check its logs in the dashboard.
Builds fail on a small server. Usually memory. Check free -h and the
build log. Upstream suggests a swap file to get a build through, but building
the image in CI and deploying the image is the lasting fix.
The disk is filling up. Every deploy keeps an image. Under Settings → disk
cleanup, set how many recent versions per app to keep and a cron schedule. It
only removes images, never volumes. Avoid docker system prune --all or
volume pruning on a CapRover host — it can delete what rollbacks or stopped
apps still need.
Verification + next steps
You're done when: the dashboard loads at https://captain.apps.example.com
with a valid certificate, captain42 no longer works, port 3000 is closed,
and https://my-first-app.apps.example.com serves the test page.
From there, deploy your real apps, set persistent directories before they store anything, and schedule both backups. For hosting picks, see Best VPS for Docker and Best VPS for Node.js.