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:
| Component | Minimum | Recommended |
|---|---|---|
| PHP | 8.1 | 8.2 or 8.3 |
| MySQL | 8.0 | 8.0+ |
| MariaDB | 10.6 | 10.11 LTS+ |
| PostgreSQL | 14 | 15 or 16 |
| Apache | 2.4 | 2.4 (with mod_rewrite) |
| Nginx | 1.20 | latest stable |
| RAM | 1 GB | 4 GB+ (busy journals) |
| CPU | 1 vCPU | 2+ vCPUs |
| Disk | 10 GB | 50 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:
| Extension | Purpose |
|---|---|
imagick | Better PDF thumbnail generation |
opcache | PHP bytecode caching (strongly recommended for performance) |
apcu | In-memory key-value cache |
redis | If 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 set | utf8mb4 + utf8mb4_unicode_ci | UTF-8 by default |
| JSON support | Native JSON columns (OJS uses some) | JSONB (superior) |
| Full-text search | MyISAM/InnoDB FTS | tsvector / GIN indexes |
| Replication | Binary log replication | Streaming / logical |
| Cloud-managed | RDS, Cloud SQL, Azure DB | RDS PG, Cloud SQL PG, Azure DB PG, Supabase |
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_sizebeing 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.
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:
| Component | Default Location | Contains |
|---|---|---|
| Database | MySQL / PostgreSQL | All journal metadata, users, submissions, settings |
| Files directory | files_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.php | Database credentials, encryption keys |
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:
rcloneto 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
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โ
| Provider | Price (per GB/month) | Notes |
|---|---|---|
| AWS S3 | ~$0.023 | Highest ecosystem, egress costs |
| Backblaze B2 | $0.006 | Free egress with Cloudflare |
| Wasabi | $0.0068 | No egress fees, S3-compatible |
| MinIO (self-hosted) | Your disk cost | Full 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:
- Configure the S3 plugin in OJS settings
- Use a migration script to copy files to the bucket (preserving the same relative path structure OJS uses)
- Verify a sample of articles render correctly
- 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:
| Provider | Entry VPS | Notes |
|---|---|---|
| DigitalOcean | $6/mo (1 vCPU / 1 GB) | Simple UI, popular in PKP forum |
| Linode / Akamai | $5/mo | Competitive pricing, good docs |
| Vultr | $6/mo | Many 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/mo | Excellent 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).
5.2 Recommended Stack on Ubuntu 22.04 LTSโ
# 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
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:
- Shared filesystem or object storage for
files_dirโ local disk is per-server; use NFS, GlusterFS, or S3 - Centralised session storage โ configure Redis or Memcached as the session handler in
config.inc.php - Centralised cache โ set
cache_driver = redisinconfig.inc.php - 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:
- Docker Hub: https://hub.docker.com/r/pkpinc/ojs
- GitHub: https://github.com/pkp/docker-ojs
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:
| Volume | Path in container | Contains |
|---|---|---|
ojs_files | /var/www/files | All uploaded article files |
ojs_public | /var/www/html/public | Public assets (logos, covers) |
./config.inc.php (bind mount) | /var/www/html/config.inc.php | OJS 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:
- pkp/docker-ojs โ official Docker image, starting point for K8s manifests
- Forum discussion on Kubernetes OJS โ community experiences
Key requirements for a Kubernetes deployment:
- Readiness and liveness probes pointing to
/ - Object storage for files (EFS/S3/MinIO โ not
ReadWriteOncePVC) - External managed database (RDS, Cloud SQL)
- Horizontal Pod Autoscaler on the PHP deployment
- ConfigMap or Secret for
config.inc.php
Summaryโ
| Topic | Key Recommendation |
|---|---|
| Server OS | Ubuntu 22.04 LTS or Debian 12 |
| Web server | Nginx + PHP-FPM (simpler to tune than Apache + mod_php) |
| PHP version | 8.2 (supported, well-tested with OJS 3.5) |
| Database | MySQL 8.0 or MariaDB 10.11 LTS (most community experience) |
| Backup schedule | Daily database + files, off-site copy via rclone/S3 |
| File storage | S3 / MinIO for production (supports horizontal scaling) |
| Cloud hosting | 2 vCPU / 4 GB RAM VPS minimum for a live journal |
| Docker | Use pkpinc/ojs official image with named volumes |
Referencesโ
-
PKP Admin Guide โ The authoritative OJS administration manual, covering installation, configuration, backup, and upgrades.
https://docs.pkp.sfu.ca/admin-guide/en/ -
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 -
PKP Community Forum โ The main community venue for questions on server setup, performance, backups, Docker, and cloud deployments.
https://forum.pkp.sfu.ca/ -
PKP docker-ojs (GitHub) โ Official Docker image and Compose files for OJS.
https://github.com/pkp/docker-ojs -
pkpinc/ojs on Docker Hub โ Pre-built OJS container images for each stable release.
https://hub.docker.com/r/pkpinc/ojs -
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 -
PKP Publishing Services โ Managed hosting option for institutions that prefer not to self-host.
https://pkpservices.sfu.ca/ -
MinIO Documentation โ Self-hosted S3-compatible object storage.
https://min.io/docs/ -
PKP Technical Documentation Hub โ Developer guides, plugin API, REST API reference.
https://docs.pkp.sfu.ca/ -
OJS GitHub Repository โ Source code, issue tracker, release notes.
https://github.com/pkp/ojs