Skip to content

Deployment

Sails SQLite is designed for production deployment with robust defaults and deployment-ready configurations.

Production Configuration

Optimized Production Settings

javascript
// config/env/production.js
module.exports = {
  datastores: {
    default: {
      adapter: 'sails-sqlite',
      url: process.env.DATABASE_URL || '/app/data/production.sqlite',

      // Production-optimized pragmas (already applied by default)
      pragmas: {
        journal_mode: 'WAL', // Better concurrency
        synchronous: 'NORMAL', // Balanced safety/performance
        cache_size: -524288, // 512MB cache for production
        mmap_size: 1073741824, // 1GB memory mapping
        busy_timeout: 30000, // 30 second timeout
        foreign_keys: 'ON' // Data integrity
      },

      // Production connection settings
      timeout: 30000, // 30 second connection timeout
      verbose: null // Disable query logging in production
    }
  },

  // Production model settings
  models: {
    migrate: 'safe', // Never auto-drop tables in production
    attributes: {
      createdAt: { type: 'number', autoCreatedAt: true },
      updatedAt: { type: 'number', autoUpdatedAt: true }
    }
  }
}

Platform Deployment

Railway

Railway provides excellent SQLite support with persistent volumes:

dockerfile
# Dockerfile
FROM node:18-alpine

# Install native dependencies
RUN apk add --no-cache python3 make g++

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application
COPY . .

# Create data directory with proper permissions
RUN mkdir -p /app/data && chmod 755 /app/data

EXPOSE 1337

CMD ["node", "app.js"]
javascript
// config/env/production.js (Railway)
module.exports = {
  datastores: {
    default: {
      adapter: 'sails-sqlite',
      url: '/app/data/production.sqlite' // Persistent volume path
    }
  }
}

Render

Render supports SQLite with persistent disks:

yaml
# render.yaml
services:
  - type: web
    name: my-sails-app
    env: node
    buildCommand: npm install
    startCommand: npm start
    disk:
      name: sqlite-data
      mountPath: /app/data
      sizeGB: 10
    envVars:
      - key: DATABASE_URL
        value: /app/data/production.sqlite
      - key: NODE_ENV
        value: production

Docker Deployment

Complete Docker setup with volume mounting:

dockerfile
# Dockerfile
FROM node:18-alpine

# Install build dependencies
RUN apk add --no-cache \
    python3 \
    make \
    g++ \
    sqlite

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production && npm cache clean --force

# Copy application code
COPY . .

# Create data directory
RUN mkdir -p /app/data

# Set proper permissions
RUN chown -R node:node /app

USER node

EXPOSE 1337

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:1337/health || exit 1

CMD ["node", "app.js"]
yaml
# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - '1337:1337'
    volumes:
      - sqlite-data:/app/data
    environment:
      - NODE_ENV=production
      - DATABASE_URL=/app/data/production.sqlite
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:1337/health']
      interval: 30s
      timeout: 10s
      retries: 3

volumes:
  sqlite-data:
    driver: local

File System Considerations

Database File Location

Choose the right location for your SQLite file:

javascript
// config/env/production.js
module.exports = {
  datastores: {
    default: {
      adapter: 'sails-sqlite',
      // Options in order of preference:
      url:
        process.env.DATABASE_URL || // Environment variable
        '/app/data/production.sqlite' || // Persistent volume
        './data/production.sqlite' || // Relative path
        '/tmp/production.sqlite' // Temporary (not recommended)
    }
  }
}

Permissions and Security

Ensure proper file permissions:

bash
# Set secure permissions
chmod 640 /app/data/production.sqlite
chown app:app /app/data/production.sqlite

# Directory permissions
chmod 755 /app/data
chown app:app /app/data

File System Performance

For optimal performance:

javascript
// Mount database on fast storage
// Prefer SSD over HDD
// Consider using tmpfs for cache directories

