Skip to main content

OJS Server Infrastructure: Requirements, Databases, Backup, Object Storage, Cloud & Docker

Running a reliable Open Journal Systems (OJS) installation requires deliberate decisions about server hardware, database engines, backup strategies, file storage, and deployment architecture. This article consolidates guidance from the PKP Admin Guide, the PKP Community Forum, and the broader developer community into a single reference for site administrators.


1. Server Requirementsโ€‹

1.1 Official Minimum Requirements (OJS 3.4 / 3.5+)โ€‹

PKP publishes authoritative requirements in the Technical Requirements section of the OJS README and in the PKP Admin Guide. The table below reflects the current minimums for OJS 3.5.x:

ComponentMinimumRecommended
PHP8.18.2 or 8.3
MySQL8.08.0+
MariaDB10.610.11 LTS+
PostgreSQL1415 or 16
Apache2.42.4 (with mod_rewrite)
Nginx1.20latest stable
RAM1 GB4 GB+ (busy journals)
CPU1 vCPU2+ vCPUs
Disk10 GB50 GB+ (scales with uploads)

Source: PKP Admin Guide โ€” System Requirements, OJS 3.5 README on GitHub

1.2 Required PHP Extensionsโ€‹

OJS requires these PHP extensions to be compiled or enabled:

mbstring xml json zip gd intl curl mysqli (or pgsql)

The full list, as maintained by PKP, is documented in the PKP technical documentation. Some optional extensions unlock additional features:

ExtensionPurpose
imagickBetter PDF thumbnail generation
opcachePHP bytecode caching (strongly recommended for performance)
apcuIn-memory key-value cache
redisIf using Redis as the job/cache driver

1.3 Web Server Notesโ€‹

Apache requires mod_rewrite and AllowOverride All in the OJS virtual host block so that .htaccess rules work correctly. OJS ships with a pre-configured .htaccess file.

Nginx requires a custom server block โ€” OJS does not use .htaccess under Nginx. The PKP Admin Guide provides a sample Nginx configuration. A minimal snippet:

server {
listen 443 ssl http2;
server_name journal.example.com;

root /var/www/html/ojs;
index index.php;

location / {
try_files $uri $uri/ /index.php?$query_string;
}

location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

# Block direct access to sensitive directories
location ~* ^/(cache|templates_c|db|config) {
deny all;
}
}

1.4 Sizing Guidance from the Communityโ€‹

PKP Community Forum discussions (forum.pkp.sfu.ca) repeatedly suggest:

  • Small journal (< 500 submissions/year): 2 vCPU, 2 GB RAM, 20 GB SSD
  • Medium journal (500โ€“2000 submissions/year): 4 vCPU, 4 GB RAM, 100 GB SSD
  • Multi-journal / high-traffic installation: 8+ vCPU, 8 GB+ RAM, separate database server, offload files to object storage

Community reference: PKP Forum โ€“ Server Sizing Discussions


2. Databasesโ€‹

2.1 Supported Enginesโ€‹

OJS supports MySQL/MariaDB and PostgreSQL. The choice affects character-set handling, performance characteristics, and operational tooling.

MySQL 8.0+ / MariaDB 10.6+PostgreSQL 14+
OJS supportโœ… Primary, most testedโœ… Fully supported
Character setutf8mb4 + utf8mb4_unicode_ciUTF-8 by default
JSON supportNative JSON columns (OJS uses some)JSONB (superior)
Full-text searchMyISAM/InnoDB FTStsvector / GIN indexes
ReplicationBinary log replicationStreaming / logical
Cloud-managedRDS, Cloud SQL, Azure DBRDS PG, Cloud SQL PG, Azure DB PG, Supabase

Source: PKP Admin Guide โ€” Database Setup

2.2 Creating the OJS Databaseโ€‹

MySQL / MariaDB:

