Skip to content

Put it on a server

On your computer you learned Manablox with the local preset. To put your CMS on the internet you need a server that is always on, a domain name, and HTTPS. This page walks you through it from an empty server to your first sign-in.

You use the docker preset of manablox create for this. It writes a project in which everything runs in Docker containers: the database, the cache, the CMS, the public API for your website and a web server in front of them that gets HTTPS certificates on its own. You do not install Node.js, Postgres or anything else on the server except Docker.

A container is a small, isolated program with everything it needs packed inside. Docker Compose starts several of them together from one file, compose.yml. Your project starts these services:

ServiceWhat it does
caddyThe web server in front. It gets and renews HTTPS certificates and sends each request to the right service. It is the only service reachable from the internet.
apiThe admin and the management API, in one process.
publicThe public API your website reads content from. It can only read, and serves one space.
migrateBrings the database tables up to date, then stops. It runs before api and public start.
postgresThe database. Everything you create in the admin lives here. Not there with SQLite, see below.
valkeyA fast cache and the queue for background jobs.
flowchart LR
db[("postgres (or the SQLite file)")]
browser[Browsers and your website] --> caddy
caddy -->|cms.example.com| api
caddy -->|content.example.com| public
migrate --> db
api --> db
api --> valkey
public -->|read only| db
public --> valkey

Uploaded files are kept in a Docker volume called uploads. A volume is a folder Docker manages for you; it survives when containers are rebuilt or restarted.

If you chose SQLite as the database (--database sqlite), there is no postgres service. The database is the file manablox.db in a volume called database, which migrate, api and public all open. SQLite allows one writer, so never run more than one api. See The database.

A small virtual private server (VPS) from any hosting company is enough. Choose a current Linux, for example Ubuntu LTS or Debian.

How much memory? The compose file gives every service an upper memory limit:

ServiceMemory limit
postgres1 GB
api1 GB
public768 MB
valkey384 MB

Together that is a little over 3 GB (about 2 GB with SQLite, which has no postgres service). A server with 4 GB of RAM gives every service its full room. The limits are ceilings, not reservations, so a small site also runs on 2 GB, but a busy one may get slow. Plan at least 20 GB of disk, plus room for your uploads and backups.

You need to reach the server with SSH, the tool for opening a terminal on another machine. Your hosting company shows you the address (for example 203.0.113.10) and how to log in:

Terminal window
ssh root@203.0.113.10

Follow the official guide for your Linux at docs.docker.com/engine/install. It installs Docker Engine together with the Compose plugin. When it is done, check both:

Terminal window
docker --version
docker compose version

Each command prints a version number. If docker compose says it is not a docker command, the Compose plugin is missing; install it as the Docker guide describes.

If you log in as a normal user instead of root, allow that user to run Docker, then log out and in again:

Terminal window
sudo usermod -aG docker $USER

Your CMS uses two addresses: one for the admin (for example cms.example.com) and one for the public API (for example content.example.com). At the company where you registered your domain, open the DNS settings and add an A record for each name with your server’s IP address. If your server also has an IPv6 address, add an AAAA record too.

DNS changes can take a few minutes up to a few hours. You can check from your computer:

Terminal window
ping cms.example.com

The answer should show your server’s IP address. Caddy can only get a certificate once both names point at the server, so do this before you start the stack.

The easiest way is to create the project on your own computer, where you already have Node.js and pnpm (see What you need), and copy it to the server.

  1. On your computer, run the create command with the docker preset:
Terminal window
pnpm dlx @manablox/cli create my-cms --preset docker
  1. When it asks how the instance will run, choose Docker behind Caddy. With --preset docker it is already highlighted, so press Enter.
  2. Enter your two domains (cms.example.com and content.example.com) and an email address for certificate notices. Let’s Encrypt, the free certificate authority Caddy uses, writes to it before a certificate would expire.
  3. Answer the questions about uploads and mail. You can change both later in .env.
  4. Answer yes to installing dependencies, and no to building and starting the stack (you start it on the server, not on your computer).
  5. Copy the folder to the server, for example into /srv/my-cms. rsync copies everything except the installed packages, which the server does not need:
