Deploying PostfixAdmin After a Mail Server Migration: Configuration and Troubleshooting

Configuration, Database Upgrades, PHP-FPM, Dovecot and SSL Troubleshooting

Migrating a production mail server is often easier when the migration is divided into layers. Postfix, Dovecot, the database, certificates and mail storage can be moved first, while administrative components such as PostfixAdmin are deployed later.

This approach works well because PostfixAdmin is primarily a web-based administration interface for managing virtual domains, mailboxes, aliases and related data stored in the mail server database. It can therefore be installed after the underlying mail infrastructure has already been migrated.

However, adding PostfixAdmin to an existing migrated environment can expose several configuration issues that were previously invisible. Typical examples include PHP-FPM permissions, database schema mismatches, password hashing compatibility and Dovecot being unable to access TLS certificates.

This article presents a practical approach to deploying PostfixAdmin on an existing Linux mail server and troubleshooting these issues without rebuilding the mail infrastructure.

Architecture

A typical installation may look like this:

Internet
   |
 Nginx
   |
 PHP-FPM
   |
 PostfixAdmin
   |
 MySQL / MariaDB
   |
   +----------------+
   |                |
Postfix          Dovecot
   |                |
SMTP          IMAP / POP3

PostfixAdmin does not replace Postfix or Dovecot. Instead, it provides an administrative interface to the database used by the mail environment.

PostfixAdmin supports MySQL/MariaDB, PostgreSQL and SQLite database backends and integrates with Postfix and IMAP/POP3 servers such as Dovecot.

This separation means that a server can already be successfully delivering and receiving mail before the PostfixAdmin web interface is installed.

1. Install the Web Stack

On an Enterprise Linux 9 compatible distribution, an Nginx/PHP installation can typically be prepared with packages such as:

dnf install -y \
    nginx \
    php \
    php-fpm \
    php-mbstring \
    php-mysqlnd \
    php-intl \
    php-xml \
    php-opcache \
    php-gd \
    php-ldap

Depending on the distribution repositories, php-imap might not be available. Its absence does not necessarily prevent the basic PostfixAdmin interface from operating.

Enable the services:

systemctl enable --now nginx
systemctl enable --now php-fpm

Verify them:

systemctl status nginx
systemctl status php-fpm

2. Deploy PostfixAdmin

Install PostfixAdmin under a dedicated directory such as:

/var/www/postfixadmin

The application should not normally be configured by modifying its main config.inc.php.

Instead, use:

/var/www/postfixadmin/config.local.php

PostfixAdmin’s own configuration documentation explicitly recommends overriding settings through config.local.php, which makes future upgrades considerably easier.

A minimal database configuration could resemble:

<?php

$CONF['configured'] = true;

$CONF['database_type'] = 'mysqli';
$CONF['database_host'] = 'localhost';
$CONF['database_user'] = 'postfix';
$CONF['database_password'] = 'REPLACE_WITH_PASSWORD';
$CONF['database_name'] = 'postfix';

$CONF['encrypt'] = 'dovecot:SHA512-CRYPT';

Never publish the actual database password in documentation, source control or public repositories.

PostfixAdmin supports mysqli for modern MySQL and MariaDB installations.

3. Configure Nginx

A simple Nginx virtual host can use PostfixAdmin’s public directory as its document root:

server {
    listen 80;
    server_name mailadmin.example.net;

    root /var/www/postfixadmin/public;
    index index.php;

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

    location ~ \.php$ {
        include fastcgi_params;

        fastcgi_pass unix:/run/php-fpm/www.sock;

        fastcgi_param SCRIPT_FILENAME
            $document_root$fastcgi_script_name;
    }
}

Validate the configuration:

nginx -t

Then reload Nginx:

systemctl reload nginx

In production, the administrative interface should normally be exposed over HTTPS rather than plain HTTP.

4. The templates_c Permission Problem

One common error during deployment is:

ERROR: the templates_c directory doesn't exist
or isn't writeable for the webserver

The important detail is that Nginx is not necessarily the process that needs write access.

Nginx forwards PHP requests to PHP-FPM, and PHP-FPM may execute them using a completely different Unix account.

Check the PHP-FPM worker identity:

grep -E '^user|^group' /etc/php-fpm.d/www.conf

For example:

user = apache
group = apache

In this situation, giving write permission only to the nginx user does not solve the problem.

Create the required directory:

mkdir -p /var/www/postfixadmin/templates_c

Then assign it to the PHP-FPM account:

chown -R apache:apache /var/www/postfixadmin/templates_c
chmod 755 /var/www/postfixadmin/templates_c

Test the permission using the same identity:

sudo -u apache touch \
    /var/www/postfixadmin/templates_c/write-test

If the command succeeds, Unix permissions are sufficient.

5. Account for SELinux

On RHEL-family systems, correct Unix permissions do not necessarily mean that PHP is allowed to write to a directory.

SELinux can independently deny access.

Check its state:

