こんにちは、UOZUです。
さくらのVPSには、サーバー作成時に任意の処理を自動実行できる「スタートアップスクリプト」という機能があります。これを利用することで、初期設定やミドルウェアの導入作業を効率化できます。

また、さくらのVPSでは公式スタートアップスクリプトとして「Mailserver」も提供されており、Postfix、Dovecot、PostfixAdmin などを利用したメールサーバー環境を構築できます。

一方で、2026年5月時点で筆者が確認した範囲では、公式の Mailserver スクリプトは AlmaLinux 8 または Rocky Linux 8 向けの構成となっていました。
そこで今回は、AlmaLinux 10 環境でメールサーバーを自動構築するための非公式スタートアップスクリプトを作成し、検証してみました。
※本記事は、さくらのVPS公式 Mailserver スタートアップスクリプトを利用する中で確認した設定上の注意点をもとに、筆者の検証環境向けに作成した非公式スクリプトの内容をまとめたものです。
掲載しているスクリプトは、さくらインターネット株式会社が提供する公式スクリプトではありません。利用する場合は、内容を確認したうえで自己責任で実行してください。
公式Mailserverスクリプトの内容の確認
公式の Mailserver スタートアップスクリプトは、さくらのVPSコントロールパネルにログイン後、以下の手順で確認できます。
スクリプト → さくらインターネット公式スクリプト → 公式スクリプト一覧をみる → Mailserver

