Quick Deploy: Railway
The fastest way to deploy Primo to production.Prerequisites
- Railway account; review plan and resource pricing
- Domain name (optional, Railway provides a free subdomain)
Deployment Steps
Configure environment (optional)
PRIMO_SUPERUSER_EMAILPRIMO_SUPERUSER_PASSWORDPRIMO_USER_EMAILandPRIMO_USER_PASSWORD: create the regular Developer account used for Primo sign-inPRIMO_APP_URL: set the public HTTPS URL used for backend linksPRIMO_DOMAIN_PROVIDER=railwayandPRIMO_RAILWAY_TOKEN: connect custom site domains from the Primo dashboard (see below). Leave these unset when you only use the Railway-generated hostname
Wait for deployment
- Pull the Docker image
- Create a persistent volume
- Deploy the container
- Generate a URL
Access your instance
PRIMO_USER_EMAIL and PRIMO_USER_PASSWORD; the superuser account is for PocketBase at /_/.Connect site domains from the dashboard
- In your Railway project, go to Settings → Tokens → New Project Token
- Add two variables to the service, then redeploy:
PRIMO_DOMAIN_PROVIDER=railwayPRIMO_RAILWAY_TOKEN= the token
- 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.
- In your Railway service, go to Settings → Networking → Custom Domain and add the domain.
- Add the DNS records Railway shows at your registrar, and wait for Railway to issue the certificate.
- In the Primo dashboard, choose Assign domain on the site, enter the domain, and click Mark as connected.
/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.
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
Option 1: Docker Compose (Recommended)
SSH into your server
Create project directory
Create docker-compose.yml
Start the container
.env file before starting.Verify it's running
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. Replaceyour-domain.com with a hostname whose DNS points to the VPS.
- Nginx
- Caddy
- Traefik
/etc/nginx/conf.d/primo.conf: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.- Choose a current VPS plan with enough memory and persistent storage for your sites
- Install Docker:
- Follow VPS deployment steps above
- Configure firewall to allow ports 80, 443, and 22
DigitalOcean
DigitalOcean provides simple VPS hosting with excellent documentation.- Choose a current Droplet plan with persistent storage
- Choose “Docker” from one-click apps (or install manually)
- Follow VPS deployment steps above
- 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:Install Fly CLI
Login to Fly
Create fly.toml
Create volume
Deploy
Database Backups
Primo uses SQLite, stored in/app/pb_data. Back up this directory regularly.
PocketBase Automated Backups (Recommended)
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, setPRIMO_BACKUP_CRON. To change it later, or add off-site storage, use the admin UI:
Access PocketBase Admin
https://your-domain.com/_/ and sign in with your PocketBase superuser credentials.Go to Settings → Backups
Configure backup schedule
- Daily at 2 AM:
0 2 * * * - Every 6 hours:
0 */6 * * * - Hourly:
0 * * * * - Every minute (not recommended):
* * * * *
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
Set retention policy
Manual backups with Docker
Prefer PocketBase’s built-in backups for a running instance. A raw copy ofpb_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:
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.
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.
Updating Primo
Docker Compose
Docker Run
Performance Optimization
Enable Gzip Compression
In your reverse proxy: Nginx: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
Use strong passwords
Use strong passwords
Enable firewall
Enable firewall
Keep Docker updated
Keep Docker updated
Use HTTPS everywhere
Use HTTPS everywhere
Restrict SSH access
Restrict SSH access
- Disable password authentication
- Use SSH keys only
- Change default SSH port
- Use fail2ban to block brute force attempts
Regular backups
Regular backups
Monitor logs
Monitor logs
Troubleshooting
See the Troubleshooting Guide for common deployment issues and solutions.Run the CMS from source
Contributor setup
Contributor setup
- devenv (install here) or Dev Container compatible environment (VS Code)
- Git
Clone the repository
Open the devenv shell
Install dependencies
Build the application
Start development server
Access the application
- Main app (dev): http://localhost:5173
- Built app: http://localhost:8090
- PocketBase Admin: http://localhost:8090/_