// config/env/production.js
module.exports = {
  datastores: {
    default: {
      adapter: 'sails-sqlite',
      url: '/fast-ssd/data/production.sqlite', // SSD storage
      pragmas: {
        temp_store: 'MEMORY', // Keep temp files in memory
        mmap_size: 1073741824 // 1GB memory mapping
      }
    }
  }
}

Database Migrations

Safe Production Migrations

javascript
// config/env/production.js
module.exports = {
  models: {
    migrate: 'safe' // Never drop tables automatically
  }
}

Manual Migration Strategy

javascript
// scripts/migrate.js
const sails = require('sails')

async function migrate() {
  await sails.lift({
    models: { migrate: 'alter' }, // Allow schema changes
    hooks: { grunt: false } // Skip grunt in migration
  })

  try {
    // Run your migrations
    console.log('Running migrations...')

    // Add new columns
    await sails.sendNativeQuery(`
      ALTER TABLE users ADD COLUMN phone_number TEXT
    `)

    // Create new indexes
    await sails.sendNativeQuery(`
      CREATE INDEX IF NOT EXISTS idx_users_phone 
      ON users(phone_number)
    `)

    console.log('Migrations completed successfully')
  } catch (error) {
    console.error('Migration failed:', error)
    throw error
  } finally {
    await sails.lower()
  }
}

migrate().catch(console.error)

Run migrations before deployment:

bash
# Run migration script
node scripts/migrate.js

# Then deploy with safe mode
NODE_ENV=production node app.js

Backup and Recovery

Automated Backups

javascript
// scripts/backup.js
const fs = require('fs').promises
const path = require('path')

async function createBackup() {
  const timestamp = new Date().toISOString().replace(/:/g, '-')
  const backupPath = `/backups/db-backup-${timestamp}.sqlite`
  const sourcePath = '/app/data/production.sqlite'

  try {
    // Copy database file
    await fs.copyFile(sourcePath, backupPath)

    // Also backup WAL and SHM files if they exist
    try {
      await fs.copyFile(`${sourcePath}-wal`, `${backupPath}-wal`)
      await fs.copyFile(`${sourcePath}-shm`, `${backupPath}-shm`)
    } catch (walError) {
      // WAL files might not exist, which is fine
    }

    console.log(`Backup created: ${backupPath}`)

    // Clean old backups (keep last 7 days)
    await cleanOldBackups()
  } catch (error) {
    console.error('Backup failed:', error)
    throw error
  }
}

async function cleanOldBackups() {
  const backupDir = '/backups'
  const files = await fs.readdir(backupDir)
  const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000 // 7 days

  for (const file of files) {
    if (file.startsWith('db-backup-')) {
      const filePath = path.join(backupDir, file)
      const stats = await fs.stat(filePath)

      if (stats.mtime.getTime() < cutoff) {
        await fs.unlink(filePath)
        console.log(`Deleted old backup: ${file}`)
      }
    }
  }
}

// Run backup
createBackup().catch(console.error)

Set up automated backups:

bash
# Add to crontab for daily backups at 2 AM
0 2 * * * /usr/bin/node /app/scripts/backup.js

Point-in-Time Recovery

With WAL mode, you can implement point-in-time recovery:

javascript
// scripts/restore.js
const fs = require('fs').promises

async function restoreFromBackup(backupPath) {
  const productionPath = '/app/data/production.sqlite'

  try {
    // Stop application first
    console.log('Restoring from backup:', backupPath)

    // Backup current database (safety)
    await fs.copyFile(productionPath, `${productionPath}.pre-restore`)

    // Restore from backup
    await fs.copyFile(backupPath, productionPath)

    // Restore WAL file if it exists
    try {
      await fs.copyFile(`${backupPath}-wal`, `${productionPath}-wal`)
    } catch (walError) {
      // WAL file might not exist
    }

    console.log('Restore completed successfully')
  } catch (error) {
    console.error('Restore failed:', error)
    throw error
  }
}

Monitoring and Health Checks

Application Health Check