スクリプト詳細画面では、実際に実行されるスクリプトの内容も確認できます。まずは公式スクリプトの構成を確認したうえで、AlmaLinux 10 環境で必要となる調整点を整理しました。
AlmaLinux10対応のスタートアップスクリプトの作成
今回作成したスクリプトでは、Apache、MariaDB、Postfix、Dovecot、PostfixAdmin、Let’s Encrypt を導入し、メールサーバーとして利用できる基本構成を自動でセットアップします。
また、筆者の検証環境では 512MB メモリプランで dnf 実行時にメモリ不足となるケースがあったため、スワップ領域を作成する処理も追加しています。あわせて、AlmaLinux 10 環境でのパッケージ構成や PostfixAdmin / Dovecot の認証方式に合わせて、一部設定を調整しています。
- スワップ領域を 4GB 作成
- PHP 8.5 系パッケージを利用
- AlmaLinux 10 環境に合わせて Postfix の設定を調整
- PostfixAdmin のパスワードハッシュ方式に php_crypt:MD5 を指定
- Dovecot 側の認証設定と PostfixAdmin 側のパスワード形式を一致させる
※長期的な検証は行っていないため、公式提供を目的としたものではなく、検証用の非公式スクリプトとして掲載しています。
AlmaLinux10対応のメールサーバ構築スタートアップスクリプト
#!/bin/bash
set -x
. /etc/os-release
D="@@@DOMAIN@@@";A="@@@ADMIN_NAME@@@";AP="@@@ADMIN_PASSWORD@@@";DP="@@@DATABASE_PASSWORD@@@";AE="$A@$D"
hostnamectl set-hostname "$D"
if [ ! -f /swapfile ];then dd if=/dev/zero of=/swapfile bs=1M count=4096;chmod 600 /swapfile;mkswap /swapfile;fi
grep -qE '^[^#][[:space:]]*/swapfile[[:space:]]' /etc/fstab||echo '/swapfile none swap sw 0 0'>>/etc/fstab
grep -q '^/swapfile[[:space:]]' /proc/swaps||swapon /swapfile
r(){ for i in 1 2 3 4 5;do dnf clean all;eval "$*"&&return;sleep 3;done;echo "Command failed five times. So exit.";exit 1;}
r 'dnf -y update'
r 'dnf -y install dovecot dovecot-mysql postfix postfix-mysql mariadb-server httpd mod_ssl'
dnf -y install "https://rpms.remirepo.net/enterprise/remi-release-${VERSION_ID%.*}.rpm"
r 'dnf -y install epel-release'
r 'dnf -y install php85-php php85-php-mbstring php85-php-imap php85-php-mysql php85-php-pecl-zip snapd git'
ln -sf php85 /usr/bin/php
systemctl enable --now firewalld.service
firewall-cmd --permanent --add-service=ssh
firewall-cmd --permanent --add-port={80,443}/tcp
firewall-cmd --permanent --add-port={25,110,143,465,587,993,995}/tcp
firewall-cmd --reload
systemctl enable --now snapd.socket||exit 1
ln -sf /var/lib/snapd/snap /snap
for i in 1 2 3 4 5;do sleep 5;snap install core&&break;[ "$i" = 5 ]&&exit 1;done
snap refresh core;snap install certbot --classic||exit 1
ln -sf /snap/bin/certbot /usr/bin/certbot||exit 1
certbot -n certonly --standalone --agree-tos -d "$D" -m "$AE" --server https://acme-v02.api.letsencrypt.org/directory||exit 1
LD=/etc/letsencrypt/live/$D;CERT=$LD/fullchain.pem;PKEY=$LD/privkey.pem;CHAIN=$LD/chain.pem;PRE=/etc/letsencrypt/renewal-hooks/pre/stop.sh;POST=/etc/letsencrypt/renewal-hooks/post/reload.sh
[ -f "$CERT" ]||{ echo "証明書の取得に失敗しました";exit 1;}
snap start --enable certbot.renew||exit 1
sed -i -e "/^server/a pre_hook = $PRE" -e "/^pre_hook/a post_hook = $POST" /etc/letsencrypt/renewal/$D.conf
echo -e '#!/bin/bash\nsystemctl stop httpd'>"$PRE";chmod +x "$PRE"
echo -e '#!/bin/bash\nsystemctl reload postfix dovecot\nsystemctl start httpd'>"$POST";chmod +x "$POST"
cd /srv/;git clone https://github.com/postfixadmin/postfixadmin.git;cd postfixadmin;git checkout $(git tag -l|sort -V|grep -iv 'push\|rc\|beta'|tail -1)
export HOME=/root COMPOSER_HOME=/root/.composer;mkdir -p /root/.composer;/bin/bash install.sh
systemctl enable --now mariadb.service
mysqladmin -u root password "$DP"
cat >/root/.my.cnf <<EOF2
[client]
host=localhost
user=root
password=$DP
socket=/var/lib/mysql/mysql.sock
EOF2
chmod 600 /root/.my.cnf
mysql --defaults-file=/root/.my.cnf <<EOF2
DELETE FROM mysql.user WHERE User='';
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost','127.0.0.1','::1');
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db='test' OR Db='test\_%';
CREATE DATABASE IF NOT EXISTS postfix;
CREATE USER 'postfix'@'localhost' IDENTIFIED BY '$DP';
GRANT ALL PRIVILEGES ON postfix.* TO 'postfix'@'localhost';
FLUSH PRIVILEGES;
EOF2
SP=$(php -r "echo password_hash(\"$AP\",PASSWORD_DEFAULT);")
cat >/srv/postfixadmin/config.local.php <<EOF2
<?php
\$CONF['configured']=true;
\$CONF['default_language']='ja';
\$CONF['database_type']='mysql';
\$CONF['database_user']='postfix';
\$CONF['database_password']='$DP';
\$CONF['database_name']='postfix';
\$CONF['setup_password']='$SP';
\$CONF['domain_path']='YES';
\$CONF['domain_in_mailbox']='NO';
\$CONF['encrypt']='php_crypt:MD5';
\$CONF['footer_link']='https://$D/postfixadmin/';
\$CONF['footer_text']='Return to $D/postfixadmin/';
?>
EOF2
mkdir -p /srv/postfixadmin/templates_c;chown -R apache /srv/postfixadmin/templates_c
cat >/etc/httpd/conf.d/postfixadmin.conf <<EOF2
Alias /postfixadmin "/srv/postfixadmin/public"
<Directory "/srv/postfixadmin/public">
DirectoryIndex index.html index.php
AllowOverride All
Options FollowSymlinks
Require all granted
</Directory>
EOF2
sed -ie "s|^[#\s]*SSLCertificateFile.*$|SSLCertificateFile $CERT|;s|^[#\s]*SSLCertificateKeyFile.*$|SSLCertificateKeyFile $PKEY|;s|^[#\s]*SSLCertificateChainFile.*$|SSLCertificateChainFile $CHAIN|;s|^[#\s]*SSLProtocol.*$|SSLProtocol All -SSLv2 -SSLv3 -TLSv1 -TLSv1.1|" /etc/httpd/conf.d/ssl.conf
sed -i 's/Options Indexes FollowSymLinks/Options FollowSymLinks/' /etc/httpd/conf/httpd.conf
sed -i 's/Options -Indexes/#Options -Indexes/;s|ErrorDocument 403 /.noindex.html|#ErrorDocument 403 /.noindex.html|' /etc/httpd/conf.d/welcome.conf
echo 'ServerTokens ProductOnly'>>/etc/httpd/conf/httpd.conf;echo 'ServerSignature Off'>>/etc/httpd/conf/httpd.conf
cat >/etc/httpd/conf.d/rewrite.conf <<'EOF2'
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R,L]
</IfModule>
EOF2
systemctl enable --now httpd.service
curl -s -o /dev/null -X POST -d "setup_password=$AP" -d "submit=setuppw" -L -k "https://$D/postfixadmin/setup.php"
PA=/srv/postfixadmin/scripts/postfixadmin-cli
$PA admin add "$AE" --superadmin 1 --active 1 --password "$AP" --password2 "$AP"
$PA domain add "$D";$PA domain update "$D" --mailboxes 0;$PA mailbox add "$AE" --password "$AP" --password2 "$AP"
$PA alias add root@$D --goto "$AE"
for a in abuse hostmaster postmaster webmaster;do $PA alias update $a@$D --goto "$AE";done
postconf -e smtpd_banner='$myhostname ESMTP' smtp_header_checks=regexp:/etc/postfix/smtp_header_checks mime_header_checks=regexp:/etc/postfix/mime_header_checks disable_vrfy_command=yes smtpd_helo_required=yes inet_interfaces=all myhostname=$D mydestination='localhost.$mydomain, localhost' relay_domains='$mydestination' virtual_alias_maps=proxy:mysql:/etc/postfix/virtual_alias_maps.cf virtual_mailbox_domains=proxy:mysql:/etc/postfix/virtual_mailbox_domains.cf virtual_mailbox_maps=proxy:mysql:/etc/postfix/virtual_mailbox_maps.cf virtual_mailbox_base=/home/vmail virtual_mailbox_limit=512000000 message_size_limit=20480000 virtual_minimum_uid=10000 virtual_transport=virtual virtual_uid_maps=static:10000 virtual_gid_maps=static:10000 local_transport=virtual local_recipient_maps='$virtual_mailbox_maps' smtpd_sasl_auth_enable=yes smtpd_sasl_type=dovecot smtpd_sasl_path=/var/run/dovecot/auth-client smtpd_recipient_restrictions='permit_auth_destination, permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination' smtpd_client_restrictions='permit_mynetworks, reject_unknown_client, permit' smtpd_sender_restrictions='reject_unknown_sender_domain, reject_non_fqdn_sender' smtpd_relay_restrictions='permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination' smtpd_sasl_security_options=noanonymous smtpd_sasl_tls_security_options='$smtpd_sasl_security_options' smtpd_tls_security_level=may smtpd_tls_auth_only=yes smtpd_tls_received_header=yes smtpd_tls_cert_file=$CERT smtpd_tls_key_file=$PKEY smtpd_tls_CAfile=/etc/pki/tls/certs/ca-bundle.crt smtpd_tls_mandatory_protocols='!SSLv2,!SSLv3,!TLSv1,!TLSv1.1' smtpd_tls_protocols='!SSLv2,!SSLv3,!TLSv1,!TLSv1.1' smtpd_tls_ask_ccert=yes smtpd_tls_mandatory_ciphers=high smtpd_use_tls=yes smtpd_sasl_local_domain='$mydomain' broken_sasl_auth_clients=yes smtpd_tls_loglevel=1 smtp_tls_security_level=may smtp_tls_loglevel=1 smtp_tls_mandatory_protocols='!SSLv2,!SSLv3,!TLSv1,!TLSv1.1' smtp_tls_protocols='!SSLv2,!SSLv3,!TLSv1,!TLSv1.1'
echo '# smtpd_client_restrictions=permit_mynetworks, reject_rbl_client bl.spamcop.net, reject_rbl_client zen.spamhaus.org, permit'>>/etc/postfix/main.cf
sed -i 's/^#\(submission.*smtpd$\)/\1/g;s/^#\(smtps.*smtpd$\)/\1 \n -o smtpd_tls_wrappermode=yes\n -o smtpd_sasl_auth_enable=yes/g' /etc/postfix/master.cf
cat >/etc/postfix/smtp_header_checks <<'EOF2'
/^Received: .*/ IGNORE
/^User-Agent: .*/ IGNORE
EOF2
cat >/etc/postfix/mime_header_checks <<'EOF2'
/^Mime-Version:/ IGNORE
EOF2
for f in virtual_alias_maps:alias:goto:address virtual_mailbox_domains:domain:domain:domain virtual_mailbox_maps:mailbox:maildir:username;do IFS=: read n t s w<<<$f;cat >/etc/postfix/$n.cf <<EOF2
user = postfix
password = $DP
hosts = localhost
dbname = postfix
table = $t
select_field = $s
where_field = $w
EOF2
done
systemctl enable --now postfix.service;systemctl restart postfix.service
groupadd -g 10000 vmail;useradd -u 10000 -g vmail -s /usr/bin/nologin -d /home/vmail -m vmail
mkdir -p /home/vmail/$D/$A/{cur,new,tmp};chown -R vmail. /home/vmail/
sed -i 's/.*!include conf.d\/\*.conf/#&/g' /etc/dovecot/dovecot.conf
cat >>/etc/dovecot/dovecot.conf <<EOF2
protocols = imap pop3
auth_mechanisms = plain
passdb {
driver = sql
args = /etc/dovecot/dovecot-sql.conf
}
userdb {
driver = sql
args = /etc/dovecot/dovecot-sql.conf
}
service auth {
unix_listener auth-client {
group = postfix
mode = 0660
user = postfix
}
user = root
}
mail_home = /home/vmail/%d/%n
mail_location = maildir:~
ssl = yes
ssl_cert = <$CERT
ssl_key = <$PKEY
ssl_min_protocol = TLSv1.2
service stats {
unix_listener stats-writer {
mode = 0666
}
}
EOF2
cat >/etc/dovecot/dovecot-sql.conf <<EOF2
driver = mysql
connect = host=localhost dbname=postfix user=postfix password=$DP
default_pass_scheme = MD5-CRYPT
user_query = SELECT '/home/vmail/%d/%n' as home, 'maildir:/home/vmail/%d/%n' as mail, 10000 AS uid, 10000 AS gid, concat('dirsize:storage=', quota) AS quota FROM mailbox WHERE username = '%u' AND active = '1'
password_query = SELECT username as user, password, '/home/vmail/%d/%n' as userdb_home, 'maildir:/home/vmail/%d/%n' as userdb_mail, 10000 as userdb_uid, 10000 as userdb_gid FROM mailbox WHERE username = '%u' AND active = '1'
EOF2
systemctl enable --now dovecot.service
reboot
上記スクリプトの使いかた
スタートアップスクリプトの作成
作成したスクリプトは、さくらのVPSコントロールパネルから登録します。
対象アカウントのVPS管理画面にログインし、「スクリプト」メニューから「新規追加」→「スクリプト」をクリックします。