CREATE DATABASE ojs CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'ojsuser'@'localhost' IDENTIFIED BY 'StrongPassword!';
GRANT ALL PRIVILEGES ON ojs.* TO 'ojsuser'@'localhost';
FLUSH PRIVILEGES;

PostgreSQL:

CREATE USER ojsuser WITH PASSWORD 'StrongPassword!';
CREATE DATABASE ojs OWNER ojsuser ENCODING 'UTF8'
LC_COLLATE='en_US.UTF-8' LC_CTYPE='en_US.UTF-8' TEMPLATE template0;
GRANT ALL PRIVILEGES ON DATABASE ojs TO ojsuser;

2.3 Configuring config.inc.phpโ€‹

; MySQL
driver = mysqli
host = localhost
username = ojsuser
password = StrongPassword!
name = ojs

; PostgreSQL
; driver = pgsql
; host = localhost
; username = ojsuser
; password = StrongPassword!
; name = ojs

2.4 Performance Tuningโ€‹

MySQL/MariaDB โ€” key my.cnf settings for a 4 GB RAM server:

[mysqld]
innodb_buffer_pool_size = 2G
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 2
query_cache_type = 0 # Disable deprecated query cache
max_connections = 150

PostgreSQL โ€” key postgresql.conf settings:

shared_buffers = 1GB
effective_cache_size = 3GB
work_mem = 16MB
maintenance_work_mem = 256MB
wal_buffers = 16MB
max_connections = 100

Community reference: PKP Forum threads on slow OJS installations often trace back to innodb_buffer_pool_size being too small โ€” see forum.pkp.sfu.ca/t/ojs-performance-slow

2.5 MariaDB vs MySQLโ€‹

The PKP Community Forum has active discussions on MariaDB vs MySQL. MariaDB 10.6 LTS is a popular choice in self-hosted Linux deployments because it ships in default Ubuntu/Debian repositories. MySQL 8.0 is generally preferred when using cloud-managed database services (AWS RDS, Google Cloud SQL). Both are fully supported by OJS.

Source: PKP Forum โ€“ MariaDB vs MySQL discussions

2.6 Database as a Separate Serviceโ€‹

For production installations, running the database on a separate server from the web/PHP server improves:

  • Independent scaling (RAM for DB, CPU for PHP)
  • Simpler backup scheduling
  • Reduced blast radius if the web server is compromised

In config.inc.php, change host = localhost to the private IP or hostname of the database server. Ensure the database user grants access from the web server's IP, not just localhost.


3. Backup Strategyโ€‹

3.1 What Must Be Backed Upโ€‹

A complete OJS backup requires all three of the following components โ€” losing any one of them means an incomplete recovery:

ComponentDefault LocationContains
DatabaseMySQL / PostgreSQLAll journal metadata, users, submissions, settings
Files directoryfiles_dir in config (e.g. /var/www/ojs-files)Uploaded manuscripts, peer-review files, galleys, supplementary files
Public directory[ojs_root]/public/Journal logos, uploaded cover images, CSS overrides
Config file[ojs_root]/config.inc.phpDatabase credentials, encryption keys

Source: PKP Admin Guide โ€” Backup and Restoration

3.2 Database Backupโ€‹

MySQL / MariaDB โ€” consistent, non-locking dump:

mysqldump -u ojsuser -p \
--single-transaction \
--routines \
--triggers \
ojs | gzip > /backups/ojs_db_$(date +%Y%m%d_%H%M).sql.gz

PostgreSQL โ€” custom binary format (fastest restore):

pg_dump -U ojsuser -Fc ojs \
> /backups/ojs_db_$(date +%Y%m%d_%H%M).pgdump

3.3 Files Backupโ€‹

# Files directory
tar -czf /backups/ojs_files_$(date +%Y%m%d_%H%M).tar.gz \
/var/www/ojs-files

# Public directory (journal logos etc.)
tar -czf /backups/ojs_public_$(date +%Y%m%d_%H%M).tar.gz \
/var/www/html/ojs/public