getenforce

If SELinux is enforcing, assign an appropriate writable web-content context:

semanage fcontext -a \
    -t httpd_sys_rw_content_t \
    "/var/www/postfixadmin/templates_c(/.*)?"

Apply it:

restorecon -Rv /var/www/postfixadmin/templates_c

Avoid solving SELinux problems by permanently disabling SELinux or using overly permissive directory modes such as 777.

6. Test Database Connectivity Separately

Another common failure is:

SQLSTATE[HY000] [1045]
Access denied for user

This indicates that PHP and PostfixAdmin have progressed far enough to attempt a database connection.

Test the same credentials independently:

mysql -u postfix -p -h localhost

Then select the database:

USE postfix;

If manual authentication fails, fix the MySQL account or password rather than troubleshooting PostfixAdmin.

You can inspect the relevant local configuration without displaying the password unnecessarily:

grep -E \
"database_type|database_host|database_user|database_name" \
/var/www/postfixadmin/config.local.php

7. Upgrade a Migrated PostfixAdmin Database

A database copied from an older server may contain an older PostfixAdmin schema.

The new application can therefore report an error similar to:

The PostfixAdmin database layout is outdated.
Please run setup.php to upgrade the database.

This is expected when the application version on the new server is newer than the version that originally managed the database.

Before upgrading, create a database backup:

mysqldump postfix > postfix-before-postfixadmin-upgrade.sql

Preferably compress it:

mysqldump postfix | gzip \
    > postfix-before-postfixadmin-upgrade.sql.gz

The PostfixAdmin setup process can then perform the required database migrations.

After successful configuration, make sure:

$CONF['configured'] = true;

PostfixAdmin explicitly requires this flag after the required local settings have been configured.

8. Migrated Administrator Passwords

A particularly interesting issue can occur with administrator accounts migrated from an older installation.

For example, an old admin table may contain hashes resembling:

$1$xxxxxxxx$xxxxxxxxxxxxxxxxxxxxxx

while the new installation is configured with:

$CONF['encrypt'] = 'dovecot:SHA512-CRYPT';

A SHA512-CRYPT password generated by Dovecot typically resembles:

{SHA512-CRYPT}$6$SALT$HASH

Dovecot documents SHA512-CRYPT as one of its supported password schemes, with the salt included in the resulting crypt string.

A new hash can be generated with:

doveadm pw -s SHA512-CRYPT

Dovecot then prompts for the new password and returns the encoded value.

The database record can subsequently be updated.

For example:

UPDATE admin
SET password = '{SHA512-CRYPT}$6$REDACTED'
WHERE username = 'administrator@example.net';

The administrator should also be active:

SELECT username, superadmin, active
FROM admin;

Never include real password hashes from production systems in public troubleshooting articles.

9. When a Correct Password Still Does Not Work

A password reset can appear completely correct and still result in failed authentication.

At this point, application logs become much more useful than repeatedly changing passwords.

For PHP-FPM:

tail -f /var/log/php-fpm/www-error.log

A particularly revealing error is:

Failed to read password from /usr/bin/doveadm pw

doveconf: Fatal:
ssl_cert: Can't open file ...
Permission denied

This changes the diagnosis completely.

The password itself is not necessarily wrong.

PostfixAdmin’s dovecot:* password method executes doveadm to generate or verify the password. PostfixAdmin’s current configuration documentation explicitly notes that Dovecot-based hashing depends on the doveadm binary and suitable permissions for Dovecot’s configuration.

Therefore the real dependency chain becomes:

PostfixAdmin
     |
     v
   PHP-FPM
     |
     v
 doveadm pw
     |
     v
Dovecot configuration
     |
     v
TLS certificate configuration

A failure at the bottom of this chain can manifest itself as a PostfixAdmin login failure.

10. The Let’s Encrypt Symlink Trap

Let’s Encrypt normally stores active certificates under:

/etc/letsencrypt/live/example.net/

but these are usually symbolic links.

For example:

fullchain.pem
    -> ../../archive/example.net/fullchain16.pem

privkey.pem
    -> ../../archive/example.net/privkey16.pem

The actual files are stored under:

/etc/letsencrypt/archive/example.net/

This matters enormously when diagnosing permission problems.

Inspect both:

ls -l /etc/letsencrypt/live/example.net

and:

ls -l /etc/letsencrypt/archive/example.net

Also inspect the complete path:

namei -l /etc/letsencrypt/live/example.net/fullchain.pem

namei is particularly useful because it shows every directory and symbolic-link component involved in reaching the real file.

11. Don’t Make the TLS Private Key Broadly Readable

A tempting response to a certificate permission problem is:

chmod 644 privkey.pem

Do not do this.

Private keys should remain tightly protected.

Dovecot’s documentation recommends restrictive permissions for the private key and notes that the normal Dovecot server reads its TLS certificate/key while it still has root privileges.

Consequently, a permission failure specifically from:

/usr/bin/doveadm pw

