WordPress, Ghost and NodeJs on your own PAAS with CapRover ($5/month)

December 14, 2021 · 15 min read


CapRover is an open-source DIY Platform as a service (PaaS) that help eliminate the cost and complexity of developing applications. Some of the well-known cloud applications would be Heroku or AWS Elastic Beanstalk, allowing developers to build, run, and operate applications entirely in the cloud with pre-built software components that help create and scale applications quickly. The problem with these services is that a higher price tag comes with convenient usability. I will show you how to host your PaaS on a VPS from $5 a month. Also, I will show you how to deploy WordPress, Ghost and NodeJs with a Postgres database.

Why CapRover

As I mentioned in the title, CapRover is based on Docker. Therefore you can deploy any app that can be containerized using Docker. It is straightforward to deploy web applications or databases in different programming languages, like NodeJS, Python, PHP, ASP.NET, Ruby, MySQL, MongoDB, Postgres, WordPress, etc.

These are some of the best CapRover’s features:

  1. Templates for one-click deployment from the UI. Full list of apps.
  2. CLI for automation and scripting.
  3. Web GUI for accessibility and convenience.
  4. No lock-in. You can remove CapRover, and your apps keep working.
  5. Docker Swarm under the hood for containerization and clustering.
  6. Nginx (fully customizable template) under the hood for load balancing.
  7. Let’s Encrypt under the hood for free SSL (HTTPS).

Install CapRover

We need to set a few pre-requisites before installing CapRover:

  1. A domain name, you can use a subdomain of one of your owned domain names.
  2. Node.js and npm installed on your local machine.
  3. VPS server with a Public IP Address, minimum 1Gb of RAM and Ubuntu 18.04 installed.

Fast Installation using DigitalOcean Droplet

If your cloud hosting is Digital Ocean, they provide an Ubuntu image with Caprover installed on it. You can skip to the configuration step now, otherwise, keep reading.

Install Caprover on a new Ubuntu Server

If you prefer to use any other VPS Cloud provider, you can install Caprover on an empty Ubuntu server, SSH into the server, and execute the following commands:

$ sudo apt-get update # update packages

# Install Docker CE Edition
$ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \

# Add Docker’s official GPG key:
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg 
| sudo apt-key add -

Update the apt package index again, and install the latest version of Docker Engine and Containerd for your Ubuntu 18.04:

$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io

Finally, we can install CapRover with this simple command:

docker run -p 80:80 -p 443:443 -p 3000:3000 -v /var/run/docker.sock:/var/run/docker.sock -v /captain:/captain caprover/caprover

Enabling Firewall and Opening Ports

For CapRover to work we need to open several ports. Enable the firewall in Ubuntu by running:

$ sudo ufw enable

# Open Ports
$ sudo ufw allow 80,443,3000,996,7946,4789,2377/tcp
$ sudo ufw allow 7946,4789,2377/udp

Configure CapRover

DNS Configuration

We have a VPS with Docker installed and a public IP to access it. It’s an excellent time to update the DNS setting for our domain name. Add the following A entry in your DNS management provider:

Type        Host          Value
A Record    *.caprover    YOUR_SERVER_IP

This configuration might take some time to propagate. To confirm once it’s ready, go to https://mxtoolbox.com/DNSLookup.aspx and enter randomthing123.caprover.yourdomain.com and check if the IP address resolves to the IP you set in your DNS. Note that randomthing123 is needed because you added a wildcard entry in your DNS by setting *.caprover as your host, not something.

Install CapRover CLI

After configuring the DNS, assuming that you have npm installed, run the following command on your local computer:

npm install -g caprover
caprover serversetup

Follow the steps and log in to your CapRover instance.

➜  ~ caprover serversetup

Setup CapRover machine on your server...