スクリプト名には任意の名前を設定し、利用可能OSには「AlmaLinux 10 x86_64」を選択します。

スクリプト本文には、作成したスクリプトをコピー&ペーストします。

また、「パラメーター(任意)」には、以下の4項目を登録します。
- @@@DOMAIN@@@
- @@@ADMIN_NAME@@@
- @@@ADMIN_PASSWORD@@@
- @@@DATABASE_PASSWORD@@@

最後に「保存」ボタンをクリックすれば、スタートアップスクリプトの登録は完了です。
自作スタートアップスクリプトを利用してOS再インストールを行う
保存が出来たら、スクリプト→マイスクリプトを選ぶと、保存したスタートアップスクリプトが表示される様になります。

表示されたスタートアップスクリプトをクリックすると、「スクリプトを利用」ボタンが表示されるので、クリックします。

その後、サーバの選択画面になるので、対象サーバーを選択します。

そのままパラメータを指定し、そのままOS再インストールを進めます。

OS再インストール後の動作確認
OS再インストール直後は、スタートアップスクリプトが実行中のため、すぐには利用できません。以下のログを確認しながら、処理が完了するまで待ちます
[root@uozu ~]# tail -f /root/.sakuravps/startup.log
Running scriptlet: libgcc-14.2.1-7.el10.alma.1.x86_64 441/441
Running scriptlet: grub2-common-1:2.12-29.el10_1.2.alma.1.noarch 441/441
Running scriptlet: iproute-6.14.0-2.el10.x86_64 441/441
Running scriptlet: ca-certificates-2025.2.80_v9.0.305-102.el10_1.no 441/441
Running scriptlet: sssd-common-2.11.1-2.el10_1.1.x86_64 441/441
Running scriptlet: rpm-4.19.1.1-20.el10.alma.1.x86_64 441/441
Running scriptlet: selinux-policy-targeted-42.1.7-1.el10_1.2.noarch 441/441
Running scriptlet: filesystem-3.18-17.el10.x86_64 441/441
Running scriptlet: kernel-modules-core-6.12.0-124.55.1.el10_1.x86_6 441/441
Running scriptlet: kernel-core-6.12.0-124.55.1.el10_1.x86_64 441/441
Broadcast message from root@localhost (Fri 2026-05-08 20:58:25 JST):
The system will reboot now!
Shared connection to xxx.xxx.xxx.xxx closed.
[root@uozu ~]# tail /root/.sakuravps/startup.log
chown: warning: '.' should be ':': ‘vmail.’
+ sed -i 's/.*!include conf.d\/\*.conf/#&/g' /etc/dovecot/dovecot.conf
+ cat
+ cat
+ systemctl enable --now dovecot.service
Created symlink '/etc/systemd/system/multi-user.target.wants/dovecot.service' → '/usr/lib/systemd/system/dovecot.service'.
+ reboot
======================================================================
STARTUP SCRIPTS END
======================================================================
筆者の検証環境では、10分程度でスタートアップスクリプトの実行が完了し、サーバーの再起動まで行われました。
再起動後、以下のURLにアクセスすると PostfixAdmin の管理画面を確認できます。
https://ドメイン名/postfixadmin/

その後、PostfixAdmin の管理画面からメールアカウントを追加することで、メールアドレスを利用できるようになります。
さいごに
今回は、AlmaLinux 10 環境で Postfix、Dovecot、PostfixAdmin を利用したメールサーバーを自動構築するための非公式スタートアップスクリプトを作成し、さくらのVPS上で検証しました。
さくらのVPSのスタートアップスクリプトには文字数制限があるため、スクリプト内の変数名を短くするなど、可読性よりも実行可能なサイズに収めることを優先しています。
そのため、より細かくユーザー設定やデータベース構成を作り込みたい場合は、スタートアップスクリプトだけで完結させるよりも、Ansible などの構成管理ツールを利用した方が管理しやすいと思います。
本記事の内容は検証時点の情報に基づくものです。実際に利用する場合は、各ミドルウェアのバージョンや設定内容を確認したうえで実行してください。
最後までお読みいただき、ありがとうございました。


