Deploy › VPS (Linux server)
Docs/Deploy/VPS (Linux server)

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:

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.

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.

Next.js: /etc/systemd/system/easy-cms.service
[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
Nuxt: /etc/systemd/system/easy-cms.service
[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
Standalone: /etc/systemd/system/easy-cms.service
[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:

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:

/etc/caddy/Caddyfile
cms.example.com
 
reverse_proxy localhost:3000
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:

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):

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

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.