CapRover’s new one-line installer refuses to run on Debian, so the CapRover Docker install is a manual job. You add Docker Engine from Docker’s own apt repository, then run the CapRover container on top. It is about fifteen minutes from a fresh Debian 13 server to your first app on its own subdomain.
You’ll install Docker, run the CapRover container (with the fix for servers behind NAT), set the root domain in the redesigned dashboard, and deploy a Node.js app from a Dockerfile with the CapRover CLI. You will also hit the 502 that most first deploys run into, and fix it. Built and run on Debian 13 with CapRover 1.16.0 in November 2026.
Check what the server needs
CapRover takes over ports 80 and 443 and turns the machine into a single-node Docker Swarm, so give it a server that isn’t already running a web server or other containers. The table puts the CapRover docs’ minimums next to the lab VM this guide ran on. The docs only name Ubuntu 24.04, so the Debian row is what was tested here, not an official listing.
| Item | Minimum | Lab VM |
|---|---|---|
| OS | amd64 or arm64 (Debian not listed by the docs) | Debian 13 (trixie), amd64 |
| RAM | 1 GB, 2 GB if builds run on the server | 4 GB |
| Free disk | 5 GB | 18 GB |
| DNS | A wildcard A record you control | sslip.io name for a LAN IP |
| Inbound ports | TCP 80, 443, 3000 | Open on the LAN |
With CapRover and two copies of the sample app running, the VM used about 720 MB of RAM and 3.9 GB of disk. A 2 GB VPS is a comfortable start, and 1 GB only works if you build images somewhere else. You also need a user with sudo rights. The Debian 13 cloud image used here had no UFW installed, and Docker’s published ports bypass UFW anyway (Docker’s own docs warn about it). The rules that actually protect you are the ones at your cloud provider. Restrict port 3000 there once the dashboard works over HTTPS, as the CapRover docs advise. Swarm also listens on 2377/tcp, 7946/tcp and udp, and 4789/udp; keep those closed to the internet on a single-node install, which is the same advice the installer check prints.
Skip the one-line installer on Debian
The CapRover getting-started page now leads with an installer script from get.caprover.com that installs Docker for you and runs preflight checks. It’s written for Ubuntu 24.04 only, and its OS check stops on anything else. You can confirm that on your own server with its read-only mode, which changes nothing.
curl -fsSL https://get.caprover.com -o caprover-install.sh
sudo bash caprover-install.sh --check
On the Debian 13 box, every hardware check passed and the OS check failed, which blocks the install. The capture below is trimmed to the lines that matter, so it shows one of the three warnings.

There is no flag to get past it. --allow-other-ubuntu only relaxes the Ubuntu version check. The Docker step inside the script is also hardwired to Docker’s Ubuntu repository, so even a patched OS check would add the wrong apt source to your Debian box. The script is still worth running for its other checks, because it caught the NAT on this box (the public egress IP wasn’t on any local interface) before anything was installed. That warning predicts the port error you will meet two sections from now. After the check, delete the script and do the two steps it would have done yourself.
Install Docker Engine from Docker’s Debian repository
CapRover needs Docker Engine 25 or newer, and its manual setup page installs docker-ce from Docker’s apt repository, so skip Debian’s own docker.io package. Remove any distro Docker packages first. On a fresh server apt just reports that none are installed.
for pkg in docker.io docker-compose docker-doc docker-buildx podman-docker containerd runc; do sudo apt-get remove -y $pkg; done
Add Docker’s signing key and repository. The codename comes from /etc/os-release, so the same lines work on Debian 12 if that’s what you’re running.
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
printf "Types: deb\nURIs: https://download.docker.com/linux/debian\nSuites: %s\nComponents: stable\nArchitectures: %s\nSigned-By: /etc/apt/keyrings/docker.asc\n" "$(. /etc/os-release && echo "$VERSION_CODENAME")" "$(dpkg --print-architecture)" | sudo tee /etc/apt/sources.list.d/docker.sources
The tee echoes the file back, and on trixie it should name the right suite and architecture:
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: trixie
Components: stable
Architectures: amd64
Signed-By: /etc/apt/keyrings/docker.asc
Now install the engine with its plugins. CapRover only uses the engine, but Compose and Buildx are handy for anything you run next to it.
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
That took 82 seconds on the lab VM. The service starts and is enabled at boot on its own, so you can go straight to the checks: the version, a test container and the Swarm state, in one go.
sudo docker version --format "Client: {{.Client.Version}} Server: {{.Server.Version}} API: {{.Server.APIVersion}}"
docker compose version
sudo docker run --rm hello-world | head -3
sudo docker info --format "Storage: {{.Driver}} Cgroup: {{.CgroupDriver}} v{{.CgroupVersion}} Swarm: {{.Swarm.LocalNodeState}}"
You want a server version well past 25, the hello-world greeting, and Swarm: inactive, because CapRover refuses to take over a Swarm that already exists.