executed by a web-service account is a different situation from the normal Dovecot daemon reading its key at startup.

That distinction is important.

12. A Better Solution: Prevent doveadm pw From Reading Dovecot Configuration

For newer Dovecot versions, there is an especially useful solution.

Dovecot supports:

doveadm -O pw

The -O option tells doveadm not to read the Dovecot configuration files. Dovecot specifically documents this as useful when generating or verifying passwords without requiring the complete Dovecot configuration.

Current PostfixAdmin configuration documentation also calls this out:

$CONF['dovecotpw'] = "/usr/bin/doveadm -O pw";

and notes that this avoids a common configuration-permission problem on compatible Dovecot versions.

This can be significantly better than granting the PHP-FPM account access to Let’s Encrypt private keys merely so that it can calculate password hashes.

First determine the installed Dovecot version:

dovecot --version

Then test:

sudo -u apache /usr/bin/doveadm -O pw -s SHA512-CRYPT

If supported and successful, configure PostfixAdmin:

$CONF['encrypt'] = 'dovecot:SHA512-CRYPT';
$CONF['dovecotpw'] = '/usr/bin/doveadm -O pw';

Restart PHP-FPM:

systemctl restart php-fpm

This architecture is preferable because password hashing does not inherently require access to the mail server’s TLS private key.

13. Test Each Layer Independently

One of the most useful lessons from this type of migration is to avoid testing everything through the browser.

Test individual components instead.

PHP-FPM identity:

grep -E '^user|^group' /etc/php-fpm.d/www.conf

Database:

mysql -u postfix -p -h localhost postfix

Dovecot configuration:

doveconf -n

Password generation:

doveadm pw -s SHA512-CRYPT

Or, where supported:

doveadm -O pw -s SHA512-CRYPT

Test as the PHP-FPM user as well:

sudo -u apache \
    /usr/bin/doveadm -O pw -s SHA512-CRYPT

Nginx:

nginx -t

SELinux denials:

ausearch -m AVC -ts recent

Services:

systemctl status nginx
systemctl status php-fpm
systemctl status postfix
systemctl status dovecot

This layered approach makes troubleshooting much faster.

14. Security Considerations

An administrative interface for a mail system deserves additional protection.

Use HTTPS for PostfixAdmin and avoid exposing an administrative interface over unencrypted HTTP.

Restrict access where practical using a firewall, VPN, reverse proxy or trusted administrative network.

Keep config.local.php protected because it normally contains database credentials.

Do not give the PHP-FPM account unnecessary access to mail storage, Dovecot private keys or Let’s Encrypt private keys.

Use strong administrator passwords and modern password hashing schemes.

PostfixAdmin currently supports several hashing approaches, including modern options and Dovecot-backed password generation. Its own configuration guidance recommends using config.local.php for overrides rather than changing the distributed configuration file.

Always back up the mail database before allowing a newer PostfixAdmin release to upgrade its schema.

15. Troubleshooting Flow

A useful diagnostic sequence is:

Does Nginx respond?
        |
        v
Does PHP execute?
        |
        v
Can PHP-FPM write templates_c?
        |
        v
Can PostfixAdmin connect to MySQL?
        |
        v
Is the PostfixAdmin DB schema current?
        |
        v
Can the admin account be retrieved?
        |
        v
Can the configured password mechanism run?
        |
        v
Can doveadm execute as the PHP-FPM user?
        |
        v
Does doveadm unnecessarily load Dovecot TLS config?
        |
        v
Login succeeds

Following this sequence avoids changing unrelated parts of a working mail server.

Conclusion

Deploying PostfixAdmin after migrating a Postfix/Dovecot mail server is entirely practical because the web interface is largely independent of the initial mail-server migration.

The more difficult part is understanding the dependencies that become visible when the web application is introduced.

A templates_c error may actually be caused by the PHP-FPM worker identity rather than Nginx. A database error may indicate credentials or an outdated schema. A failed administrator login may not be a password problem at all: with Dovecot-backed hashing, PostfixAdmin may invoke doveadm, which in turn may attempt to parse the complete Dovecot configuration and encounter inaccessible TLS certificates.

The most important troubleshooting principle is therefore to work from the bottom up and test each component independently.

For compatible Dovecot releases, using:

$CONF['dovecotpw'] = '/usr/bin/doveadm -O pw';

is particularly worth considering because it allows password operations without loading unrelated Dovecot configuration, avoiding the dangerous alternative of granting a web-service account access to TLS private keys.

A successful migration is not simply one where the PostfixAdmin login page loads. It is one where the administrative interface, database, password hashing, Postfix, Dovecot, PHP-FPM, TLS configuration and operating-system security controls work together without unnecessarily weakening the security of the mail server.

This article is inspired by real-world challenges we tackle in our projects. If you're looking for expert solutions or need a team to bring your idea to life,

Let's talk!

    Please fill your details, and we will contact you back

      Please fill your details, and we will contact you back