javascript
// api/controllers/health/check.js
module.exports = {
  friendlyName: 'Health check',

  description: 'Check application and database health status.',

  exits: {
    success: {
      description: 'Application is healthy.'
    },
    unhealthy: {
      statusCode: 503,
      description: 'Application or database is unhealthy.'
    }
  },

  fn: async function () {
    try {
      const dsEntry = sails.datastores.default

      // Check database health
      const isHealthy = dsEntry.manager.isHealthy()

      if (!isHealthy) {
        throw {
          unhealthy: {
            status: 'unhealthy',
            database: 'disconnected'
          }
        }
      }

      // Test query to ensure database is responding
      await sails.sendNativeQuery('SELECT 1')

      return {
        status: 'healthy',
        timestamp: Date.now(),
        database: 'connected'
      }
    } catch (error) {
      throw {
        unhealthy: {
          status: 'unhealthy',
          error: error.message
        }
      }
    }
  }
}

Database Metrics

javascript
// api/controllers/metrics/database.js
module.exports = {
  friendlyName: 'Get database metrics',

  description: 'Retrieve database statistics and file information.',

  exits: {
    success: {
      description: 'Database metrics retrieved successfully.'
    },
    serverError: {
      statusCode: 500,
      description: 'Server error occurred while retrieving metrics.'
    }
  },

  fn: async function () {
    try {
      const dsEntry = sails.datastores.default

      // Get database statistics
      const stats = await sails.sendNativeQuery(`
        SELECT 
          name,
          (SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=m.name) as table_count,
          (SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND tbl_name=m.name) as index_count
        FROM sqlite_master m WHERE type='table'
      `)

      // Get file size
      const fs = require('fs')
      const filePath = '/app/data/production.sqlite'
      const fileStats = fs.statSync(filePath)

      return {
        file_size_mb: Math.round(fileStats.size / 1024 / 1024),
        tables: stats.rows,
        wal_mode: true,
        cache_size_mb: 512,
        mmap_size_mb: 1024
      }
    } catch (error) {
      throw {
        serverError: {
          error: error.message
        }
      }
    }
  }
}

Performance in Production

Connection Monitoring

javascript
// config/bootstrap.js
module.exports.bootstrap = async function () {
  // Monitor database performance in production
  if (process.env.NODE_ENV === 'production') {
    const dsEntry = sails.datastores.default

    // Log slow queries
    dsEntry.manager.onSlowQuery((sql, time) => {
      console.warn(`SLOW QUERY (${time}ms):`, sql.substring(0, 200))
    })

    // Periodic health checks
    setInterval(() => {
      if (!dsEntry.manager.isHealthy()) {
        console.error('Database health check failed!')
      }
    }, 60000) // Every minute

    // Periodic optimization
    setInterval(() => {
      dsEntry.manager.analyze() // Update statistics
    }, 3600000) // Every hour
  }

  return
}

Security Considerations

File System Security

bash
# Secure the database file
chmod 600 /app/data/production.sqlite
chown app:app /app/data/production.sqlite

# Secure the data directory
chmod 700 /app/data
chown app:app /app/data

Application Security

javascript
// Ensure SQL injection protection
const users = await sails.sendNativeQuery(
  `
  SELECT * FROM users WHERE email = ?
`,
  [email]
) // Always use parameterized queries

// Never construct SQL with string concatenation
// ❌ DANGEROUS
const users = await sails.sendNativeQuery(`
  SELECT * FROM users WHERE email = '${email}'
`)

Troubleshooting

Common Production Issues

javascript
// Database locked errors
pragmas: {
  busy_timeout: 60000  // Increase timeout to 60 seconds
}

// Memory issues
pragmas: {
  cache_size: -131072,  // Reduce cache to 128MB
  mmap_size: 134217728  // Reduce mmap to 128MB
}

// Disk space issues
// Implement regular VACUUM operations
setInterval(async () => {
  const dsEntry = sails.datastores.default
  dsEntry.manager.vacuum()
}, 86400000) // Daily vacuum

All open source projects are released under the MIT License.