Skip to main content
This guide covers deploying Primo to production. Choose the deployment method that matches your needs and technical expertise.
Persistent Storage Required: Primo stores all data in a SQLite database and needs persistent disk storage to function. Traditional serverless platforms (Vercel, Netlify, Cloudflare Pages) have ephemeral file systems and won’t work.

Quick Deploy: Railway

The fastest way to deploy Primo to production.

Prerequisites

Deployment Steps

1

Click deploy button

Deploy Primo on Railway
2

Configure environment (optional)

Set environment variables if you want automated setup:
  • PRIMO_SUPERUSER_EMAIL
  • PRIMO_SUPERUSER_PASSWORD
  • PRIMO_USER_EMAIL and PRIMO_USER_PASSWORD: create the regular Developer account used for Primo sign-in
  • PRIMO_APP_URL: set the public HTTPS URL used for backend links
  • PRIMO_DOMAIN_PROVIDER=railway and PRIMO_RAILWAY_TOKEN: connect custom site domains from the Primo dashboard (see below). Leave these unset when you only use the Railway-generated hostname
3

Wait for deployment

Railway will:
  • Pull the Docker image
  • Create a persistent volume
  • Deploy the container
  • Generate a URL
4

Access your instance

Click the generated URL to access Primo. If you did not create a superuser through environment variables, complete the setup screen. If you did, sign in to Primo with the regular account created by PRIMO_USER_EMAIL and PRIMO_USER_PASSWORD; the superuser account is for PocketBase at /_/.
5

Connect site domains from the dashboard

To connect domains to sites without leaving the Primo dashboard, give Primo access to your Railway service:
  1. In your Railway project, go to Settings → Tokens → New Project Token
  2. Add two variables to the service, then redeploy:
    • PRIMO_DOMAIN_PROVIDER = railway
    • PRIMO_RAILWAY_TOKEN = the token
  3. In the Primo dashboard, open a site’s menu and choose Assign domain. Primo attaches the domain to your Railway deployment, shows the DNS records to add at your registrar, and marks the site live once the certificate is issued.
Without these variables, connect the domain by hand:
  1. In your Railway service, go to Settings → Networking → Custom Domain and add the domain.
  2. Add the DNS records Railway shows at your registrar, and wait for Railway to issue the certificate.
  3. In the Primo dashboard, choose Assign domain on the site, enter the domain, and click Mark as connected.
Verify the volume mounts at /app/pb_data before creating content. See Railway volumes for the host controls. Cost: Check Railway plan and resource pricing. Compute, volume storage, and bandwidth contribute to usage.
Railway manages the infrastructure and certificates. You still configure domains, backups, and access for your Primo instance.

VPS Deployment

Deploy to any VPS for full control over your infrastructure.

Prerequisites

  • VPS with Docker installed (Hetzner, DigitalOcean, etc.)
  • SSH access to your server
  • Domain name pointed to your server IP
  • Basic command line knowledge
1

SSH into your server

2

Create project directory

3

Create docker-compose.yml

4

Start the container

Leave the account variables commented out for browser setup. For automated setup, enable both account pairs and set distinct generated passwords in a private .env file before starting.
5

Verify it's running

You should see logs indicating the server started on port 8080.

Option 2: Docker Run

For simpler setups without compose:

Configure Reverse Proxy

The VPS examples bind port 8080 to the server’s loopback address. Run the reverse proxy on that host and expose ports 80 and 443 for public traffic. Replace your-domain.com with a hostname whose DNS points to the VPS.
Install Nginx and Certbot for your operating system using the Certbot instructions. On a typical Linux installation, save this initial HTTP configuration as /etc/nginx/conf.d/primo.conf:
primo.conf
Test and reload Nginx, then verify the hostname reaches the instance over HTTP. Complete account setup after HTTPS is enabled.
Obtain a certificate and let Certbot add HTTPS and the HTTP redirect:
Open https://your-domain.com/admin/ and verify the certificate before signing in. This sequence starts without certificate paths; Certbot adds them after issuance.

Platform-Specific Guides

Hetzner Cloud

Hetzner offers affordable VPS hosting in Europe and the US.
  1. Choose a current VPS plan with enough memory and persistent storage for your sites
  2. Install Docker:
  3. Follow VPS deployment steps above
  4. Configure firewall to allow ports 80, 443, and 22

DigitalOcean

DigitalOcean provides simple VPS hosting with excellent documentation.
  1. Choose a current Droplet plan with persistent storage
  2. Choose “Docker” from one-click apps (or install manually)
  3. Follow VPS deployment steps above
  4. Use DigitalOcean’s cloud firewalls for security

Fly.io

Fly.io bills compute and persistent volumes by usage. New accounts have a trial, not an ongoing free tier; check Fly.io pricing. Use one Machine for this single-volume SQLite deployment:
1

Install Fly CLI