Debian 13 runs cgroup v2 with the systemd driver and the overlayfs storage driver, which is what CapRover expects. The standalone Docker CE guide for Debian 13 and 12 covers rootless use, the docker group and uninstalling, none of which CapRover needs.
Point a wildcard DNS record at the server
Every app gets its own subdomain under one root domain, and CapRover’s Nginx picks the app by host name, so all of those names have to land on this server. Create one A record with a wildcard host. For a root of apps.example.com, the record you add at your DNS provider looks like this.
Type: A Name: *.apps Value: 203.0.113.10
Leave it DNS-only (no Cloudflare orange cloud) during setup, which is also what the CapRover docs ask for. Before you go on, check that a name under it resolves to the server, because every later step depends on it.
getent hosts captain.apps.example.com
If you have no domain yet, sslip.io covers a LAN test. It maps any name with an IP in it back to that IP, so captain.192-168-1-126.sslip.io resolved to the lab VM at 192.168.1.126 with zero setup, and that is what the rest of this guide uses. It works over HTTP only, though, because Let’s Encrypt can’t reach a private address. Switch to a real domain for anything you plan to keep, since moving the root domain later means every app URL changes with it.
Run the CapRover container
CapRover installs itself from a one-shot container that checks the host, starts Swarm and creates its own services. On a server that owns a public IP (most VPS plans), you run the command from the docs as is.
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
If it finishes with a “please wait at least 60 seconds” line, you can skip the next part. The lab VM sat behind NAT, so it didn’t, and a home server or a VM on your laptop will fail the same way.
Installation failed. Error: Port seems to be closed: 80
The installer finds the server’s public IP and connects back to it on port 80. Behind NAT that address belongs to your router, and on a cloud server the same error means the provider firewall still blocks 80. The log names both causes, so read it before you change anything.
Are you trying to run CapRover on a local machine or a machine without a public IP?
In that case, you need to add this to your installation command:
-e MAIN_NODE_IP_ADDRESS='127.0.0.1'
...
Installation failed.
Error: Port seems to be closed: 80
On a cloud server, open 80, 443 and 3000 at the provider and rerun the same command. On a LAN box, remove the failed container and pass the server’s LAN address, the same IP your wildcard name points at. This run goes detached, so the third line follows its log.
sudo docker rm $(sudo docker ps -aq --filter ancestor=caprover/caprover)
sudo docker run -d -p 80:80 -p 443:443 -p 3000:3000 -e ACCEPTED_TERMS=true -e MAIN_NODE_IP_ADDRESS=192.168.1.126 -v /var/run/docker.sock:/var/run/docker.sock -v /captain:/captain caprover/caprover
sudo docker logs -f $(sudo docker ps -lq)
Swarm was up 26 seconds later. Two warnings in the log are Debian-specific, and both are harmless.

“Non-generic kernel detected” fires because Debian’s cloud image runs the -cloud-amd64 kernel flavour, and “work best on Ubuntu” is printed on every non-Ubuntu host. Neither stopped the install, and nothing later in this guide behaved differently because of them. Give it the full 60 seconds before you open the dashboard. The first login attempt here, a few seconds early, came back with 1001 : Captain is not ready yet..., which looks like a failure but only means the API is still starting. Once it’s ready, the CapRover services show one replica each.
sudo docker service ls --format "{{.Name}} {{.Replicas}} {{.Image}}"
Right after install there were only two, the dashboard and the router:
captain-captain 1/1 caprover/caprover:1.16.0
captain-nginx 1/1 nginx:1.31
A third service, captain-certbot, showed up later on its own after the root domain was set, so don’t chase it if it’s missing at this point. Older guides list three services straight after install, which is why a two-line list looks wrong when you compare. A service stuck at 0/1 is the thing to worry about, and sudo docker service ps <name> prints the reason.
Set the root domain and change the password
Open http://192.168.1.126:3000 (your server’s IP) and log in with the default password, captain42. The redesigned dashboard drops you on a Setup page that shows Root domain, HTTPS and Enforce HTTPS as not configured. Scroll down to the root domain card.

