Ubuntu 22.04 安装 AnonAddy 1.7.3 指南

在 Ubuntu 22.04 真实服务器从零安装 AnonAddy(addy.io)v1.7.3:PHP 8.3 PPA + Nginx + MariaDB + Redis + Postfix 管道集成、队列与调度、邮件收发端到端实测与六条排障实录。

AnonAddyAnonAddy邮件别名Ubuntu 22.04PHP 应用2026/10/25

Ubuntu 22.04 安装 AnonAddy 1.7.3 指南

目标:在一台 Ubuntu 22.04 真实服务器上从零安装并跑通 AnonAddy(addy.io)v1.7.3:PHP 8.3 + Nginx + MariaDB + Redis + Postfix 完整依赖链、Web 控制台、邮件收发管道端到端验证。已在 Ubuntu 22.04 服务器实测通过。

一、环境信息

  • 操作系统:Ubuntu 22.04 LTS(Jammy),x86_64,systemd 托管
  • 应用版本:AnonAddy v1.7.3(2026-10-06 发布,GitHub Releases 实查)
  • 依赖组件实测版本:PHP 8.3.36(ondrej/php PPA)、Nginx 1.18.0、MariaDB 10.6.23、Redis 6.0.16、Postfix 3.6.4、Composer 2.10.3、Node.js 22.23.4
  • 主机名约定:mail.example.com;Web 控制台:app.example.com;应用目录 /var/www/anonaddy
  • 演示管理员:admin / q1w2e3r4(默认收件人 admin@qq.com)
  • 网络说明:GitHub/getcomposer/NodeSource 均可直连下载;Composer 包经 Packagist 官方源安装成功

二、前置条件

  • 一台干净的 Ubuntu 22.04 服务器(建议 ≥2C2G),hostname 已设为 FQDN:
hostnamectl set-hostname mail.example.com
hostname -f

实测回显:

mail.example.com
  • /etc/hosts 增加本机解析(公网部署时改用真实 DNS A 记录):
echo "127.0.0.1 mail.example.com app.example.com" >> /etc/hosts
  • 安全组/防火墙放行:25(SMTP)、80/443(Web)。注意:Vultr、UpCloud 等厂商默认封 25 端口,需开工单解封(官方 SELF-HOSTING 明示)。

三、版本选型说明

版本发布日期说明
v1.7.32026-10-06当前最新稳定版,本文采用
v1.7.22026-09-02前序补丁版
v1.6.x2026-07旧分支

关键约束来自 composer.json:"php": "^8.3" 且依赖 laravel/framework: ^13.0。Ubuntu 22.04 默认仓库只有 PHP 8.1,不满足要求,必须引入 ondrej/php PPA 安装 PHP 8.3——这是 Ubuntu 22.04 装 AnonAddy 与装普通 PHP 应用最大的差异点。

四、方案 A:官方源码在线安装(主方案)

4.1 安装基础环境

安装工具与 PPA 源:

apt-get update -qq
apt-get install -y software-properties-common ca-certificates curl gnupg lsb-release apt-transport-https
add-apt-repository -y ppa:ondrej/php
apt-get update -qq

实测回显(节选):

Adding deb entry to /etc/apt/sources.list.d/ondrej-ubuntu-php-jammy.list
Adding key to /etc/apt/trusted.gpg.d/ondrej-ubuntu-php.gpg with fingerprint B8DC7E53946656EFBCE4C1DD71DAEAAB4AD4CAB6

一次性安装全部服务组件(Postfix 以非交互方式选 “Internet Site”):

export DEBIAN_FRONTEND=noninteractive
echo "postfix postfix/mailname string mail.example.com" | debconf-set-selections
echo "postfix postfix/main_mailer_type string Internet Site" | debconf-set-selections
apt-get install -y nginx mariadb-server redis-server postfix \
  php8.3-fpm php8.3-cli php8.3-mysql php8.3-mbstring php8.3-xml php8.3-bcmath \
  php8.3-curl php8.3-gd php8.3-intl php8.3-zip php8.3-gmp php8.3-redis php8.3-opcache \
  php8.3-mailparse supervisor unzip git cron rsyslog