3.4 Automated Backup Scriptโ€‹

Create /etc/ojs-backup.cnf (MySQL credentials file โ€” readable only by root):

[client]
user=ojsuser
password=StrongPassword!
chmod 600 /etc/ojs-backup.cnf

Create /usr/local/bin/ojs-backup.sh:

#!/bin/bash
set -euo pipefail

BACKUP_DIR=/backups/ojs
OJS_ROOT=/var/www/html/ojs
FILES_DIR=/var/www/ojs-files
DB_NAME=ojs
# Credentials are stored in a secured options file, not in this script
DB_OPTS="--defaults-file=/etc/ojs-backup.cnf"
RETENTION_DAYS=30

DATE=$(date +%Y%m%d_%H%M)
mkdir -p "$BACKUP_DIR"

# 1. Database (credentials read from /etc/ojs-backup.cnf)
mysqldump $DB_OPTS \
--single-transaction --routines "$DB_NAME" \
| gzip > "$BACKUP_DIR/ojs_db_$DATE.sql.gz"

# 2. Files directory
tar -czf "$BACKUP_DIR/ojs_files_$DATE.tar.gz" "$FILES_DIR"

# 3. Public directory
tar -czf "$BACKUP_DIR/ojs_public_$DATE.tar.gz" "$OJS_ROOT/public"

# 4. Config
cp "$OJS_ROOT/config.inc.php" "$BACKUP_DIR/ojs_config_$DATE.inc.php"

# 5. Remove old backups
find "$BACKUP_DIR" -mtime +"$RETENTION_DAYS" -delete

echo "[$(date)] OJS backup completed: $DATE"
chmod +x /usr/local/bin/ojs-backup.sh

Cron entry (runs daily at 2 AM):

0 2 * * * /usr/local/bin/ojs-backup.sh >> /var/log/ojs-backup.log 2>&1

3.5 Off-Site Backupโ€‹

A local backup is not a real backup. Common approaches to off-site copies:

  • rclone to S3 / Backblaze B2 / Wasabi โ€” schedule after the backup script
  • rsync to a remote server over SSH
  • Cloud snapshot โ€” if using a VPS (DigitalOcean, Linode, Vultr), enable automated volume snapshots
# Example: sync backups to an S3-compatible bucket with rclone
rclone copy /backups/ojs s3:my-ojs-backups/$(hostname) \
--transfers 4 --log-file /var/log/rclone-ojs.log

3.6 Restoration Procedureโ€‹

# 1. Restore database (MySQL)
gunzip < /backups/ojs_db_YYYYMMDD_HHMM.sql.gz | mysql -u ojsuser -p ojs

# 1. Restore database (PostgreSQL)
pg_restore -U ojsuser -d ojs /backups/ojs_db_YYYYMMDD_HHMM.pgdump

# 2. Restore files
tar -xzf /backups/ojs_files_YYYYMMDD_HHMM.tar.gz -C /

# 3. Restore public
tar -xzf /backups/ojs_public_YYYYMMDD_HHMM.tar.gz -C /

# 4. Restore config
cp /backups/ojs_config_YYYYMMDD_HHMM.inc.php /var/www/html/ojs/config.inc.php

# 5. Clear cache
cd /var/www/html/ojs && php lib/pkp/tools/cacheClear.php

Source: PKP Admin Guide โ€” Backup and Restoration


4. Object Storageโ€‹

4.1 Why Use Object Storage?โ€‹

By default, OJS stores all uploaded files (manuscripts, galleys, supplementary files) in a local filesystem directory (files_dir). This becomes a bottleneck when:

  • The server disk fills up with large PDF/multimedia galleys
  • You want to scale OJS horizontally (multiple PHP servers cannot share a local filesystem)
  • You need a cost-effective, redundant file store separate from the compute server
  • You are running OJS inside Docker/Kubernetes

