Rocky Linux 9 is the community-maintained binary-compatible RHEL 9 rebuild — the natural choice when your operations policy requires a Red Hat-family OS but you don't want to pay for a RHEL subscription. CentOS Stream and AlmaLinux work the same way; everything in this guide applies to all three with no command changes.
The hard differences from the Ubuntu and Debian guides: package manager is dnf (not apt), default firewall is firewalld (not ufw), SELinux is enforcing by default and will block the AcelleMail web user from writing logs/storage unless you set the right context, and PHP comes from the Remi repo (not Sury or Ondrej). Everything else — the workload sizing, the nginx vhost, the certbot flow, the supervisor config — is the same.
Step 0 — Pre-flight#
Same as the Ubuntu pre-flight. Spec a 2 vCPU / 4 GB / 50 GB Rocky 9 droplet, point a DNS A record at it, create a sudo user, and have your acellemail-latest.zip + CodeCanyon code ready.
Step 1 — Base packages + EPEL#
sudo dnf -y update
sudo dnf -y install epel-release
sudo dnf -y install curl wget unzip ca-certificates dnf-utils \
policycoreutils-python-utils tar
policycoreutils-python-utils brings semanage, which we'll need to grant SELinux permissions. Without it, you can't add file contexts.
Step 2 — PHP 8.3 via Remi#
The Remi repo is the canonical PHP source on RHEL-family distributions:
sudo dnf -y install https://rpms.remirepo.net/enterprise/remi-release-9.rpm
sudo dnf -y module reset php
sudo dnf -y module install php:remi-8.3
sudo dnf -y install php-fpm php-cli php-mysqlnd php-mbstring php-xml \
php-curl php-zip php-gd php-intl php-imap php-gmp php-mailparse \
php-bcmath php-redis php-soap
The module reset undoes RHEL's default PHP 8.0 module stream; the next line opts into Remi's 8.3 stream. Apply the AcelleMail-specific php.ini settings:
sudo sed -i 's/^memory_limit = .*/memory_limit = 512M/' /etc/php.ini
sudo sed -i 's/^upload_max_filesize = .*/upload_max_filesize = 300M/' /etc/php.ini
sudo sed -i 's/^post_max_size = .*/post_max_size = 300M/' /etc/php.ini
sudo sed -i 's/^max_execution_time = .*/max_execution_time = 300/' /etc/php.ini
Note: RHEL has only one php.ini (vs. Ubuntu's separate fpm + cli). Set the FPM pool to run as nginx:
sudo sed -i 's/^user = apache/user = nginx/' /etc/php-fpm.d/www.conf
sudo sed -i 's/^group = apache/group = nginx/' /etc/php-fpm.d/www.conf
sudo sed -i 's|^listen.owner = apache|listen.owner = nginx|' /etc/php-fpm.d/www.conf
sudo sed -i 's|^listen.group = apache|listen.group = nginx|' /etc/php-fpm.d/www.conf
sudo systemctl enable --now php-fpm
Step 3 — MariaDB 10.11#
curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version=10.11
sudo dnf -y install MariaDB-server MariaDB-client
sudo systemctl enable --now mariadb
sudo mysql_secure_installation
sudo mysql <<SQL
CREATE DATABASE acellemail
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'acellemail'@'localhost' IDENTIFIED BY '$(openssl rand -base64 24)';
GRANT ALL PRIVILEGES ON acellemail.* TO 'acellemail'@'localhost';
FLUSH PRIVILEGES;
SQL
Step 4 — Redis 7#
sudo dnf module reset redis
sudo dnf module install redis:remi-7.2 -y
sudo systemctl enable --now redis
Step 5 — Nginx + firewalld#
sudo dnf -y install nginx
sudo systemctl enable --now nginx
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
Vhost at /etc/nginx/conf.d/acellemail.conf (RHEL uses conf.d/, not sites-enabled/):
server {
listen 80;
server_name mail.example.com;
root /var/www/acellemail/public;
index index.php index.html;
client_max_body_size 300M;
location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ \.php$ {
fastcgi_pass unix:/run/php-fpm/www.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_read_timeout 300;
}
location ~ /\.(?!well-known).* { deny all; }
}
sudo nginx -t && sudo systemctl reload nginx
Step 6 — Drop in the bundle (with SELinux)#
This is where Rocky differs meaningfully from Ubuntu. Default SELinux lets nginx read files in /var/www, but not write them — and AcelleMail needs the web user to write to storage/ and bootstrap/cache/.
sudo mkdir -p /var/www/acellemail
cd /var/www/acellemail
unzip -q /path/to/acellemail-latest.zip -d .
sudo chown -R nginx:nginx /var/www/acellemail
sudo chmod -R 0755 /var/www/acellemail
# Mark storage + bootstrap/cache writable from web user under SELinux
sudo semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/acellemail/storage(/.*)?'
sudo semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/acellemail/bootstrap/cache(/.*)?'
sudo restorecon -R /var/www/acellemail/storage /var/www/acellemail/bootstrap/cache
# Allow nginx/php-fpm outbound network calls (sending API, license refresh)
sudo setsebool -P httpd_can_network_connect 1
sudo setsebool -P httpd_can_network_connect_db 1
Without httpd_can_network_connect=1, AcelleMail's Amazon SES driver and license verification calls will silently fail with permission errors that don't show up in any application log — only in /var/log/audit/audit.log. This is the single most-common Rocky/CentOS install gotcha.
Step 7 — TLS#
sudo dnf -y install certbot python3-certbot-nginx
sudo certbot --nginx -d mail.example.com --non-interactive --agree-tos \
--email you@example.com --redirect
sudo systemctl enable --now certbot-renew.timer
Step 8 — Supervisor#
sudo dnf -y install supervisor
sudo tee /etc/supervisord.d/acellemail-worker.ini <<CONF
[program:acellemail-worker]
process_name=%(program_name)s_%(process_num)02d
command=/usr/bin/php /var/www/acellemail/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=nginx
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/acellemail-worker.log
CONF
sudo systemctl enable --now supervisord
sudo supervisorctl reread && sudo supervisorctl update
sudo supervisorctl start acellemail-worker:*
Step 9 — Cron#
sudo crontab -u nginx -e
* * * * * cd /var/www/acellemail && php artisan schedule:run >> /dev/null 2>&1
Step 10 — Web installer#
Browse to https://mail.example.com/install. Same flow as Ubuntu.
SELinux troubleshooting#
If something doesn't work after install, the audit log is your friend:
sudo ausearch -m avc -ts recent | head -30
sudo ausearch -m avc -ts recent | audit2allow -a
Most-common AVCs on a fresh AcelleMail install:
| Denial |
Fix |
nginx … denied { write } for path=/var/www/acellemail/storage/... |
restorecon -R after semanage fcontext (Step 6) |
php-fpm … denied { name_connect } for port=443 |
setsebool -P httpd_can_network_connect 1 (Step 6) |
php-fpm … denied { name_connect } for port=3306 (remote DB) |
setsebool -P httpd_can_network_connect_db 1 |
php-fpm … denied { write } for path=/tmp/... |
chcon -t httpd_tmp_t /var/www/acellemail/storage/framework/cache |
Related reading#
FAQ#
Can I disable SELinux instead of configuring it?#
You can (setenforce 0 and SELINUX=permissive in /etc/selinux/config), but you shouldn't. SELinux materially reduces blast radius if AcelleMail or one of its dependencies has a remote-code vulnerability. The configuration above is the minimum confinement that lets AcelleMail work.
What about AlmaLinux / CentOS Stream / RHEL?#
This guide works on all four with no command changes. Same package manager, same SELinux behavior, same firewalld.
Should I use the Software Collections (SCL) PHP?#
No. Remi's modular streams are the actively-maintained option. SCL is on the path to deprecation in RHEL 10.
Why MariaDB and not MySQL?#
Both work; MariaDB ships in EPEL and Remi, MySQL needs Oracle's repo. AcelleMail's MySQL adapter speaks both fine.
Migrating Rocky 8 → Rocky 9 with AcelleMail in place#
Rocky 8 reaches end-of-life May 2029 (later than 9, paradoxically, due to the long RHEL 8 maintenance window) but new installs should start on 9 directly. If you have existing AcelleMail on Rocky 8 and want to move to 9, the safe path is:
- Snapshot the source droplet.
- Provision a fresh Rocky 9 droplet, run this install guide.
- Stop AcelleMail on Rocky 8 (
supervisorctl stop acellemail-worker:* + crontab -r for the www-data user).
mysqldump the database from Rocky 8, restore to the new Rocky 9.
- Rsync
/var/www/acellemail/storage/ to the new droplet (preserves user-uploaded campaign assets).
- Update DNS to the new droplet's IP.
- Run
php artisan migrate --force on the new droplet to apply any AcelleMail migrations that came with newer point releases.
In-place upgrade (Rocky's dnf system-upgrade or LEAPP for RHEL) is technically possible but risky for a multi-component stack like AcelleMail — if the upgrade leaves PHP modules in a half-upgraded state, debugging is hostile. The fresh-droplet approach is 30 minutes more work and dramatically safer.
Automated patching with dnf-automatic#
Rocky 9 ships dnf-automatic for unattended security updates:
sudo dnf -y install dnf-automatic
sudo sed -i 's/^apply_updates = .*/apply_updates = yes/' /etc/dnf/automatic.conf
sudo sed -i 's/^upgrade_type = .*/upgrade_type = security/' /etc/dnf/automatic.conf
sudo systemctl enable --now dnf-automatic.timer
Set upgrade_type = security (not default) so you only auto-apply security errata — not feature updates that could change PHP/MySQL behavior unexpectedly. Pair this with the daily DB backup from the hardening checklist.