⚠️ 排障实录:php8.3-mailparse 是 AnonAddy 依赖 php-mime-mail-parser 的硬性扩展,漏装会在 composer install 时报 ext-mailparse platform 错误,务必一次装齐。

安装 Node.js 22(前端构建需要,Ubuntu 仓库自带版本太旧):

curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/nodesource_setup.sh
bash /tmp/nodesource_setup.sh
apt-get install -y nodejs
node -v

实测回显:

v22.23.4

安装 Composer:

curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php
php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer
composer --version

实测回显:

Composer version 2.10.3 2026-08-27 13:34:23

版本核验(逐条实测):

php -v | head -1      # PHP 8.3.36 (cli) (built: Oct 21 2026 14:31:50) (NTS)
nginx -v              # nginx version: nginx/1.18.0 (Ubuntu)
mariadb --version     # mariadb  Ver 15.1 Distrib 10.6.23-MariaDB ...
redis-server --version
postconf mail_version

实测回显:

Redis server v=6.0.16 sha=00000000:0 malloc=jemalloc-5.2.1 bits=64 build=d6ed09173b889769
mail_version = 3.6.4

4.2 下载并解压 AnonAddy v1.7.3

cd /var/www
curl -sSL -o anonaddy.tar.gz https://github.com/anonaddy/anonaddy/archive/refs/tags/v1.7.3.tar.gz
tar -xzf anonaddy.tar.gz
mv anonaddy-1.7.3 anonaddy
ls /var/www/anonaddy | head

实测回显:

AGENTS.md
LICENSE.md
README.md
SECURITY.md
SELF-HOSTING.md
app
artisan
boost.json
bootstrap
composer.json

4.3 安装 PHP 依赖与构建前端

cd /var/www/anonaddy
composer install --no-dev --optimize-autoloader --no-interaction
npm ci --no-audit --no-fund

⚠️ 排障实录:vite.config.mjs 会读取 .env 里的 APP_URL,必须先建好 .env 再构建前端,否则报 Cannot read properties of undefined (reading 'replace')。正确顺序是先做 4.4 再回来执行:

npm run production

实测回显(节选):

> production
> vite build

public/build/assets/app-YpEXFbPO.js           1,499.52 kB │ gzip: 440.71 kB
✓ built in 1.05s

4.4 配置 .env

cp .env.example .env

关键修改项(完整逐项说明见本专栏配置参数详解篇):

APP_NAME=AnonAddy
APP_URL=http://app.example.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=addy_database
DB_USERNAME=addy
DB_PASSWORD=q1w2e3r4

MAIL_FROM_NAME=AnonAddy
MAIL_FROM_ADDRESS=mailer@example.com
MAIL_HOST=mail.example.com
MAIL_PORT=25
MAIL_EHLO_DOMAIN=mail.example.com
MAIL_VERIFY_PEER=false

ANONADDY_ADMIN_USERNAME=admin
ANONADDY_DOMAIN=example.com
ANONADDY_HOSTNAME=mail.example.com
ANONADDY_ALL_DOMAINS=example.com
ANONADDY_DNS_RESOLVER=127.0.0.1
ANONADDY_SECRET=<64 位随机十六进制串>
ANONADDY_GNUPGHOME=/var/www/anonaddy/.gnupg
SESSION_SECURE_COOKIE=false

ANONADDY_SECRET 用 php -r "echo bin2hex(random_bytes(32)).PHP_EOL;" 生成。

⚠️ 说明:MAIL_VERIFY_PEER=false、SESSION_SECURE_COOKIE=false 仅用于无受信证书的实验环境;生产环境使用 Let's Encrypt 证书后应恢复 true 并启用 HTTPS(APP_URL 改 https)。