Object storage (Amazon S3, MinIO, Backblaze B2, Wasabi, etc.) solves all of these.

4.2 OJS S3 File Manager Pluginโ€‹

PKP publishes an official plugin โ€” S3 File Manager โ€” that integrates Amazon S3 (or any S3-compatible store) as the OJS files driver.

GitHub: https://github.com/pkp/s3files

Installation:

cd /var/www/html/ojs/plugins/generic
git clone https://github.com/pkp/s3files.git

config.inc.php settings:

; Use S3 as the storage backend
storage_driver = s3
; Your bucket region
s3_region = ap-south-1
; Bucket name
s3_bucket = my-ojs-files
; Credentials (use IAM role on EC2/ECS instead of hard-coding)
s3_key = AKIAIOSFODNN7EXAMPLE
s3_secret = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
; For S3-compatible storage (MinIO, Wasabi, Backblaze B2, etc.)
; s3_endpoint = https://s3.wasabisys.com

4.3 MinIO โ€” Self-Hosted S3-Compatible Storageโ€‹

MinIO is a popular self-hosted alternative to AWS S3. It exposes a fully S3-compatible API, so OJS's S3 plugin works without modification.

# Run MinIO with Docker
docker run -d \
-p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
-v /data/minio:/data \
--name minio \
quay.io/minio/minio server /data --console-address ":9001"

Then set s3_endpoint = http://localhost:9000 in config.inc.php.

Community reference: Multiple PKP Forum posts document MinIO + OJS deployments as a cost-effective self-hosted alternative to AWS S3. See forum.pkp.sfu.ca

4.4 S3-Compatible Providers Comparisonโ€‹

ProviderPrice (per GB/month)Notes
AWS S3~$0.023Highest ecosystem, egress costs
Backblaze B2$0.006Free egress with Cloudflare
Wasabi$0.0068No egress fees, S3-compatible
MinIO (self-hosted)Your disk costFull S3 API, docker-native
Cloudflare R2$0.015 (free egress)Zero egress fees

4.5 Migrating Existing Files to Object Storageโ€‹

When switching an existing installation from local filesystem to S3, files must be migrated without breaking the database references. The community-recommended approach:

  1. Configure the S3 plugin in OJS settings
  2. Use a migration script to copy files to the bucket (preserving the same relative path structure OJS uses)
  3. Verify a sample of articles render correctly
  4. Remove the local files only after confirming

Community reference: PKP Forum โ€“ Migrating files to S3


5. Cloud Server Hostingโ€‹

5.1 Choosing a Cloud Providerโ€‹

Any cloud provider that offers virtual machines running Linux is suitable for OJS. The most commonly discussed in the PKP community:

ProviderEntry VPSNotes
DigitalOcean$6/mo (1 vCPU / 1 GB)Simple UI, popular in PKP forum
Linode / Akamai$5/moCompetitive pricing, good docs
Vultr$6/moMany data-center regions
AWS EC2~$10/mo (t3.micro)Most features, steeper learning curve
Google Cloud~$10/mo (e2-micro)Free tier available
Hetznerโ‚ฌ4/moExcellent price-to-performance (EU)