? have you already started CapRover container on your server? Yes
? IP address of your server: YOUR_SERVER_IP
? CapRover server root domain: caprover.yourdomain.com
? new CapRover password (min 8 characters): [hidden]
? enter new CapRover password again: [hidden]
? "valid" email address to get certificate and enable HTTPS: your@email.com
? CapRover machine name, with whom the login credentials are stored locally: caprover-vps-01

CapRover server setup completed: it is available as caprover-vps-01 at https://captain.caprover.yourdomain.com

For more details and docs see CapRover.com

Now you can access your CapRover from https://captain.caprover.yourdomain.com


You can view the apps deployed from the dashboard, and you can deploy new apps with one click and some other features that I’ll keep explaining below.

Deploy Wordpress

CapRover has built-in support for several popular apps that you can deploy using a straightforward form wizard. These include WordPress, MySQL, MongoDB and many more.

There is a repository of One Click Apps on GitHub, and it’s continuously growing.

I will show you how to set up a WordPress blog. From the left menu, select Apps, then click on One-click apps, and search for “WordPress” as in the image below.


We have two results; one says “No database”, this only means that the wizard will create the WordPress container, but we need to provide the database connection details; the wizard will not create the database for us. I will select the “WordPress” template with the database in this demo.

We need to fill in the form with the details for our blog; I have updated the WordPress version to use the latest stable 5.8.


Scroll down and click on “Deploy”.


Please wait for a few minutes until our containers finish creating.


As shown in the warning message, we see the screen below after creating the containers, but the containers might take a bit to boot up.


If we check on the “Apps” section, we can see that we have two containers created for WordPress, one for the web application itself and another for the database. We can also see that they both have “Persistent Data”, the web application need it for file uploads and the database for all the data. If we didn’t have persistent data, we’d lose all the data and configuration for our blog on each restart.


If we click on the web app container, we can see Http settings, where we can enable HTTPS and force HTTPS redirection.


If we select the app config tab, we can see the environment variables that we have defined in the container and the path for the persistent volumes.


If we go to the deployment tab, we can see the deployment history, access the logs for the container, and configure CI if we are interested from here.


From the http settings tab, we can see the URL where our blog is deployed. If we open it on a new browser tab, we will get the newly installed WordPress setup form, wizard.


We can see it published after setting up our blog, and we’re ready to customize it.


Deploy Ghost

Using the same approach above for WordPress, from the left menu, select Apps, then click on One-click apps, and search for “Ghost”, and click on it, as in the image below.


In the form below, I changed the Ghost version to 4, as it’s the latest. I filled in the other details and clicked on Deploy.


You will see a screen while the containers are being created.


I got an error while the containers where being created, I wanted to check the logs to be sure about the issue. I selected “Apps” from the left menu, then I clicked on ghost-db and checked the logs output:

------------------------- Mon Dec 13 2021 07:14:02 GMT+0000 (Coordinated Universal Time)
Build started for ghost-db
An explicit image name was provided (bitnami/mariadb:10.1). Therefore, no build process is needed.
Pulling this image: bitnami/mariadb:10.1 This process might take a few minutes.
Build has failed!
Deploy failed!

failed to register layer: Error processing tar file(exit status 2): fatal error: runtime: out of memory

As I am running a container with only 1Gb of RAM, sometimes deployment might need more memory.

Adding 1Gb Swap Memory

I’m going to add 1Gb of Swap Memory on my VPS server. To do that, connect via SSH and run the following command:

sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

The previous command, created a swap file, then give write permission only to the root user and enable it. Now to make the change permanent we need to edit /etc/fstab and add the following line:

/swapfile swap swap defaults 0 0

To verify that the swap is active, use either the swapon or the free command as shown below:

$ sudo swapon --show

/swapfile file 1024M 507.4M   -1

Installing Ghost Take Two

Now I repeated, the previous process, and it succeeded.


When I tried to navigate to the URL of my newly set up blog, I got a 502 error page.