- 1Root domain
- 2Update Domain
- Type the root domain without
*.into the Root domain field (here192-168-1-126.sslip.io). - Click Update Domain.
The dashboard moves to http://captain.192-168-1-126.sslip.io and asks you to log in again. The sslip.io name passed CapRover’s domain check here without help. If a private-only domain fails at Update Domain, the CapRover advanced installation docs have you write {"skipVerifyingDomains":true} to /captain/data/config-override.json before installing, which this lab didn’t need. On a public server with real DNS, Enable HTTPS and then Force HTTPS sit right under the field; they stay greyed out until the domain is set. A LAN box stops at HTTP.
Change captain42 straight away, because anyone who can reach port 3000 knows it. In the new layout the password form lives in the Security card on the Settings page, not on Setup.

- 1Settings
- 2Change Password
- Open Settings in the sidebar.
- Enter
captain42as the old password, the new one twice, then click Change Password.
To be sure it took, try the old password against the login API from the server. A rejection is what you want here.
curl -s -X POST http://localhost:3000/api/v2/login -H "x-namespace: captain" -H "Content-Type: application/json" -d '{"password":"captain42"}'
Status 1105 means the default no longer works:
{"status":1105,"description":"Invalid credentials","data":{}}
If you get a token back instead, the change didn’t save, so go back to Settings and repeat it before you open port 3000 to anything wider than your LAN.
Install the CapRover CLI with Debian’s Node.js
The CLI pushes your code to the server and needs Node.js 20 or newer. Debian 13’s own repository ships Node 20, so there’s no NodeSource repo to add, which isn’t true on Ubuntu 24.04 (Node 18 there). You can install it on your workstation or, as here, on the server itself.
sudo apt-get install -y nodejs npm git
sudo npm install -g caprover
caprover --version
The global npm install took 15 seconds and caprover --version printed the CLI version. If you want a newer Node line, the Node.js on Debian guide covers the other sources. Log in once and give the machine a short name, because every later command refers to the server by that name.
caprover login -u http://captain.192-168-1-126.sslip.io -p 'your-password' -n qs-01
The token is saved locally, so the password doesn’t go in any later command.
Logged in successfully.
Authorization token is now saved as qs-01 at http://captain.192-168-1-126.sslip.io.
Use https:// in that URL once HTTPS is forced on a public server. The caprover serversetup wizard is the other route through the setup steps above, and it needs the same Node version.
Deploy your first app
An app has to exist in CapRover before the CLI can deploy to it, so you create it in the dashboard first. The Apps page now opens a menu instead of a single form.

- 1Create App
- 2Empty App
- Open Apps and click Create App.
- Pick Empty App, the entry for your own code or image.
- Enter
hello-nodeas the Application Name, leave Has Persistent Data unticked, and click Create App.

- 1Application Name
- 2Create App
The other two entries in that menu, One-Click App and Docker Compose, are for packaged apps and compose files, so leave them for later. Now build the app itself, a small Node.js server with no dependencies, in a folder that will become a Git repository.
mkdir ~/hello-node && cd ~/hello-node
vim server.js
It answers every request with the container’s hostname and an APP_ENV value, which makes scaling and config changes easy to see from curl.
const http = require("http");
const os = require("os");
const port = process.env.PORT || 3000;
http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end(`Hello from CapRover on ${os.hostname()} (${process.env.APP_ENV || "dev"})\n`);
}).listen(port, () => console.log(`listening on ${port}`));
Add a minimal package.json next to it with vim package.json:
{
"name": "hello-node",
"version": "1.0.0",
"main": "server.js",
"scripts": { "start": "node server.js" }
}
Then the Dockerfile. CapRover builds this on the server, and port 3000 here matters in a minute:
FROM node:24-alpine
WORKDIR /app
COPY package.json server.js ./
ENV PORT=3000
EXPOSE 3000
CMD ["node", "server.js"]
Last comes captain-definition, the file CapRover reads to find the Dockerfile:
{
"schemaVersion": 2,
"dockerfilePath": "./Dockerfile"
}
The CLI deploys the last commit on a branch, not the files on disk, so commit before every deploy. A new server has no Git identity, so set one first or Git refuses to commit.
git config --global user.name "Your Name"
git config --global user.email [email protected]
git init -b main && git add . && git commit -m "first deploy"
caprover deploy -n qs-01 -a hello-node -b main
The build log streams back to your terminal. The whole deploy took 10.5 seconds, most of it pulling node:24-alpine:

“Deployed successfully” only means the container started, not that anything answers on the URL it prints.
NGINX 502 Error after the first deploy
Open http://hello-node.192-168-1-126.sslip.io in a browser right after that first deploy and CapRover’s own error page comes back instead of your app.