4.5 数据库初始化与迁移

systemctl start mariadb
mariadb -e "CREATE DATABASE addy_database CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER \"addy\"@\"localhost\" IDENTIFIED BY \"q1w2e3r4\"; GRANT ALL PRIVILEGES ON addy_database.* TO \"addy\"@\"localhost\"; FLUSH PRIVILEGES;"
systemctl start redis-server
cd /var/www/anonaddy
php artisan key:generate
php artisan migrate --force

实测回显(迁移节选):

   INFO  Application key set successfully.  

  2026_06_04_105152_add_description_to_recipients_table .......... 5.77ms DONE
  2026_09_25_142227_add_theme_to_users_table .................... 10.13ms DONE
  2026_10_04_132200_add_show_intentional_failed_deliveries_to_users_table  6.40ms DONE

授权应用属主(Postfix 管道与 PHP-FPM 都以 www-data 运行):

chown -R www-data:www-data /var/www/anonaddy
php artisan storage:link

4.6 Nginx + PHP-FPM 站点

新建 /etc/nginx/sites-available/anonaddy:

server {
    listen 80;
    server_name app.example.com;
    root /var/www/anonaddy/public;
    index index.php;
    client_max_body_size 25M;

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

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

启用并重启:

ln -sf /etc/nginx/sites-available/anonaddy /etc/nginx/sites-enabled/anonaddy
rm -f /etc/nginx/sites-enabled/default
nginx -t
systemctl restart php8.3-fpm
systemctl restart nginx
systemctl is-active nginx
systemctl is-active php8.3-fpm

实测回显:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
active
active

生产缓存与首验:

php artisan config:cache
php artisan route:cache
php artisan view:cache
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: app.example.com" http://127.0.0.1/login
curl -s -H "Host: app.example.com" http://app.example.com/login | grep -oE "<title>[^<]*</title>"

实测回显:

200
<title>AnonAddy</title>

4.7 Postfix 邮件管道集成

这是 AnonAddy 与普通 PHP 应用最大的不同:Postfix 负责收信并把邮件管道给应用。安装 MySQL 查询支持并生成自签证书(生产换 Let's Encrypt):

apt-get install -y postfix-mysql acl
mkdir -p /etc/nginx/ssl
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
  -keyout /etc/nginx/ssl/mail.example.com.key \
  -out /etc/nginx/ssl/mail.example.com.crt -subj "/CN=mail.example.com"

写 /etc/postfix/main.cf(核心项如下,完整文件见官方 SELF-HOSTING.md):

smtpd_banner = $myhostname ESMTP
biff = no
append_dot_mydomain = no
compatibility_level = 3.6

smtpd_tls_cert_file=/etc/nginx/ssl/mail.example.com.crt
smtpd_tls_key_file=/etc/nginx/ssl/mail.example.com.key
smtpd_use_tls=yes
smtpd_tls_security_level = may
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1
smtp_use_tls=yes
smtp_tls_security_level = may
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1

myhostname = mail.example.com
mydomain = example.com
myorigin = /etc/mailname
mydestination = localhost.$mydomain, localhost

virtual_transport = anonaddy:
virtual_mailbox_domains = $mydomain, unsubscribe.$mydomain, mysql:/etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf

mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
recipient_delimiter = +
inet_interfaces = all
local_recipient_maps =
smtpd_helo_required = yes
smtpd_recipient_restrictions =
   permit_mynetworks,
   reject_unauth_destination,
   check_policy_service unix:private/policy
disable_vrfy_command = yes

新建 /etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf(判断收件域是否归本服务):

user = addy
password = q1w2e3r4
hosts = 127.0.0.1
dbname = addy_database
query = SELECT (SELECT 1 FROM usernames WHERE '%s' IN (CONCAT(username, '.example.com'))) AS usernames, (SELECT 1 FROM domains WHERE domain = '%s' AND domain_verified_at IS NOT NULL) AS domains LIMIT 1;
chmod o= /etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf
chgrp postfix /etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf

⚠️ 排障实录:上述 query 行中的单引号必须原样保留。曾因 shell 引号转义丢单引号,Postfix 报 Unknown column 'example.com' in 'WHERE'、451 4.3.0 Temporary lookup failure 拒收所有外发投递;用 postmap -q "admin.example.com" mysql:/etc/postfix/...cf 可独立验证,实测修复后回显 1。

在 /etc/postfix/master.cf 末尾追加管道与访问策略服务:

# Pipe to addy.io application
anonaddy unix - n n - - pipe
  flags=F user=www-data argv=php /var/www/anonaddy/artisan anonaddy:receive-email --sender=${sender} --recipient=${recipient} --local_part=${user} --extension=${extension} --domain=${domain} --size=${size}

# addy.io access policy
policy  unix  -       n       n       -       0       spawn
  user=www-data argv=php /var/www/anonaddy/postfix/AccessPolicy.php
echo example.com > /etc/mailname
postfix check
systemctl restart postfix
systemctl is-active postfix
ss -ltn | grep :25

实测回显:

active
LISTEN 0      100          0.0.0.0:25        0.0.0.0:*
LISTEN 0      100             [::]:25           [::]:*

4.8 Supervisor 队列与 cron 调度

转发邮件走 Redis 队列异步发送,必须有常驻 worker。新建 /etc/supervisor/conf.d/anonaddy-worker.conf:

[program:anonaddy-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/anonaddy/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/anonaddy-worker.log
stopwaitsecs=3600

Laravel 计划任务(清理失败投递、重置带宽、解析 Postfix 日志等)靠 cron 驱动:

echo "* * * * * www-data cd /var/www/anonaddy && php artisan schedule:run >> /dev/null 2>&1" > /etc/cron.d/anonaddy
systemctl restart supervisor
systemctl start cron
supervisorctl status
systemctl is-active cron

实测回显:

anonaddy-worker:anonaddy-worker_00   RUNNING   pid 21023, uptime 0:00:03
anonaddy-worker:anonaddy-worker_01   RUNNING   pid 21024, uptime 0:00:03
active

查看应用注册的全部计划任务:

runuser -u www-data -- php artisan schedule:list

实测回显(节选):

  0   0   1 * *  php artisan anonaddy:reset-bandwidth  Next Due: 6 days from now
  0   0   * * *  php artisan anonaddy:check-domains-sending-verification  Next Due: 21 hours from now
  0   0   * * *  php artisan anonaddy:check-domains-mx-validation  Next Due: 21 hours from now
  0   0   * * *  php artisan anonaddy:clear-failed-deliveries  Next Due: 21 hours from now
  */5 *   * * *  php artisan anonaddy:parse-postfix-mail-log  Next Due: 44 seconds from now

4.9 端到端验证

第一步:创建管理员。收件邮箱必须通过 DNS 校验(Laravel email:rfc,dns 规则,域名需有 MX/A 记录),且不能用本服务的本地域:

cd /var/www/anonaddy
php artisan anonaddy:create-user admin admin@qq.com

实测回显:

Created user: "admin" with user_id: "7f113ba9-cb3b-459e-8d24-12910a2895db"
This user can now reset their password (the default password is their user_id)

初始密码即 user_id,登录后请立即修改;也可直接在数据库层重置为演示口令:

php artisan tinker
$u = App\Models\Username::where("username","admin")->first()->user;
$u->password = bcrypt("q1w2e3r4"); $u->save();
$r = $u->defaultRecipient; $r->email_verified_at = now(); $r->save();

⚠️ 排障实录:若用 admin@example.org 之类无 MX 的保留域,命令报 The email must be a valid email address.——该校验做真实 DNS 查询,实验环境需保证 DNS 可用(本文曾因上游解析对个别域返回 SERVFAIL 而失败,更换收件域后通过)。

第二步:创建别名(Web 控制台操作,或 tinker):

$u = App\Models\Username::where("username","admin")->first()->user;
$a = App\Models\Alias::create([
  "id" => (string) Ramsey\Uuid\Uuid::uuid4(),
  "user_id" => $u->id, "local_part" => "test1", "extension" => null,
  "domain" => "example.com", "email" => "test1@example.com", "active" => true,
  "emails_forwarded" => 0, "emails_blocked" => 0, "emails_replied" => 0, "emails_sent" => 0
]);
$a->recipients()->attach($u->defaultRecipient->id);

实测回显:alias created: test1@example.com

第三步:发信触发完整链路:

printf "From: sender@gmail.com\r\nTo: test1@example.com\r\nSubject: AnonAddy forwarding test 2\r\nMessage-ID: <test002@gmail.com>\r\n\r\nHello again.\r\n" > /tmp/test2.eml
sendmail -t < /tmp/test2.eml
sleep 4
tail -12 /var/log/mail.log

实测回显(核心链路逐行照抄):

Oct 25 02:23:08 mail postfix/smtpd[20983]: Anonymous TLS connection established from localhost[127.0.0.1]: TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) ...
Oct 25 02:23:08 mail postfix/qmgr[20960]: CD9B6FE9667: from=<b_ny4g64lypezdi53dgnra_g4ytim3dgrrtc@example.com>, size=1894, nrcpt=1 (queue active)
Oct 25 02:23:08 mail postfix/smtpd[20983]: disconnect from localhost[127.0.0.1] ehlo=2 starttls=1 mail=1 rcpt=1 data=1 quit=1 commands=7
Oct 25 02:23:09 mail postfix/smtp[20985]: Trusted TLS connection established to mx3.qq.com[157.255.4.243]:25: TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)
Oct 25 02:23:09 mail postfix/smtp[20985]: CD9B6FE9667: to=<admin@qq.com>, relay=mx3.qq.com[157.255.4.243]:25, delay=0.69, ..., dsn=5.0.0, status=bounced (host mx3.qq.com[157.255.4.243] said: 550 Mailbox not found. ...)
Oct 25 02:23:25 mail postfix/pipe[20973]: 8F946FE974E: to=<b_ny4g64lypezdi53dgnra_g4ytim3dgrrtc@example.com>, relay=anonaddy, delay=16, delays=16/0/0/0.14, dsn=2.0.0, status=sent (delivered via anonaddy service)

链路解读:sendmail 投信 → Postfix 经 anonaddy 管道交给应用 → 应用匹配别名后经 SMTP(TLS)投回本地 Postfix → Postfix 直连收件方 MX(qq.com 因演示邮箱不存在返回 550,属预期)→ 退信经 VERP 地址 b_...@example.com 回到管道入库为失败投递。整条「收件→别名匹配→转发→退信回执」闭环已跑通;真实邮箱场景下替换为真实收件人即可送达。

队列侧实测(worker 处理转发任务):

2026-10-25 02:23:08 App\Mail\ForwardEmail .......................... RUNNING
2026-10-25 02:23:08 App\Mail\ForwardEmail .................... 103.64ms DONE

第四步:Web 登录验证:

TOKEN=$(curl -s -c /tmp/cj -H "Host: app.example.com" http://app.example.com/login | grep -oE 'value="[^"]{30,}"' | head -1 | cut -d'"' -f2)
curl -s -b /tmp/cj -c /tmp/cj -o /dev/null -w "%{http_code} -> %{redirect_url}\n" -H "Host: app.example.com" --data-urlencode "_token=$TOKEN" --data-urlencode "username=admin" --data-urlencode "password=q1w2e3r4" http://app.example.com/login
curl -s -b /tmp/cj -H "Host: app.example.com" http://app.example.com/aliases -o /tmp/aliases.html -w "%{http_code}\n"
grep -c "test1@example.com" /tmp/aliases.html

实测回显:

302 -> http://app.example.com/
200
1

五、方案 B:Docker 镜像安装(备选)

官方维护一体化镜像 anonaddy/anonaddy:1.7.3(内置 Nginx + PHP-FPM + Postfix + cron),配合 MariaDB/Redis 用 Compose 编排即可,适合不想手工维护依赖链的场景。完整编排与实测见本专栏《AnonAddy 1.7.3 Docker Compose 部署指南》。

六、方案对比与选择建议

维度方案 A 源码安装方案 B Docker 镜像
依赖控制完全自控(PPA/版本自选)镜像内置,固定
运维成本高(PHP/Nginx/Postfix 逐层维护)低(单镜像多进程)
适合生产、需定制 Rspamd/加固快速起步、测试、NAS
端口25/80/443 直挂宿主25/8000 映射

建议:正式对外服务选方案 A 并按官方文档补 Rspamd 与 DKIM(见本专栏配置参数详解篇);体验与小规模自用选方案 B。

七、配置说明

.env 逐项参数(限额、带宽、DKIM 签名密钥、SPF/DKIM/DMARC 加固)见本专栏《AnonAddy 1.7.3 配置参数详解》;日常 artisan 运维命令见《AnonAddy 1.7.3 常用 CLI 命令详解》。最常改的三项:ANONADDY_DOMAIN(主域)、ANONADDY_ADMIN_USERNAME(管理员用户名)、MAIL_VERIFY_PEER(证书校验开关)。

八、常见问题

Q1:composer install 报 ext-mailparse 缺失? 安装 php8.3-mailparse 后重跑即可,实测 php -m | grep mailparse 应输出 mailparse。

Q2:npm run production 报 Cannot read properties of undefined (reading 'replace')? vite.config.mjs 读 .env 的 APP_URL,先 cp .env.example .env 并填好 APP_URL 再构建。

Q3:创建用户报 The email must be a valid email address.? 两个原因:收件域是 ANONADDY_DOMAIN/ANONADDY_ALL_DOMAINS 内的本地域(规则禁止);或收件域无 MX/A 记录、本机 DNS 查询失败。换真实可解析域,并确认 getent hosts <域> 正常。

Q4:登录返回 419 Page Expired? HTTP 环境下 SESSION_SECURE_COOKIE=true 导致浏览器/curl 不回传会话 Cookie。实验环境改 SESSION_SECURE_COOKIE=false 并 php artisan config:cache;生产启用 HTTPS 后恢复 true。

Q5:管道日志报 storage/logs/laravel-*.log could not be opened: Permission denied? 以 root 跑过 artisan 导致日志文件属 root,www-data 管道写不进去。执行 chown -R www-data:www-data /var/www/anonaddy/storage /var/www/anonaddy/bootstrap/cache 后 postqueue -f 重投。

Q6:外发投递 451 Temporary lookup failure? /etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf 的 query 单引号丢失或库账号不通。用 postmap -q "admin.example.com" mysql:/etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf 定位,正常返回 1。

九、卸载与清理

systemctl stop supervisor
rm -f /etc/supervisor/conf.d/anonaddy-worker.conf /etc/cron.d/anonaddy
rm -f /etc/nginx/sites-enabled/anonaddy /etc/nginx/sites-available/anonaddy
systemctl restart nginx
sed -i '/# Pipe to addy.io application/,+2d; /# addy.io access policy/,+1d' /etc/postfix/master.cf
systemctl restart postfix
mariadb -e "DROP DATABASE addy_database; DROP USER 'addy'@'localhost';"
rm -rf /var/www/anonaddy

按需卸载 PHP PPA、Node、MariaDB、Redis、Postfix。

十、参考链接