That isn’t good. Most of the one-click apps offered in CapRover are a slight variation of docker-compose files. The template for Ghost is for version 2, and I want to install the latest version, Ghost 4. For this reason, I found a Github Gist for Ghost 4, also failing, so I forked and fixed it: Github Gist, but it’s only deploying the docker container for Ghost, not for the database. I need first to create a Database container; then, I can create the Ghost container and set the connection details for the database. Let’s start:

Create the Maria DB Container through the One-click app.


Select it, fill in the form and click on Deploy.


You will see the following screen once your MariaDB container has been deployed.


Login via SSH to the server to create the database for Ghost:

$ docker ps

CONTAINER ID   IMAGE                              COMMAND                  CREATED         STATUS     PORTS                                                                      NAMES
7706a370054c   img-captain-mariadb-db:1           "docker-entrypoint.s…"   6 minutes ago   Up 5 minutes   3306/tcp                                                                   srv-captain--mariadb-db.1.lqdhp0eiufn7t1nmoyiof0p8k
53fafaca463c   wordpress:5.8                      "docker-entrypoint.s…"   2 hours ago     Up 2 hours     80/tcp                                                                     srv-captain--my-wp-blog-wordpress.1.xjondjn2ayml2ct7fqqjr9n0x
99f81b1a56a6   mysql:5.7                          "docker-entrypoint.s…"   2 hours ago     Up 2 hours     3306/tcp, 33060/tcp                                                        srv-captain--my-wp-blog-db.1.n4zmp5g0dihv27in3mh80ttgj
4a4bbc02a0e5   caprover/certbot-sleeping:v1.6.0   "/bin/sh -c 'sleep 9…"   2 days ago      Up 2 days      80/tcp, 443/tcp                                                            captain-certbot.1.96zaiqpgi009o3a7l77sq3p22
e62667e60b7f   nginx:1                            "/docker-entrypoint.…"   2 days ago      Up 2 days>80/tcp, :::80->80/tcp,>443/tcp, :::443->443/tcp   captain-nginx.1.lnl09cnopat4hvc6igu5n95dj
a0fd1c959d7e   caprover/caprover:1.10.1           "docker-entrypoint.s…"   2 days ago      Up 2 days>3000/tcp, :::3000->3000/tcp                                  captain-captain.1.y2h98lsskc2i0oh4a30n0b9mf

$ docker exec -it 7706a370054c /bin/bash
$ mysql -u root -p
Enter password:

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 3
Server version: 10.7.1-MariaDB-1:10.7.1+maria~focal mariadb.org binary distribution

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> create database ghostdb;
Query OK, 1 row affected (0.000 sec)

MariaDB [(none)]> show databases;
| Database           |
| ghostdb            |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
5 rows in set (0.001 sec)

MariaDB [(none)]> exit
root@7706a370054c:$ exit

In the commands above, I first get a list of the running docker containers executing docker ps. Then I can get the container ID for MariaDB and execute docker exec -it 7706a370054c /bin/bash I am opening a terminal inside the Database container. Then I just log in to the CLI for MariaDB running mysql -u root -p, and I create the database for Ghost. If you prefer to do this process using a UI, you can install Adminer to manage your database with a UI.

Installing Ghost Take Three

For this deployment, I will use the Github Gist) I mentioned earlier. Once again, go to Apps, click on One-click deploy, then search for “template”.


Click on “Template” and paste the contents of the Gist in there, then click Next.


The main data that we need to configure correctly here is the database host, that’s the container we previously created, and the password we chose for the database. Then click Deploy.


After the containers finish creating and booting up, we can navigate to our new Ghost Blog.


It took me three tries to get it right, but I thought it would be interesting to document also the attempts that didn’t work and why they didn’t work for me.

Deploy Sample Node.js app

In this third demo, I will deploy a simple NodeJS app that connects to a Postgres database. I am also setting up Continuous Integration using Github Actions to deploy the app when you push to your master branch.