For a production journal, the minimum practical size is 2 vCPU / 2 GB RAM with a dedicated block storage volume for OJS files (so the OS disk doesn't fill up).

# 1. Update system
apt update && apt upgrade -y

# 2. Install LEMP stack
apt install -y nginx php8.2-fpm php8.2-mbstring php8.2-xml \
php8.2-json php8.2-zip php8.2-gd php8.2-intl php8.2-curl \
php8.2-mysql php8.2-opcache mysql-server certbot \
python3-certbot-nginx

# 3. Secure MySQL
mysql_secure_installation

# 4. Obtain SSL certificate
certbot --nginx -d journal.example.com

Source: PKP Admin Guide โ€“ Linux Installation

5.3 PKP Publishing Services (Official Managed Hosting)โ€‹

For institutions without in-house server administration capacity, PKP offers PKP Publishing Services (PS) โ€” a fully managed hosting service operated by the PKP team. It includes:

  • Managed upgrades
  • Backups
  • SSL certificates
  • Support from the OJS developers

Source: https://pkpservices.sfu.ca/

5.4 CDN and Caching in Front of OJSโ€‹

A CDN (Cloudflare, AWS CloudFront) in front of OJS provides:

  • DDoS mitigation โ€” critical for open-access journals that receive automated crawl traffic
  • Static asset caching โ€” reduces server load for CSS, JS, images
  • Global latency reduction โ€” for international reader bases

Cloudflare's free tier is widely used in the PKP community. The key OJS configuration to ensure compatibility:

; In config.inc.php โ€” trust Cloudflare's forwarded IP
force_ssl = On
; Trust the CF-Connecting-IP header
trust_x_forwarded_for = On

Community reference: PKP Forum โ€“ Cloudflare with OJS

5.5 Horizontal Scaling Considerationsโ€‹

OJS was originally designed for single-server deployments. Running multiple PHP app servers behind a load balancer requires:

  1. Shared filesystem or object storage for files_dir โ€” local disk is per-server; use NFS, GlusterFS, or S3
  2. Centralised session storage โ€” configure Redis or Memcached as the session handler in config.inc.php
  3. Centralised cache โ€” set cache_driver = redis in config.inc.php
  4. Job queue โ€” use the database or Redis queue driver, not the local filesystem queue
; Redis for session + cache
cache_driver = redis
session_driver = redis
redis_host = 127.0.0.1
redis_port = 6379

Community reference: PKP Forum โ€“ Load balancer / multi-server OJS


6. Docker Deploymentโ€‹

6.1 Official PKP Docker Imageโ€‹

PKP maintains an official Docker image and docker-compose stack at:

The image is based on the Alpine + Apache + PHP stack and supports environment-variable-driven configuration.

6.2 Minimal docker-compose.ymlโ€‹

version: "3.9"

services:
db:
image: mariadb:10.11
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: ojs
MYSQL_USER: ojsuser
MYSQL_PASSWORD: ojspassword
volumes:
- db_data:/var/lib/mysql

ojs:
image: pkpinc/ojs:3_5_0-0
restart: unless-stopped
depends_on:
- db
ports:
- "8080:80"
environment:
OJS_DB_HOST: db
OJS_DB_USER: ojsuser
OJS_DB_PASSWORD: ojspassword
OJS_DB_NAME: ojs
OJS_WEB_CONF: /etc/apache2/conf.d/ojs.conf
volumes:
- ojs_files:/var/www/files
- ojs_public:/var/www/html/public
# config.inc.php is managed inside the container on first run.
# To customise it, copy it out and use a bind mount to a host file:
# - ./config.inc.php:/var/www/html/config.inc.php

volumes:
db_data:
ojs_files:
ojs_public:

Source: PKP docker-ojs GitHub

6.3 Persistent Volumesโ€‹

The volumes that must be persisted across container restarts and upgrades:

VolumePath in containerContains
ojs_files/var/www/filesAll uploaded article files
ojs_public/var/www/html/publicPublic assets (logos, covers)
./config.inc.php (bind mount)/var/www/html/config.inc.phpOJS configuration file

Losing these volumes means losing journal data. Always map them to named Docker volumes or a bind mount on a managed disk. For config.inc.php, use a bind mount to a host file (not a named volume) so Docker maps the single file rather than a directory.

6.4 Adding Nginx Reverse Proxy and SSLโ€‹

In production, never expose the OJS container directly. Use Nginx as a reverse proxy with certbot for Let's Encrypt:

nginx:
image: nginx:stable-alpine
restart: unless-stopped
depends_on:
- ojs
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- certbot_www:/var/www/certbot
- certbot_certs:/etc/letsencrypt

certbot:
image: certbot/certbot
volumes:
- certbot_www:/var/www/certbot
- certbot_certs:/etc/letsencrypt
entrypoint: >
sh -c "trap exit TERM; while :; do certbot renew --webroot
-w /var/www/certbot --quiet; sleep 12h; done"

6.5 Upgrading OJS in Dockerโ€‹

# 1. Pull the new image version
docker pull pkpinc/ojs:3_5_0-1

# 2. Stop the running stack (database + files volumes are preserved)
docker compose down

# 3. Update the image tag in docker-compose.yml

# 4. Start the updated stack โ€” OJS runs DB migrations automatically
docker compose up -d

# 5. Monitor upgrade logs
docker compose logs -f ojs

Community reference: PKP Forum โ€“ Docker OJS upgrade discussions

6.6 Docker + Object Storageโ€‹

When running OJS in Docker, using object storage for the files volume eliminates the need to manage a persistent volume for uploaded files entirely. Mount a MinIO or S3 bucket via the S3 plugin (see Section 4), and the only persistent volumes you need are ojs_public and ojs_config.

6.7 Kubernetes / Container Orchestrationโ€‹

For large or multi-tenant deployments, OJS can be deployed on Kubernetes. The PKP community has early-stage discussion and community-contributed Helm charts:

Key requirements for a Kubernetes deployment:

  • Readiness and liveness probes pointing to /
  • Object storage for files (EFS/S3/MinIO โ€” not ReadWriteOnce PVC)
  • External managed database (RDS, Cloud SQL)
  • Horizontal Pod Autoscaler on the PHP deployment
  • ConfigMap or Secret for config.inc.php

Summaryโ€‹

TopicKey Recommendation
Server OSUbuntu 22.04 LTS or Debian 12
Web serverNginx + PHP-FPM (simpler to tune than Apache + mod_php)
PHP version8.2 (supported, well-tested with OJS 3.5)
DatabaseMySQL 8.0 or MariaDB 10.11 LTS (most community experience)
Backup scheduleDaily database + files, off-site copy via rclone/S3
File storageS3 / MinIO for production (supports horizontal scaling)
Cloud hosting2 vCPU / 4 GB RAM VPS minimum for a live journal
DockerUse pkpinc/ojs official image with named volumes

Referencesโ€‹

  1. PKP Admin Guide โ€” The authoritative OJS administration manual, covering installation, configuration, backup, and upgrades.
    https://docs.pkp.sfu.ca/admin-guide/en/

  2. OJS README / System Requirements โ€” Official PHP, database, and web-server requirements from the PKP team.
    https://github.com/pkp/ojs/blob/stable-3_5_0/README.md

  3. PKP Community Forum โ€” The main community venue for questions on server setup, performance, backups, Docker, and cloud deployments.
    https://forum.pkp.sfu.ca/

  4. PKP docker-ojs (GitHub) โ€” Official Docker image and Compose files for OJS.
    https://github.com/pkp/docker-ojs

  5. pkpinc/ojs on Docker Hub โ€” Pre-built OJS container images for each stable release.
    https://hub.docker.com/r/pkpinc/ojs

  6. PKP S3 Files Plugin (GitHub) โ€” Official plugin for using Amazon S3 (or S3-compatible stores) as the OJS file storage backend.
    https://github.com/pkp/s3files

  7. PKP Publishing Services โ€” Managed hosting option for institutions that prefer not to self-host.
    https://pkpservices.sfu.ca/

  8. MinIO Documentation โ€” Self-hosted S3-compatible object storage.
    https://min.io/docs/

  9. PKP Technical Documentation Hub โ€” Developer guides, plugin API, REST API reference.
    https://docs.pkp.sfu.ca/

  10. OJS GitHub Repository โ€” Source code, issue tracker, release notes.
    https://github.com/pkp/ojs