2

Login to Fly

3

Create fly.toml

4

Create volume

5

Deploy

Database Backups

Primo uses SQLite, stored in /app/pb_data. Back up this directory regularly.
Backups stored on the same volume do not protect against losing that volume. Keep an off-site copy and separately back up uploads stored in S3; PocketBase backups exclude S3-uploaded files.
PocketBase has built-in automated backups that you can configure through the admin UI. New instances back up daily at 03:00 and keep the 7 most recent backups, stored locally. To use a different schedule from the start, set PRIMO_BACKUP_CRON. To change it later, or add off-site storage, use the admin UI:
1

Access PocketBase Admin

Navigate to https://your-domain.com/_/ and sign in with your PocketBase superuser credentials.
2

Go to Settings → Backups

Click on Settings in the sidebar, then select the Backups tab.
3

Configure backup schedule

Set a cron expression for your backup schedule:
  • Daily at 2 AM: 0 2 * * *
  • Every 6 hours: 0 */6 * * *
  • Hourly: 0 * * * *
  • Every minute (not recommended): * * * * *
4

Choose backup location

  • Local storage: Backups saved to /app/pb_data/backups
  • S3-compatible storage: Configure AWS S3, Backblaze B2, or similar for off-site backups
5

Set retention policy

Configure how many backup files to keep. For example, retaining 7 files gives you about 7 days of history with one successful daily backup.
For production sites, configure S3-compatible storage for off-site backups. This protects against server failures.

Manual backups with Docker

Prefer PocketBase’s built-in backups for a running instance. A raw copy of pb_data must be made while Primo is stopped to keep SQLite and its associated files consistent. See PocketBase backup and restore. For the container named primo created with the Docker Run example:
backup-primo.sh
This briefly stops the site. Run the script on the Docker host and copy the backup and image record off-server. For Compose, use docker compose stop primo, get its container ID with docker compose ps -aq primo, copy from that ID, and restart with docker compose start primo. Do not use docker compose down before copying: it removes the container.

Restore from a backup

For a PocketBase backup ZIP, use Settings → Backups → Restore in /_/. Test on an isolated instance before restoring production. For a raw pb_data directory made with the script above, restore into a new named volume and container. Keep the existing instance and backup until you verify the restored site. Set task_backup_path to your actual backup folder; the recorded image must still exist on the Docker host, or you must pull the same version from the registry.
Use a new container and volume name for each restore attempt. Open http://localhost:8081/admin/ on the Docker host and check sign-in, pages, content, and uploads before switching your reverse proxy. Hostname routing may require a matching Host header or a temporary domain assignment in the isolated copy. Restore separately stored S3 media too. Docker supports copying into a stopped container.
Restoring replaces the site’s state with the backup’s state. Keep a backup of the current instance and verify the restore before routing production traffic to it.

Updating Primo

Docker Compose

Docker Run

Keep the original environment variables and volume when recreating the container.
Always backup before updating, especially for major version changes.

Performance Optimization

Enable Gzip Compression

In your reverse proxy: Nginx:
Caddy:

Cache published assets carefully

Keep /admin, /api, and authentication traffic out of CDN caches. Use long-lived immutable caching only for assets whose URLs change when their contents change. Fixed image names, HTML, and unversioned scripts need revalidation or short cache lifetimes. For a reverse proxy, keep asset requests routed to Primo and verify the response headers after publishing. A new Nginx location block without proxy_pass would try to serve files from Nginx’s own filesystem.

Use a CDN

Point your domain through Cloudflare (free tier) for:
  • Global CDN
  • DDoS protection
  • SSL certificates
  • Caching

Security Checklist

Generate random passwords for all accounts. Never use default or simple passwords.
Only allow ports 80, 443, and 22 (SSH). Block all other inbound traffic.
Regularly update Docker and the Primo image for security patches.
Never serve Primo over HTTP in production. Use Let’s Encrypt for free SSL certificates.
  • Disable password authentication
  • Use SSH keys only
  • Change default SSH port
  • Use fail2ban to block brute force attempts
Automate daily backups and store them off-server (S3, Backblaze, etc.).
Check Docker logs regularly for suspicious activity:

Troubleshooting

See the Troubleshooting Guide for common deployment issues and solutions.

Run the CMS from source

These steps run the CMS application from its source repository. To build a site locally, use CLI local development instead.
  • devenv (install here) or Dev Container compatible environment (VS Code)
  • Git
Local Development Steps:
1

Clone the repository

2

Open the devenv shell

If you have configured direnv to automatically open the devenv shell, you may ignore this step.
3

Install dependencies

4

Build the application

Initial build is required before starting the dev server.
5

Start development server

6

Access the application

Local development is for testing only. All code and content is stored in local database under pb_data directory.

Next Steps

Environment Variables

Configure Primo with environment variables

Troubleshooting

Solve common deployment issues