The source code that I will deploy is hosted in Github and uses docker-compose and Express.js. Here’s the list of steps that we’ll do for this deployment:

  1. Set up a Postgres database with CapRover’s One-Click Apps.
  2. Create a new custom app inside the CapRover interface.
  3. Configure the codebase to deploy in CapRover.
  4. Connect our Github repo and deploy.

Postgres database

This process is very similar to when I created MariaDB earlier, so navigate to apps, click on one-click apps and search for Postgres.


Fill in the form and click Deploy.


When the database container creation finishes, you get an example of a connection string for a NodeJs app.


Create a New Custom App in CapRover

Next, we need to create another container app in Rover, and here we will deploy our NodeJs app.


Here we can see the details for the newly created app container.


Configure the codebase to deploy in CapRover

When deploying an app to CapRover, we need to create a captain-definition file that sits at the root of your project. This file tells CapRover what actually to do. I am going to specify CapRover to use the Dockerfile in the root of the project:

  "schemaVersion": 2,
  "dockerfilePath": "./Dockerfile"

According to the official CapRover documentation, “schemaVersion” is always 2.

Next, we need to update the connection string for the database in index.ts. Based on my Postgres container HTTP endpoints, I’m editing the contents of my connection string to be:

const client = new Client({
  user: "dbuser",
  password: "secure_password",
  host: "srv-captain--node-postgres-db",
  database: "node_app"

Connect our Github repo and deploy.

The last step is to deploy our NodeJs app, as we already have it in Github. Setting up Continuous Integration to automatically push new code to the master branch updates is an excellent way of setting things up.

I will create a new SSH Key to connect Github with my CapRover server. Run in your terminal:

$ ssh-keygen -f ~/caprover-node-postgres -m PEM -t rsa -b 4096

Now, I need to copy the public key to Github. Select Deploy keys, create one, and paste the public key on your Repository Settings.


You can copy the public key to the clipboard running in your terminal:

 # copy the public key to the clipboard
$ cat ~/caprover-node-postgres.pub | pbcopy

Next, we need to save the private key in the Deploy section of the app in CapRover, if we scroll down until we see Method 3: Github like in the image below, we copy and paste the private key:


You can copy the private key to the clipboard running in your terminal:

 # copy the private key to the clipboard
$ cat ~/caprover-node-postgres | pbcopy

After you click Save & Update, a new URL will be populated where it says Webhook, like in the image below. Copy it, as we need to add it to Github.


Navigate back to Github, and within your Repository Settings, select Webhooks, and add one. On the Payload URL field, paste the URL we just copied from CapRover.


Click on Add Webhook, and push your changes to the repository. If you have already done it, you can do a minor change and push it again to master to trigger a new deployment.

After you push your changes to Github, you should see in the deployment section of the NodeJs app that the deployment is in progress like in the image below:


Once the deployment finishes you can navigate to your subdomain/ping, and you’ll get a similar result to this:


Remember to add any relevant environment variables:


And one last cool feature is that you can add an SSL certificate and force a redirection to HTTPS with two clicks!


And this is all to get our third and last demo deployed.


In my previous posts, I’ve shown you how to deploy NodeJs to Dokku and Ruby on Rails on Dokku. The main difference between Dokku and CapRover is that Dokku offers buildpacks linke Heroku. The developer experience is a lot similar to Heroku. CapRover offers a UI that makes management more visual. One of the positive aspects of CapRover is that if you have a Dockerfile, you can deploy anything you like here. Also, as it runs on top of Docker Swarm, you can have one server, or multiple servers on a Cluster, while Dokku only supports one server. Given the price of a VPS, from $5 a month, I guess if I have a Ruby on Rails app to deploy, I’d probably choose Dokku, but if I want to have several Ghost blogs or WordPress websites, I would probably be more inclined towards CapRover. But now that I have explained how both work, you must decide which one you prefer for your deployments.

Pedro Alonso

Software developer and consultant. I help companies build great products. I've worked with all kinds of companies. Contact me by email.

Get my new content delivered straight to your inbox. No spam, ever.

© 2022 Pedro Alonso