> ## Documentation Index
> Fetch the complete documentation index at: https://docs.primo.build/llms.txt
> Use this file to discover all available pages before exploring further.

# Deployment Guide

> Deploy Primo to production on Railway, VPS, or custom infrastructure.

This guide covers deploying Primo to production. Choose the deployment method that matches your needs and technical expertise.

<Warning>
  **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.
</Warning>

## Quick Deploy: Railway

The fastest way to deploy Primo to production.

### Prerequisites

* [Railway account](https://railway.com/?referralCode=RCPU7k) (free to start)
* Domain name (optional, Railway provides a free subdomain)

### Deployment Steps

<Steps>
  <Step title="Click deploy button">
    [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/primo?referralCode=RCPU7k)
  </Step>

  <Step title="Configure environment (optional)">
    Set environment variables if you want automated setup:

    * `PRIMO_SUPERUSER_EMAIL`
    * `PRIMO_SUPERUSER_PASSWORD`
    * `PRIMO_APP_URL` (optional, auto-detected)
  </Step>

  <Step title="Wait for deployment">
    Railway will:

    * Pull the Docker image
    * Create a persistent volume
    * Deploy the container
    * Generate a URL
  </Step>

  <Step title="Access your instance">
    Click the generated URL to access Primo. If you didn't set environment variables, you'll see the setup screen.
  </Step>

  <Step title="Configure custom domain (optional)">
    Railway Settings → Domains → Add custom domain
  </Step>
</Steps>

**Cost**: From \$5/month (includes persistent volume)

<Tip>
  Railway automatically handles SSL certificates, deployments, and scaling. Perfect for production use.
</Tip>

## VPS Deployment

Deploy to any VPS for full control over your infrastructure.

### Prerequisites

* VPS with Docker installed ([Hetzner](https://www.hetzner.com), [DigitalOcean](https://www.digitalocean.com), etc.)
* SSH access to your server
* Domain name pointed to your server IP
* Basic command line knowledge

### Option 1: Docker Compose (Recommended)

<Steps>
  <Step title="SSH into your server">
    ```bash theme={null}
    ssh user@your-server-ip
    ```
  </Step>

  <Step title="Create project directory">
    ```bash theme={null}
    mkdir primo
    cd primo
    ```
  </Step>

  <Step title="Create docker-compose.yml">
    ```yaml theme={null}
    services:
      primo:
        image: ghcr.io/primocms/primo:latest
        restart: always
        ports:
          - "8080:8080"
        volumes:
          - primo-data:/app/pb_data
        environment:
          - PRIMO_APP_URL=https://your-domain.com
          # Optional: automated setup
          # - PRIMO_SUPERUSER_EMAIL=admin@example.com
          # - PRIMO_SUPERUSER_PASSWORD=<secret>

    volumes:
      primo-data:
    ```
  </Step>

  <Step title="Start the container">
    ```bash theme={null}
    docker-compose up -d
    ```
  </Step>

  <Step title="Verify it's running">
    ```bash theme={null}
    docker-compose logs -f
    ```

    You should see logs indicating the server started on port 8080.
  </Step>
</Steps>

### Option 2: Docker Run

For simpler setups without compose:

```bash theme={null}
docker run -d \
  --name primo \
  --restart always \
  -p 8080:8080 \
  -v primo-data:/app/pb_data \
  -e PRIMO_APP_URL=https://your-domain.com \
  ghcr.io/primocms/primo:latest
```

### Configure Reverse Proxy

Primo runs on port 8080. Use a reverse proxy for SSL and domain routing:

<Tabs>
  <Tab title="Nginx">
    ```nginx theme={null}
    server {
        listen 80;
        server_name your-domain.com;
        return 301 https://$server_name$request_uri;
    }

    server {
        listen 443 ssl http2;
        server_name your-domain.com;

        ssl_certificate /path/to/cert.pem;
        ssl_certificate_key /path/to/key.pem;

        location / {
            proxy_pass http://localhost:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
    ```

    Get SSL certificates with [Certbot](https://certbot.eff.org/):

    ```bash theme={null}
    sudo certbot --nginx -d your-domain.com
    ```
  </Tab>

  <Tab title="Caddy">
    Caddy automatically handles SSL certificates:

    ```caddy theme={null}
    your-domain.com {
        reverse_proxy localhost:8080
    }
    ```

    Start Caddy:

    ```bash theme={null}
    caddy run --config Caddyfile
    ```
  </Tab>

  <Tab title="Traefik">
    Add labels to docker-compose.yml:

    ```yaml theme={null}
    services:
      primo:
        image: ghcr.io/primocms/primo:latest
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.primo.rule=Host(`your-domain.com`)"
          - "traefik.http.routers.primo.entrypoints=websecure"
          - "traefik.http.routers.primo.tls.certresolver=letsencrypt"
          - "traefik.http.services.primo.loadbalancer.server.port=8080"
    ```
  </Tab>
</Tabs>

## Platform-Specific Guides

### Hetzner Cloud

[Hetzner](https://www.hetzner.com) offers affordable VPS hosting in Europe and the US.

1. Create a VPS (CX22 recommended: €4/month)
2. Install Docker:
   ```bash theme={null}
   curl -fsSL https://get.docker.com -o get-docker.sh
   sudo sh get-docker.sh
   ```
3. Follow VPS deployment steps above
4. Configure firewall to allow ports 80, 443, and 22

### DigitalOcean

[DigitalOcean](https://www.digitalocean.com) provides simple VPS hosting with excellent documentation.

1. Create a Droplet (\$6/month minimum)
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](https://fly.io) offers a free tier with persistent volumes:

<Steps>
  <Step title="Install Fly CLI">
    ```bash theme={null}
    curl -L https://fly.io/install.sh | sh
    ```
  </Step>

  <Step title="Login to Fly">
    ```bash theme={null}
    fly auth login
    ```
  </Step>

  <Step title="Create fly.toml">
    ```toml theme={null}
    app = "your-app-name"
    primary_region = "sjc"

    [build]
      image = "ghcr.io/primocms/primo:latest"

    [http_service]
      internal_port = 8080
      force_https = true

    [[mounts]]
      source = "primo_data"
      destination = "/app/pb_data"

    [env]
      PRIMO_APP_URL = "https://your-app-name.fly.dev"
    ```
  </Step>

  <Step title="Create volume">
    ```bash theme={null}
    fly volumes create primo_data --size 3
    ```
  </Step>

  <Step title="Deploy">
    ```bash theme={null}
    fly deploy
    ```
  </Step>
</Steps>

## Database Backups

Primo uses SQLite, stored in `/app/pb_data`. Back up this directory regularly.

<Info>
  **Cloud users:** Backups are handled automatically. Backup frequency depends on your plan.
</Info>

### PocketBase Automated Backups (Recommended)

PocketBase has built-in automated backups that you can configure through the admin UI.

<Steps>
  <Step title="Access PocketBase Admin">
    Navigate to `https://your-domain.com/_/` and log in with your admin credentials.
  </Step>

  <Step title="Go to Settings → Backups">
    Click on Settings in the sidebar, then select the Backups tab.
  </Step>

  <Step title="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): `* * * * *`
  </Step>

  <Step title="Choose backup location">
    * **Local storage**: Backups saved to `/app/pb_data/backups`
    * **S3-compatible storage**: Configure [AWS S3](https://aws.amazon.com/s3/), [Backblaze B2](https://www.backblaze.com/b2/cloud-storage.html), or similar for off-site backups
  </Step>

  <Step title="Set retention policy">
    Configure how many backups to keep (e.g., keep last 7 days of backups).
  </Step>
</Steps>

<Tip>
  For production sites, configure S3-compatible storage for off-site backups. This protects against server failures.
</Tip>

### Manual Backups with Docker

```bash theme={null}
#!/bin/bash
# backup-primo.sh

BACKUP_DIR="/backups/primo"
DATE=$(date +%Y%m%d_%H%M%S)

# Create backup directory
mkdir -p $BACKUP_DIR

# Copy pb_data
docker cp primo:/app/pb_data $BACKUP_DIR/pb_data_$DATE

# Keep only last 7 days
find $BACKUP_DIR -type d -mtime +7 -exec rm -rf {} +
```

Add to crontab:

```bash theme={null}
0 2 * * * /path/to/backup-primo.sh
```

### Restore from Backup

```bash theme={null}
# Stop container
docker-compose down

# Restore backup
docker cp pb_data_20250101_020000 primo:/app/pb_data

# Start container
docker-compose up -d
```

<Warning>
  Test your backups regularly. A backup you haven't tested is not a backup.
</Warning>

## Updating Primo

### Docker Compose

```bash theme={null}
# Pull latest image
docker-compose pull

# Restart with new image
docker-compose up -d
```

### Docker Run

```bash theme={null}
# Stop and remove old container
docker stop primo
docker rm primo

# Pull latest image
docker pull ghcr.io/primocms/primo:latest

# Start new container (same command as initial deploy)
docker run -d --name primo ...
```

<Tip>
  Always backup before updating, especially for major version changes.
</Tip>

## Performance Optimization

### Enable Gzip Compression

In your reverse proxy:

**Nginx:**

```nginx theme={null}
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
```

**Caddy:**

```caddy theme={null}
encode gzip
```

### Add Caching Headers

```nginx theme={null}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}
```

### Use a CDN

Point your domain through Cloudflare (free tier) for:

* Global CDN
* DDoS protection
* SSL certificates
* Caching

## Security Checklist

<AccordionGroup>
  <Accordion title="Use strong passwords">
    Generate random passwords for all accounts. Never use default or simple passwords.
  </Accordion>

  <Accordion title="Enable firewall">
    Only allow ports 80, 443, and 22 (SSH). Block all other inbound traffic.
  </Accordion>

  <Accordion title="Keep Docker updated">
    Regularly update Docker and the Primo image for security patches.
  </Accordion>

  <Accordion title="Use HTTPS everywhere">
    Never serve Primo over HTTP in production. Use Let's Encrypt for free SSL certificates.
  </Accordion>

  <Accordion title="Restrict SSH access">
    * Disable password authentication
    * Use SSH keys only
    * Change default SSH port
    * Use fail2ban to block brute force attempts
  </Accordion>

  <Accordion title="Regular backups">
    Automate daily backups and store them off-server (S3, Backblaze, etc.).
  </Accordion>

  <Accordion title="Monitor logs">
    Check Docker logs regularly for suspicious activity:

    ```bash theme={null}
    docker-compose logs -f --tail=100
    ```
  </Accordion>
</AccordionGroup>

## Troubleshooting

See the [Troubleshooting Guide](/reference/troubleshooting) for common deployment issues and solutions.

## Next Steps

<CardGroup cols={2}>
  <Card title="Environment Variables" icon="gear" href="/reference/environment-variables">
    Configure Primo with environment variables
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="/reference/troubleshooting">
    Solve common deployment issues
  </Card>
</CardGroup>
