Deploy to production
This example uses one Linux VPS with systemd and a Debian/Ubuntu-style Nginx layout. Replace cms.example.com with your domain. Install required software, Nginx, and a certificate client using your distribution's supported packages first.
By default, the backend serves the built frontend in production (server.serveFrontend: true). Nginx terminates HTTPS and proxies the whole application to 127.0.0.1:3000. Alternatively, serve the frontend with Nginx and disable backend frontend serving.
1. Prepare the host
Run administrative commands with an operator account that has sudo access. Create a service identity and persistent directories:
sh
sudo useradd --system --user-group --home-dir /var/lib/tinycms --shell /usr/sbin/nologin tinycms
sudo install -d -o root -g tinycms -m 0750 /etc/tinycms
sudo install -d -o tinycms -g tinycms -m 0700 /var/lib/tinycms
sudo install -d -o tinycms -g tinycms -m 0700 /var/cache/tinycms
sudo install -d -o tinycms -g tinycms -m 0700 /var/backups/tinycms
sudo install -d -o root -g root -m 0755 /srv/tinycmsExtract a complete downloaded release at /srv/tinycms/app, owned by your deployment account and readable/traversable by tinycms. Keep the release and dependencies outside public Nginx document roots. Install dependencies as the deployment account rather than the runtime service user.
| Path | Purpose |
|---|---|
/srv/tinycms/app | Replaceable release, installed dependencies, and prebuilt application |
/etc/tinycms | Protected environment and complete configuration |
/var/lib/tinycms | Persistent database, originals, profile images, and content-rebuild exports |
/var/cache/tinycms | Regenerable transformations |
/var/backups/tinycms | Private completed backup bundles; copy these independently |
2. Configure production
Save the complete production JSON to /etc/tinycms/config.json. Create /etc/tinycms/backend.env using the backend environment template and set:
dotenv
NODE_ENV=production
PORT=3000
DB_FILE_NAME=file:/var/lib/tinycms/database.db
APP_CONFIG_FILE=/etc/tinycms/config.json
BETTER_AUTH_URL=https://cms.example.com
FRONTEND_ORIGIN=https://cms.example.com
BETTER_AUTH_SECRET=REPLACE_WITH_RANDOM_SECRET
SETUP_TOKEN=REPLACE_WITH_DIFFERENT_RANDOM_SECRETAdd real email settings and, when using webhooks, the encryption key. Do not leave placeholder secrets in a running deployment.
sh
sudo chown root:tinycms /etc/tinycms/backend.env /etc/tinycms/config.json
sudo chmod 0640 /etc/tinycms/backend.env /etc/tinycms/config.jsonUse simple KEY=value lines compatible with systemd EnvironmentFile and Node env files; avoid shell substitutions and export statements. Maintain a separate secure recovery copy.
3. Install dependencies and migrate
As the deployment account, from /srv/tinycms/app:
sh
pnpm install --frozen-lockfileThe downloaded release is already built. Keep its complete installed workspace, including the built shared package. Backend dist/ alone is insufficient: runtime package dependencies, workspace links, dist/resources/, and dist/backup/build.json must remain available. Preserve the exact matching release for recovery. When moving artifacts to another host, install native dependencies for that host rather than copying incompatible binaries.
Apply the release's shipped, reviewed migration set before first startup, following database initialization. If required migration files are missing, obtain a complete matching release from your provider before continuing.
For this layout, the actual migration script can be invoked as the service user after making the extracted release readable:
sh
sudo -u tinycms env DB_FILE_NAME=file:/var/lib/tinycms/database.db pnpm --config.verifyDepsBeforeRun=false --filter backend run db.migrateEnsure pnpm is available to that command; use its absolute installed path if needed. The command uses already installed dependencies without automatic dependency verification/installation. The service user must be able to write the database directory. On upgrades, stop the existing service and take a validated backup before migration.
4. Install the systemd service
Save this as /etc/systemd/system/tinycms.service. Use the real absolute Node path from command -v node if it differs from /usr/bin/node.
ini
[Unit]
Description=TinyCMS
After=network.target
[Service]
Type=simple
User=tinycms
Group=tinycms
WorkingDirectory=/srv/tinycms/app/apps/backend
EnvironmentFile=/etc/tinycms/backend.env
ExecStart=/usr/bin/node /srv/tinycms/app/apps/backend/dist/index.js
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
UMask=0077
NoNewPrivileges=true
[Install]
WantedBy=multi-user.targetSystemd supplies the environment and sends a graceful termination signal on stop. It does not rely on an interactive shell or a version manager's shell initialization. See the systemd service reference.
sh
sudo systemctl daemon-reload
sudo systemctl enable --now tinycms
sudo systemctl status tinycms
curl --fail http://127.0.0.1:3000/healthHealth should return {"status":"ok"}. Inspect journalctl -u tinycms if startup fails. Do not continue until the backend is healthy.
5. Configure Nginx and HTTPS
Point the domain's DNS at this server. Allow inbound ports 80 and 443 using the host/provider firewall, preserving SSH access; keep port 3000 private.
Create /etc/nginx/sites-available/tinycms:
nginx
server {
listen 80;
server_name cms.example.com;
client_max_body_size 101m;
location ^~ /api/v1/server/ {
return 403;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 60s;
}
}The request limit matches the example backend's 101 MiB request ceiling; adjust both together. Keep server-control routes unavailable through the public proxy. Do not add proxy caching for authenticated CMS or API responses. See Nginx proxy directives.
Enable and validate the site:
sh
sudo ln -s /etc/nginx/sites-available/tinycms /etc/nginx/sites-enabled/tinycms
sudo nginx -t
sudo systemctl reload nginxInstall Certbot using its official instructions for your host, then obtain and configure the certificate:
sh
sudo certbot --nginx -d cms.example.com
sudo certbot renew --dry-run
sudo nginx -tSelect HTTPS redirection when offered and verify HTTP redirects to HTTPS. Do not enter credentials or bootstrap an account until HTTPS works. If public DNS or firewall checks fail, fix them before retrying certificate issuance.
Serve the frontend with Nginx
To have Nginx serve frontend files directly, set server.serveFrontend to false in /etc/tinycms/config.json and restart the backend. Keep paths.frontendDist pointing to the frontend build directory. Replace the server block above with this alternative before enabling the site and configuring HTTPS:
Download nginx.conf. Save it as /etc/nginx/sites-available/tinycms on Debian/Ubuntu, or include it inside the http context of your Nginx configuration.
nginx
# Set server.serveFrontend to false in the backend configuration.
# Enable HTTPS before using the CMS; see the deployment guide.
server {
listen 80;
server_name cms.example.com;
root /srv/tinycms/app/apps/frontend/dist;
index index.html;
client_max_body_size 101m;
location ^~ /api/v1/server/ {
return 403;
}
location ~ ^/(api|resources|profile-images)(/|$)|^/health$ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 60s;
}
location / {
try_files $uri $uri/ /index.html;
}
}The Nginx worker needs read access to the build directory and traversal access to its parents. Point its document root only at apps/frontend/dist. Keep media and profile-image requests proxied to the backend, which owns their delivery behavior. The SPA fallback applies only to frontend paths; missing API or resource requests reach the backend.
Both hosting options use the same public origin and the same BETTER_AUTH_URL and FRONTEND_ORIGIN. Use the included frontend files and test direct navigation to /login and a nested CMS route. With backend frontend serving disabled, direct requests to http://127.0.0.1:3000/login should return 404 while the public frontend route works.
Serve the frontend with Apache HTTP Server (httpd)
Use this instead of Nginx when Apache is your public web server. Set server.serveFrontend to false and use the same frontend build path and public origin described above. Download httpd.conf. It is a virtual-host include, not a replacement for the global httpd.conf; for example, save it as /etc/apache2/sites-available/tinycms.conf on Debian/Ubuntu.
Enable mod_proxy, mod_proxy_http, mod_headers, and mod_rewrite, together with the usual mod_authz_core, mod_dir, and mod_mime modules. On Debian/Ubuntu, the additional modules can be enabled with sudo a2enmod proxy proxy_http headers rewrite and the site with sudo a2ensite tinycms. Other distributions use their own module-loading and include layout.
apache
# Include this virtual host in Apache HTTP Server 2.4 configuration.
# Set server.serveFrontend to false in the backend configuration.
# Enable HTTPS before using the CMS; see the deployment guide.
<VirtualHost *:80>
ServerName cms.example.com
DocumentRoot "/srv/tinycms/app/apps/frontend/dist"
DirectoryIndex index.html
LimitRequestBody 105906176
ProxyRequests Off
ProxyPreserveHost On
ProxyTimeout 60
RequestHeader set X-Forwarded-Proto "expr=%{REQUEST_SCHEME}"
<LocationMatch "^/api/v1/server(/|$)">
Require all denied
</LocationMatch>
ProxyPassMatch "^/((api|resources|profile-images)(/.*)?|health)$" "http://127.0.0.1:3000/$1"
ProxyPassReverse "/" "http://127.0.0.1:3000/"
<Directory "/srv/tinycms/app/apps/frontend/dist">
Options -Indexes -MultiViews
AllowOverride None
Require all granted
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/(api|resources|profile-images)(/|$)
RewriteCond %{REQUEST_URI} !^/health$
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /index.html [END]
</Directory>
</VirtualHost>The example listens on HTTP for initial setup, like the Nginx example. Configure HTTPS with your certificate client before signing in; use its Apache integration instead of the earlier Nginx commands. The forwarded protocol is derived from the request scheme so it also works when copied into the HTTPS virtual host. The Apache worker must be able to read the build and traverse its parent directories.
Validate with sudo apachectl configtest, then reload the appropriate service (apache2 on Debian/Ubuntu or httpd on many other distributions). See the official reverse proxy and rewrite references.
Serve the frontend with Caddy
Use this instead of Nginx or Apache. Set server.serveFrontend to false, retain the same frontend path and public URL settings, and download Caddyfile. Save it as /etc/caddy/Caddyfile after replacing the domain and frontend path.
text
# Set server.serveFrontend to false in the backend configuration.
# Replace the domain and build path before loading this configuration.
cms.example.com {
root * /srv/tinycms/app/apps/frontend/dist
request_body {
max_size 105906176
}
route {
@server path /api/v1/server /api/v1/server/*
handle @server {
respond "Forbidden" 403
}
@backend path /api /api/* /resources /resources/* /profile-images /profile-images/* /health
handle @backend {
reverse_proxy 127.0.0.1:3000
}
handle {
try_files {path} /index.html
file_server
}
}
}Caddy manages HTTPS for this domain when public DNS, ports 80/443, and certificate issuance are available. Its worker needs read/traversal access to the frontend build. The ordered route keeps restricted server routes ahead of general backend routes; handle preserves backend request paths, and only the final frontend handler uses the SPA fallback.
Validate with caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile, then reload with sudo systemctl reload caddy when using its packaged systemd service. See Caddy's official SPA and proxy patterns and handle reference.
For all three examples, the request-body limit is 101 MiB, matching the backend example. Keep /api, /resources, /profile-images, and /health on the backend, and verify that missing API/resource paths return backend errors instead of frontend HTML. Keep the server-control endpoint blocked at the public web server.
6. Verify and initialize
- Check
https://cms.example.com/healthand open/loginover HTTPS. - Complete initial setup at
/setup; verify email and sign in. - Remove
SETUP_TOKENfrom the environment file and restart the service. - Test a direct browser refresh on a nested CMS route.
- Upload an image, save and publish test content, and inspect published delivery.
- Verify a restricted user cannot edit content they are denied.
- Check scheduled jobs and webhook delivery if enabled.
- Create a validated backup and perform an isolated recovery drill.
Record the release identity, Node/pnpm versions, data paths, configuration locations, and certificate renewal method in your deployment record.