PostgreSQL Installation and Configuration: Complete Guide for Rocky Linux 9.6

PostgreSQL Installation and Configuration: A Production-Ready Setup Guide⌗
Setting up PostgreSQL correctly from the beginning can save you countless hours of troubleshooting later. Whether you’re a developer setting up your first database or a DBA preparing a production environment, this comprehensive guide covers everything you need to know about PostgreSQL installation, directory structure, permissions, and performance optimization.
Pre-Installation Planning⌗
Before diving into the installation, let’s plan our setup properly. A well-planned PostgreSQL installation considers data location, backup strategies, and future scaling needs.
Optimizing for Rocky Linux 9.6 with Limited Resources⌗
Our target environment:
- Operating System: Rocky Linux 9.6 on Oracle VM
- RAM: 4 GiB (requires careful memory tuning)
- Storage: 40 GiB with XFS file system on
/var - Administrative User: dbadmin (root disabled for security)
Storage layout optimization:
# Leveraging the existing disk partitioning
/var/lib/postgresql/ # Main data directory (20 GiB /var mount)
/var/log/postgresql/ # Log files (same /var mount)
/tmp/backup/postgresql/ # Temporary backup location (2 GiB /tmp)
/home/dbadmin/scripts/ # Administrative scripts
Why this setup works well:
- XFS file system provides excellent performance for database workloads
- 20 GiB
/varmount gives plenty of space for data growth - Separate
/tmpmount prevents backup operations from filling root partition
Package Manager Installation⌗
# Rocky Linux 9.6
# As postgres user with sudo privileges
# Install PostgreSQL repository for RHEL 9 compatible
sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm
# Install PostgreSQL 17
sudo dnf install -y postgresql17-{server,contrib}
# Initialize database with custom data directory in /var
sudo /usr/pgsql-17/bin/postgresql-17-setup initdb
# Initializing database ... OK
tail /var/lib/pgsql/17/initdb.log
# selecting default time zone ... Europe/Istanbul
# creating configuration files ... ok
# running bootstrap script ... ok
# performing post-bootstrap initialization ... ok
# syncing data to disk ... ok
#
# Success. You can now start the database server using:
#
# /usr/pgsql-17/bin/pg_ctl -D /var/lib/pgsql/17/data/ -l logfile start
Directory Structure and Permissions⌗
Understanding PostgreSQL’s directory structure is crucial for proper administration.
Standard Directory Layout⌗
/var/lib/pgsql/17/data/
├── base/ # Database files
├── global/ # Cluster-wide tables
├── pg_wal/ # Write-Ahead Log files
├── pg_xact/ # Transaction status data
├── pg_log/ # Server log files
├── postgresql.conf # Main configuration file
├── pg_hba.conf # Host-based authentication
├── pg_ident.conf # User name mapping
└── postmaster.pid # Process ID file
Initial Configuration for Performance⌗
Understanding postgresql.conf Tuning Strategy⌗
PostgreSQL’s performance heavily depends on proper configuration tuning. Our tuning strategy for your Rocky Linux 9.6 environment considers:
- Hardware Constraints: 4 GiB RAM, XFS file system, VM environment
- Disk Layout: 20 GiB
/varpartition for database files - Workload Assumptions: Mixed OLTP workload with moderate concurrency
- Resource Allocation: Conservative approach to prevent memory exhaustion
Complete postgresql.conf Configuration⌗
Here’s a production-ready postgresql.conf optimized for your specific environment:
# =============================================================================
# POSTGRESQL.CONF - OPTIMIZED FOR ROCKY LINUX 9.6 (4GB RAM, 20GB /var)
# =============================================================================
sudo mkdir /var/log/postgresql/ /var/lib/pgsql/17/data/postgresql.conf.d
chown postgres:dbadmin /var/log/postgresql
tee /var/lib/pgsql/17/data/postgresql.conf.d/00_connections_and_authentication.conf 1>/dev/null <<BODY
# -----------------------------------------------------------------------------
# CONNECTIONS AND AUTHENTICATION
# -----------------------------------------------------------------------------
listen_addresses = '*' # Security: only local connections initially
port = 5432 # Standard PostgreSQL port
max_connections = 50 # Conservative: each connection uses ~4MB RAM
superuser_reserved_connections = 3 # Reserve connections for superuser
BODY
tee /var/lib/pgsql/17/data/postgresql.conf.d/01_memory.conf 1>/dev/null <<BODY
# -----------------------------------------------------------------------------
# MEMORY SETTINGS (Critical for 4GB RAM)
# -----------------------------------------------------------------------------
shared_buffers = 1GB # 25% of RAM - conservative for VM
huge_pages = try # Use huge pages if available
temp_buffers = 32MB # Temporary tables buffer
work_mem = 16MB # Per-operation memory (50 conn * 16MB = 800MB max)
hash_mem_multiplier = 1.0 # Default hash memory factor
maintenance_work_mem = 256MB # For VACUUM, CREATE INDEX operations
autovacuum_work_mem = 128MB # Separate memory for autovacuum
max_stack_depth = 2MB # Stack depth limit
dynamic_shared_memory_type = posix # Shared memory implementation
# Memory usage calculation check:
# shared_buffers (1GB) + work_mem_total (800MB) + maintenance (256MB) + OS (1GB) = ~3GB
# This leaves 1GB buffer for OS and other processes
BODY
tee /var/lib/pgsql/17/data/postgresql.conf.d/03_disk_and_io.conf 1>/dev/null <<BODY
# -----------------------------------------------------------------------------
# DISK AND I/O SETTINGS (Optimized for XFS on /var)
# -----------------------------------------------------------------------------
# XFS is excellent for PostgreSQL, these settings optimize for it
wal_buffers = 32MB # Conservative WAL buffer for 4GB RAM
effective_cache_size = 2GB # Available memory for disk caching
effective_io_concurrency = 100 # VM environment - moderate concurrency
random_page_cost = 1.1 # XFS on modern storage
seq_page_cost = 1.0 # Sequential scan cost baseline
min_parallel_table_scan_size = 64MB # Parallel scan threshold
min_parallel_index_scan_size = 32MB # Parallel index scan threshold
BODY
tee /var/lib/pgsql/17/data/postgresql.conf.d/04_writed_ahead_logging.conf 1>/dev/null <<BODY
# -----------------------------------------------------------------------------
# WRITE-AHEAD LOGGING (WAL) - Optimized for 20GB /var partition
# -----------------------------------------------------------------------------
wal_level = replica # Enable replication and backup
fsync = on # Ensure data durability
synchronous_commit = on # Wait for WAL write completion
wal_sync_method = fdatasync # Fastest sync method on Linux
full_page_writes = on # Protection against torn pages
wal_compression = on # Compress WAL files to save space
wal_log_hints = off # Save space if no replication planned
# Checkpoint settings - balanced for limited disk space
checkpoint_timeout = 15min # Maximum time between checkpoints
checkpoint_completion_target = 0.9 # Spread I/O over 90% of interval
max_wal_size = 1GB # Maximum WAL size before checkpoint
min_wal_size = 256MB # Minimum WAL files to keep
checkpoint_warning = 30s # Warn if checkpoints happen too frequently
BODY
tee /var/lib/pgsql/17/data/postgresql.conf.d/05_query_planner.conf 1>/dev/null <<BODY
# -----------------------------------------------------------------------------
# QUERY PLANNER SETTINGS
# -----------------------------------------------------------------------------
default_statistics_target = 100 # Statistics detail level
constraint_exclusion = partition # Enable for partitioned tables
cursor_tuple_fraction = 0.1 # Optimize for cursor fetches
from_collapse_limit = 8 # FROM item collapse limit
join_collapse_limit = 8 # JOIN collapse limit
BODY
tee /var/lib/pgsql/17/data/postgresql.conf.d/06_error_reporting_and_logging.conf 1>/dev/null <<BODY
# -----------------------------------------------------------------------------
# ERROR REPORTING AND LOGGING (Space-conscious)
# -----------------------------------------------------------------------------
log_destination = 'jsonlog' # Send logs to stderr
logging_collector = on # Enable log file collector
log_directory = '/var/log/postgresql/' # Log directory in /var partition
log_filename = 'postgresql-%a.log' # Weekly rotation (7 files max)
log_file_mode = 0640 # Log file permissions
log_rotation_age = 1d # Rotate logs daily
log_rotation_size = 100MB # Rotate when file reaches 100MB
log_truncate_on_rotation = on # Overwrite old log files
# What to log (performance-focused, space-conscious)
log_min_duration_statement = 2000 # Log queries taking 2+ seconds
log_checkpoints = on # Log checkpoint activity
log_connections = off # Disabled to save space
log_disconnections = off # Disabled to save space
log_lock_waits = on # Important for debugging deadlocks
log_temp_files = 10MB # Log temp files larger than 10MB
log_autovacuum_min_duration = 1000 # Log slow autovacuum operations
# Log format for analysis
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
log_statement = 'none' # Don't log all statements (space concern)
log_timezone = 'UTC' # Use UTC for consistency
BODY
tee /var/lib/pgsql/17/data/postgresql.conf.d/07_autovacumm.conf 1>/dev/null <<BODY
# -----------------------------------------------------------------------------
# AUTOVACUUM SETTINGS (Critical for maintenance)
# -----------------------------------------------------------------------------
autovacuum = on # Enable automatic vacuuming
autovacuum_max_workers = 2 # Conservative for 4GB RAM
autovacuum_naptime = 1min # Check for work every minute
autovacuum_vacuum_threshold = 50 # Minimum updates before vacuum
autovacuum_vacuum_scale_factor = 0.1 # Fraction of table size for vacuum trigger
autovacuum_vacuum_insert_threshold = 1000 # Inserts before vacuum
autovacuum_analyze_threshold = 50 # Minimum updates before analyze
autovacuum_analyze_scale_factor = 0.05 # Fraction for analyze trigger
BODY
tee /var/lib/pgsql/17/data/postgresql.conf.d/08_client_connection.conf 1>/dev/null <<BODY
# -----------------------------------------------------------------------------
# CLIENT CONNECTION DEFAULTS
# -----------------------------------------------------------------------------
default_text_search_config = 'pg_catalog.english' # Default search config
timezone = 'UTC' # Use UTC for consistency
lc_messages = 'C.UTF-8' # Message locale
lc_monetary = 'C.UTF-8' # Monetary locale
lc_numeric = 'C.UTF-8' # Numeric locale
lc_time = 'C.UTF-8' # Time locale
default_transaction_isolation = 'read committed' # Default isolation level
BODY
tee /var/lib/pgsql/17/data/postgresql.conf.d/09_resource_usage.conf 1>/dev/null <<BODY
# -----------------------------------------------------------------------------
# RESOURCE USAGE (Additional settings for VM environment)
# -----------------------------------------------------------------------------
max_files_per_process = 1000 # Conservative file limit
max_locks_per_transaction = 64 # Default lock allocation
max_wal_senders = 3 # For backup and replication
max_replication_slots = 3 # Replication slot limit
BODY
tee /var/lib/pgsql/17/data/postgresql.conf.d/10_performance_monitoring.conf 1>/dev/null <<BODY
# -----------------------------------------------------------------------------
# PERFORMANCE MONITORING
# -----------------------------------------------------------------------------
track_activities = on # Enable activity tracking
track_counts = on # Enable statistics collection
track_io_timing = on # Track I/O timing (useful for analysis)
track_functions = all # Track function call statistics
track_wal_io_timing = on # Track WAL I/O timing
# Extensions for monitoring (add these to shared_preload_libraries)
shared_preload_libraries = 'pg_stat_statements'
# pg_stat_statements configuration
pg_stat_statements.max = 1000 # Maximum statements tracked
pg_stat_statements.track = all # Track all statements
pg_stat_statements.track_utility = on # Track utility commands
pg_stat_statements.save = on # Save statistics across restarts
BODY
tee --append /var/lib/pgsql/17/data/postgresql.conf 1>/dev/null <<BODY
include_dir = '/var/lib/pgsql/17/data/postgresql.conf.d'
BODY
# Enable and start the service
sudo systemctl enable --now postgresql-17.service
# Verify installation
systemctl status postgresql-17.service
Setting Correct Permissions⌗
Critical security setup for Rocky Linux with dbadmin user:
# PostgreSQL user already created by package installation
# Ensure correct ownership (dbadmin can manage through sudo)
sudo chown -R postgres:dbadmin /var/lib/pgsql/17/
sudo chmod 700 /var/lib/pgsql/15/data/
# Grant dbadmin user necessary sudo permissions
sudo usermod -aG wheel dbadmin
echo 'dbadmin ALL=(postgres) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/dbadmin-postgres
# Secure configuration files
sudo chmod 640 /var/lib/pgsql/17/data/postgresql.conf
sudo chmod 640 /var/lib/pgsql/17/data/pg_hba.conf
# Create directories for scripts and backups
sudo mkdir -p /home/dbadmin/scripts
sudo mkdir -p /tmp/backup/postgresql
sudo chown postgres:dbadmin /home/dbadmin/scripts
sudo chown postgres:dbadmin /tmp/backup/postgresql
Configuration Tuning Methodology⌗
Step 1: Calculate Memory Allocation
# Our 4GB RAM breakdown:
# - OS and processes: ~1GB
# - PostgreSQL shared_buffers: 1GB
# - PostgreSQL work_mem pool: ~800MB (50 connections × 16MB)
# - PostgreSQL maintenance: 256MB
# - Safety buffer: ~200MB
Step 2: Monitor and Adjust
-- Check current memory usage
SELECT
setting AS shared_buffers_mb,
unit
FROM pg_settings
WHERE name = 'shared_buffers';
-- Monitor connection memory usage
SELECT
count(*) as active_connections,
sum(
CASE
WHEN state = 'active' THEN 1
ELSE 0
END
) as active_queries
FROM pg_stat_activity;
Step 3: Validate Settings
# Check if PostgreSQL can start with new settings
sudo -u postgres /usr/pgsql-17/bin/postgres --config-file=/var/lib/pgsql/17/data/postgresql.conf --check
# Monitor system resources after restart
watch -n 2 'free -m && echo "---" && df -h /var'
Write-Ahead Logging (WAL) Optimization for Limited Disk⌗
# WAL settings optimized for 20GB /var partition
wal_buffers = 32MB # Conservative for 4GB RAM
checkpoint_completion_target = 0.9
max_wal_size = 1GB # Smaller for limited space
min_wal_size = 256MB # Reduced for space conservation
# Archive settings using /tmp for temporary backup storage
wal_level = replica
archive_mode = on
archive_command = 'test ! -f /tmp/postgresql_backup/wal/%f && cp %p /tmp/postgresql_backup/wal/%f'
# Create WAL archive directory
# Run as dbadmin: sudo mkdir -p /tmp/postgresql_backup/wal && sudo chown postgres:postgres /tmp/postgresql_backup/wal
Query Optimization Settings⌗
# Query planner settings optimized for XFS and limited resources
random_page_cost = 1.1 # For SSD/fast storage
effective_io_concurrency = 100 # Conservative for VM environment
seq_page_cost = 1.0
# Logging for performance analysis (be careful with disk space)
log_min_duration_statement = 2000 # Log queries taking 2+ seconds
log_checkpoints = on
log_connections = off # Disabled to save disk space
log_disconnections = off # Disabled to save disk space
log_lock_waits = on # Important for debugging
Security Hardening⌗
Authentication Configuration⌗
pg_hba.conf setup:
# TYPE DATABASE USER ADDRESS METHOD
# Local connections
local all postgres peer
local all all md5
# Remote connections (be specific about IP ranges)
host myapp_db myapp_user 192.168.1.0/24 md5
host all all 127.0.0.1/32 md5
# Reject all other connections
host all all 0.0.0.0/0 reject
SSL Configuration⌗
# In postgresql.conf
ssl = on
ssl_cert_file = 'server.crt'
ssl_key_file = 'server.key'
ssl_ca_file = 'root.crt'
ssl_prefer_server_ciphers = on
Post-Installation Steps⌗
1. Create Application Database and User⌗
-- Connect as superuser (dbadmin can use sudo)
sudo -u postgres psql
-- Create database with locale compatible with Rocky Linux 9.6
CREATE DATABASE myapp_db
WITH ENCODING='UTF8'
LC_COLLATE='C.UTF-8'
LC_CTYPE='C.UTF-8'
TEMPLATE=template0;
-- Create user with limited privileges
CREATE USER myapp_user WITH PASSWORD 'secure_password_here';
GRANT CONNECT ON DATABASE myapp_db TO myapp_user;
GRANT USAGE ON SCHEMA public TO myapp_user;
GRANT CREATE ON SCHEMA public TO myapp_user;
2. Set Up Backup Strategy for Limited Disk Space⌗
# Create backup script for dbadmin user
cat > /home/dbadmin/scripts/pg_backup.sh << 'EOF'
#!/bin/bash
BACKUP_DIR="/tmp/postgresql_backup"
DATE=$(date +%Y%m%d_%H%M%S)
MAX_BACKUPS=3 # Keep only 3 backups due to space limitations
# Ensure backup directory exists
mkdir -p "$BACKUP_DIR"
# Full database backup with compression to save space
sudo -u postgres pg_dump -h localhost myapp_db | gzip > "$BACKUP_DIR/myapp_db_$DATE.sql.gz"
# Remove old backups, keep only the most recent ones
ls -t "$BACKUP_DIR"/myapp_db_*.sql.gz | tail -n +$((MAX_BACKUPS + 1)) | xargs -r rm
# Log backup completion
echo "$(date): Backup completed - $BACKUP_DIR/myapp_db_$DATE.sql.gz" >> /var/log/postgresql/backup.log
EOF
chmod +x /home/dbadmin/scripts/pg_backup.sh
# Test the backup script
/home/dbadmin/scripts/pg_backup.sh
# Add to dbadmin's crontab for daily backups at 2 AM
(crontab -u dbadmin -l 2>/dev/null; echo "0 2 * * * /home/dbadmin/scripts/pg_backup.sh") | crontab -u dbadmin -
3. Monitoring Setup⌗
-- Enable pg_stat_statements for query analysis
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Check configuration
SELECT name, setting FROM pg_settings WHERE name LIKE 'shared_preload%';
Performance Testing on Limited Resources⌗
Verify your installation with scaled performance tests suitable for 4GB RAM:
# Initialize pgbench with smaller scale for limited resources
sudo -u postgres pgbench -i -s 10 myapp_db # Reduced from 100 to 10
# Run performance test with reduced connections
sudo -u postgres pgbench -c 5 -j 2 -t 500 myapp_db # Conservative settings
# Monitor system resources during testing
# In another terminal as dbadmin:
watch -n 1 'free -m && echo "---" && df -h /var'
Common Pitfalls to Avoid in Resource-Constrained Environments⌗
- Never exceed 25% of RAM for shared_buffers on 4GB systems
- Monitor swap usage - PostgreSQL performance degrades severely with swapping
- Don’t set max_connections too high - each connection uses ~2-4MB RAM
- Always compress backups to save disk space in /tmp
- Use
VACUUMregularly to prevent table bloat in limited disk space - Monitor /var disk usage - 20GB can fill up quickly with logs and data
Rocky Linux 9.6 Specific Considerations⌗
# Configure firewall for PostgreSQL (if enabled)
sudo firewall-cmd --permanent --add-port=5432/tcp
sudo firewall-cmd --reload
# SELinux considerations (if enforcing)
sudo setsebool -P postgresql_can_rsync on
# Check system limits for postgres user
sudo -u postgres ulimit -a
Maintenance Tasks for Resource-Constrained Systems⌗
Create a maintenance checklist optimized for your 4GB/20GB setup:
# Daily tasks (automated via cron)
# Monitor disk space
df -h /var >> /var/log/postgresql/disk_usage.log
# Weekly tasks (run as dbadmin)
sudo -u postgres psql -d myapp_db -c "VACUUM ANALYZE;"
# Monthly tasks
# Backup verification and cleanup
/home/dbadmin/scripts/pg_backup.sh
# Log file rotation (automatically handled by Rocky Linux logrotate)
# Quarterly tasks
# Extension updates and minor version upgrades
sudo dnf update postgresql15*
Conclusion⌗
A properly configured PostgreSQL installation is the foundation of any robust database system. By following this guide, you’ve set up a secure, performant, and maintainable PostgreSQL environment optimized for Rocky Linux 9.6 with limited resources. Remember to monitor your system regularly and adjust configurations based on your specific workload patterns.
Next Steps: Now that you have a solid PostgreSQL installation, you can focus on advanced topics like index optimization, query tuning, and scaling strategies to maximize your database performance.
Related Articles⌗
- PostgreSQL Index Optimization Techniques
- Database Performance Monitoring on Linux
- PostgreSQL Backup and Recovery Strategies
Frequently Asked Questions⌗
Q: Can I increase shared_buffers beyond 1GB on a 4GB system? A: It’s not recommended. Leave sufficient memory for the OS and other processes to avoid swapping, which severely impacts PostgreSQL performance.
Q: How often should I run VACUUM on my database? A: For active databases, run VACUUM ANALYZE weekly. For high-transaction systems, consider enabling autovacuum with custom settings.
Q: What’s the minimum disk space required for PostgreSQL on Rocky Linux 9.6?
A: The installation requires about 500MB, but plan for at least 10GB for data, logs, and growth. Our 20GB /var partition provides comfortable headroom.
This installation guide provides a production-ready PostgreSQL setup optimized for Rocky Linux 9.6 in resource-constrained environments. Always test configuration changes in a development environment before applying them to production systems.
Keywords: PostgreSQL installation, Rocky Linux database setup, PostgreSQL configuration, database performance tuning, PostgreSQL security, Linux database administration