CapRover routes every app to port 80 inside its container unless told otherwise, and this one listens on 3000, so Nginx finds nothing there. The EXPOSE 3000 line doesn’t change that, since CapRover never reads it. You fix it with one field on the app’s HTTP Settings tab.

- 1Container HTTP Port
- 2Save & Restart
- Set Container HTTP Port to
3000. - Click Save & Restart and give it about 15 seconds.
Then hit the app again from any machine that resolves the name.
curl http://hello-node.192-168-1-126.sslip.io/
That’s your first app live, answering from its container ID:
Hello from CapRover on 91905fd83390 (dev)
You can avoid the detour entirely by making your app listen on port 80 in the container. Most real frameworks default to 3000, 5000 or 8080, though, so expect to set this field on almost every app you bring over, and check it first whenever a fresh deploy returns 502. New apps also get WebSocket support switched on by default now, so you won’t need that checkbox for a typical Node app.
Add an environment variable and a second instance
Config and scale both live on the App Configs tab, and neither needs a rebuild. CapRover passes the variables to the containers and restarts them, so the image you built a minute ago stays as it is. Set APP_ENV and run two copies.

- 1App Configs
- 2Add Key/Value Pair
- 3Instance Count
- 4Save & Restart
- Open App Configs on the app page.
- Click Add Key/Value Pair, then enter
APP_ENVandproduction. - Change Instance Count to
2. - Click Save & Restart.
Send a few requests in a row. Swarm load-balances them across both containers, and each one should report production.
for i in 1 2 3 4; do curl -s http://hello-node.192-168-1-126.sslip.io/; done
sudo docker service ls --format "{{.Name}} {{.Replicas}} {{.Image}}"
Two container IDs alternate, and the service list shows hello-node 2/2 next to the CapRover services, now with captain-certbot among them:

Each extra copy of this app cost about 12 MB of RAM. Replicas on one server share its CPU and memory, so scaling here buys you rolling restarts and a spare process, not more hardware. Only scale apps that keep no state inside the container, because two replicas writing to the same volume is how data gets corrupted. The new Overview page sums all of this up in four cards, with app health shown as running over desired instances.

The “Initial setup is incomplete” banner stays on a LAN box because HTTPS never gets enabled. On a public server it clears once you finish Enable HTTPS and Force HTTPS.
Redeploy after a code change
This step catches most people once. Change the greeting in server.js from “Hello from CapRover” to “Hi again from CapRover”, run caprover deploy without committing, and the CLI happily ships the previous commit. It even says so, but the line is easy to miss in the output.
Using last commit on "main": ffc0d16171ec5945d80590d35faaba5c147f6e90
Deployed successfully hello-node
The app kept saying “Hello from CapRover” after that deploy, and nothing in the dashboard flags it. Commit first, then deploy.
git commit -am "change greeting"
caprover deploy -n qs-01 -a hello-node -b main
With the base image cached, this deploy took 2.5 seconds, and the new greeting came back with APP_ENV still set to production, because settings live on the app, not in the image.
Hi again from CapRover on 979afea8ab1e (production)
Every deploy builds a new numbered image tag, and the lab app ended on img-captain-hello-node:3 after three deploys. Those tags back the version history on the app’s Deployment tab, so keep an eye on disk use if you deploy many times a day. If you’d rather compare this workflow with the alternatives, the Dokku install guide uses git push instead of a CLI upload, and Coolify on Ubuntu 26.04 builds from a connected Git repository.
Keep this CapRover CLI reference card
These are the commands this guide ran, plus the Swarm reads that help when an app won’t come up. In Swarm, each app’s service carries the plain app name you typed in the dashboard:
| Task | Command |
|---|---|
| Log the CLI in | caprover login -u http://captain.<root> -p '<password>' -n qs-01 |
| List saved machines | caprover list |
| Deploy the last commit | caprover deploy -n qs-01 -a hello-node -b main |
| CapRover and app services | sudo docker service ls |
| App logs | sudo docker service logs --tail 20 hello-node |
| Why a replica won’t start | sudo docker service ps hello-node |
| Installer log | sudo docker logs $(sudo docker ps -aq --filter ancestor=caprover/caprover | tail -1) |
Keep the Docker cheat sheet handy for the plain Docker side of the box. On Ubuntu instead of Debian, the CapRover guide for Ubuntu 26.04 and 24.04 covers one-click apps and backups, and the official CapRover advanced installation page documents the MAIN_NODE_IP_ADDRESS option used above for private networks.