Skip to content

Deploy on a VPS ​

What you'll build

Your site with Easy CMS on one Linux server you control (Ubuntu or Debian): Node.js, SQLite and uploads on the server's disk, systemd to keep it running, Caddy for HTTPS, and daily backups.

Before this page: Choosing a host, Migrations & deployment.

One server is the simplest and cheapest way to run Easy CMS. The server keeps running, so it runs scheduled jobs every minute by itself, and SQLite needs nothing but a file.

1. Node.js and a user ​

Install Node.js 22.12 or newer (24 LTS), for example from nodejs.org or your distribution's NodeSource packages, then create a user that runs the site and owns its files:

bash
node -v   # v22.12 or newer
sudo useradd --system --create-home --shell /usr/sbin/nologin cms
sudo mkdir -p /srv/site && sudo chown cms:cms /srv/site

2. The site ​

As the cms user, put the code in /srv/site (git clone, or copy it), add the secrets, install and build. Build on the server itself: SQLite's native driver must match its system.

bash
sudo -u cms bash
cd /srv/site
git clone https://github.com/you/your-site.git .
printf 'EASY_CMS_SECRET=%s\nEASY_CMS_SETUP_CODE=%s\n' "$(openssl rand -hex 32)" "choose-a-phrase" > .env
chmod 600 .env
npm ci
npm run build   # Next.js or Nuxt; nothing to build for the standalone server
exit

Without DATABASE_URL, sqlite({ url: process.env.DATABASE_URL ?? 'file:./cms.db' }) keeps the database in /srv/site/cms.db, and uploads go to /srv/site/uploads. Postgres works too: install it on the server or use a hosted one, and set DATABASE_URL in .env.

3. A systemd service ​

systemd starts the site at boot, applies migrations before each start and restarts it if it stops.

ini
[Unit]
Description=Easy CMS site
After=network.target

[Service]
Type=simple
User=cms
WorkingDirectory=/srv/site
Environment=NODE_ENV=production
Environment=PORT=3000
EnvironmentFile=/srv/site/.env
ExecStartPre=/usr/bin/npx easy-cms migrate
ExecStart=/usr/bin/npx next start
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
ini
[Unit]
Description=Easy CMS site
After=network.target

[Service]
Type=simple
User=cms
WorkingDirectory=/srv/site
Environment=NODE_ENV=production
Environment=PORT=3000
EnvironmentFile=/srv/site/.env
ExecStartPre=/usr/bin/npx easy-cms migrate
ExecStart=/usr/bin/node .output/server/index.mjs
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
ini
[Unit]
Description=Easy CMS site
After=network.target

[Service]
Type=simple
User=cms
WorkingDirectory=/srv/site
Environment=NODE_ENV=production
Environment=PORT=3000
EnvironmentFile=/srv/site/.env
ExecStartPre=/usr/bin/npx easy-cms migrate
ExecStart=/usr/bin/npx easy-cms serve --trust-proxy
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

EnvironmentFile gives the server .env (Nuxt doesn't read it by itself), and WorkingDirectory makes file:./cms.db, the migrations and uploads resolve in the project. Check where npx and node are with which npx and adjust the paths. Then:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now easy-cms
journalctl -u easy-cms -f   # the applied migrations, then the server

HTTPS with Caddy ​

Caddy gets and renews the HTTPS certificate by itself. Point the domain's DNS at the server, open ports 80 and 443, install Caddy and use:

txt
cms.example.com

reverse_proxy localhost:3000
bash
sudo systemctl reload caddy

Caddy redirects HTTP to HTTPS by itself. It sends X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host, so trust it: trustProxy: true in the Next.js route handlers or the Nuxt module (--trust-proxy for the standalone server, already in the unit above). Set serverURL: 'https://cms.example.com' in the config. With nginx, proxy to the same port and set those headers yourself, and redirect HTTP to HTTPS with a server block on port 80:

nginx
server {
  listen 80;
  server_name cms.example.com;
  return 301 https://$host$request_uri;
}

Backups ​

Let Easy CMS back up the database every day (see Backups):

ts
backups: { frequency: 'daily', at: '03:00', keep: 7 },

Backups go to backups/ on the same disk, so copy them and uploads/ somewhere else too: Automate backups uses a cron job with S3 or rclone.

Updating ​

bash
cd /srv/site
sudo -u cms git pull
sudo -u cms npm ci
sudo -u cms npm run build
sudo systemctl restart easy-cms   # migrates, then starts

The site is down for the few seconds of the restart.

Next steps ​

  • Security: the checklist for a server on the internet.
  • Docker: the same site in a container.

Released under the MIT License.