Terminal window
rsync -av --exclude node_modules my-cms/ root@203.0.113.10:/srv/my-cms/
  1. Log in to the server and go into the folder:
Terminal window
ssh root@203.0.113.10
cd /srv/my-cms

You can also keep the project in a private Git repository and pull it on the server. .env is deliberately left out of Git, so copy that one file to the server yourself (for example with scp).

If you prefer to create the project directly on the server, install Node.js and pnpm there and run the same command inside /srv.

The command options are all listed in The manablox command.

The file pnpm-lock.yaml records the exact version of every package. The image build refuses to run without it, so the server always installs exactly what you tested.

If manablox create installed the dependencies, the file is already there. Check:

Terminal window
ls pnpm-lock.yaml

If it says “No such file”, write it with the script that came with your project. It runs pnpm inside a temporary Docker container, so it works on the server without Node.js:

Terminal window
./scripts/lockfile.sh

Run it again whenever you change package.json.

.env holds everything that differs between installations: domains, passwords and settings. Open it on the server, for example with nano .env. The secrets (POSTGRES_PASSWORD, POSTGRES_PUBLIC_PASSWORD, AUTH_SECRET; with SQLite only AUTH_SECRET) were generated for you; leave them as they are. Check these lines:

VariableWhat to check
ADMIN_DOMAIN, PUBLIC_DOMAINYour two domains, without https://
PUBLIC_URL, PUBLIC_API_URLThe same two domains with https:// in front. They must match the domains above.
ACME_EMAILAn address you read
STORAGE_DRIVERlocal keeps uploads on the server; s3 needs the S3_* lines filled in. See Uploads and images.
MAIL_DRIVERnone by default, so the CMS sends no email. See Sending email.

Every variable is explained in The .env file.

First build the image. It downloads the Manablox packages and packs them with your project files. The first build takes a few minutes:

Terminal window
docker compose build

Then start everything in the background (-d means “detached”, so you get your terminal back):

Terminal window
docker compose up -d

Compose starts postgres (not with SQLite) and valkey, runs migrate once, then starts api, public and caddy. Check the state of the services:

Terminal window
docker compose ps

After about half a minute api should show Up and (healthy). migrate is not in the list because it finished its work. public may show Restarting for now; that is expected until you create a space (see below).

To watch what the CMS is doing, follow its logs. Press Ctrl+C to stop watching; the services keep running:

Terminal window
docker compose logs -f api public

If api does not become healthy, look at docker compose logs migrate and docker compose logs api. If the site does not load in the browser, docker compose logs caddy usually says why, for example that a domain does not point at the server yet. More help is in Common problems.

Open https://cms.example.com in your browser. You should see the admin’s sign-up form, and the lock icon in the address bar shows that HTTPS works.

The first account you create becomes the superadmin, the account that may do everything. Sign-up closes after it.

Then create a space, as you did on your computer. Sign in and create a space shows each step.

The public API serves exactly one space. Until a space exists it stops at startup and Docker keeps restarting it. As soon as there is exactly one space, it picks that one by itself on its next restart. Check it:

Terminal window
curl https://content.example.com/readyz

An answer that starts with {"status":"ready" means the public API is running.

If you have several spaces, tell it which one to serve. Find the space’s technical name in the admin (see Spaces and members), put it into .env:

Terminal window
MANABLOX_SPACE=blog

Then recreate the public service so it reads the new value:

Terminal window
docker compose up -d public

Your website reads content from https://content.example.com. When you create it with manablox frontend, pass --url https://content.example.com and --editor-origin https://cms.example.com; see The starter website and The public API. The website itself is a separate project and is not part of this compose file.

Before real content goes in, set up Backups and go through the Security checklist.