Mailcow VPS: How to Make Gmail Accept Your Email
Test outbound port 25 before mailcow installation: a block keeps mail in queue until bounce. Set DNS correctly to avoid Gmail 550 5.7.25 PTR rejection.
Wetin you dey build
One complete mail server for machine wey you own: SMTP to send and receive mail, IMAP make your phone and laptop stay in sync, webmail client, and spam filter wey dey score every message for both directions. mailcow-dockerized bundle Postfix, Dovecot, Rspamd, SOGo webmail, MariaDB, Redis and ACME client inside one Docker Compose stack, so na the software no be the difficult part. You go get am running within half an hour.
The difficult part na everything around am. Email na the one service wey the rest of internet actively dey distrust when server just new, and the difference between “e dey work” and “Gmail silently dey swallow every message” mostly depend on four DNS records and one IP-reputation setting wey you fit no fully control. Read the prerequisites below before you rent anything. If, after you read dem, you decide say the reputation grind no worth am, that one na valid answer, our rundown of wetin really worth self-hosting for 2026 put email under “only if you mean it” for exactly these reasons.
Prerequisites na the project
If you miss any one of these, you go send mail wey no go ever arrive. For rough order of how often each one dey cause problem:
Outbound port 25 must dey open. Your server dey deliver mail to Gmail and Microsoft through TCP port 25. Plenty VPS and cloud providers dey block outbound port 25 by default to fight spam. The block dey silent: nothing dey error during boot, everything dey look healthy, and mail dey sit deferred for queue until Postfix give up and bounce am. (mailcow set maximum queue lifetime to five days.) Test am before you install anything. If e dey blocked, the only fix na to open support ticket and ask your provider to open am. Some providers go do am for old accounts, but some no go ever do am.
A clean IP wey get usable reputation. Recycled VPS IPs often don already enter blocklists because the previous tenant use dem send spam. Check your own IP with service like Spamhaus lookup or mxtoolbox before you commit. If the IP dey listed, mail fit get rejected and you no fit solve that with code.
Control of DNS plus correct PTR record. You need add records to your domain zone. You also need reverse DNS (PTR) for the server IP, pointing back to your mail hostname. PTR almost never dey configured for your DNS panel. E dey with the person or company wey own the IP, so you go set am for your VPS provider control panel or through support ticket.
6 GiB RAM and 2 vCPU na the comfortable floor. mailcow own minimum na 6 GiB RAM plus 1 GiB swap for private installation. E advise 8 GiB once some users dey depend on the server. Below about 2.5 GiB, generate_config.sh dey offer to disable the ClamAV virus scanner so the kernel no go start kill containers. mailcow documented disk minimum na 20 GiB before you store any mail, so start with more than that and leave space for mailboxes to grow.
A DNS name, no be bare IP. Choose hostname like mail.example.com. That one name go become your MAILCOW_HOSTNAME, your TLS certificate subject, your PTR target, and your SMTP banner. Keep am consistent everywhere.
Step 1, Prove outbound port 25 dey open
Do this first. If e fail, everything else na wasted effort. From the fresh VPS, try open SMTP conversation with real mail server:
sudo apt update && sudo apt install -y netcat-openbsd
nc -vz -w 5 gmail-smtp-in.l.google.com 25Working result go show immediately:
Connection to gmail-smtp-in.l.google.com (142.250.x.x) 25 port [tcp/smtp] succeeded!If port block, e go hang for full five seconds, then fail:
nc: connect to gmail-smtp-in.l.google.com port 25 (tcp) timed out: Operation now in progressFor fresh VPS wey you never set outbound firewall rules for, that timeout na provider block. Na provider-side network filter, no be your firewall, so no local change fit fix am. Open ticket: "Please enable outbound TCP port 25 for my VPS at <IP>; I am running a legitimate mail server." No install mailcow until this return "succeeded". Note say inbound 25 (when other servers dey reach you) na separate path and e usually dey open. Na outbound side providers dey throttle.
Step 2, Set the DNS records now
DNS changes dey take time before dem propagate, so publish everything wey you fit before you install. Assume say your domain na example.com, your mail host na mail.example.com, and the IP na 10.0.0.10. For your zone, create:
mail.example.com. A 10.0.0.10
mail.example.com. AAAA 2001:db8::10 ; only if you have IPv6
example.com. MX 10 mail.example.com.
example.com. TXT "v=spf1 mx -all"
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:postmaster@example.com"The SPF record dey talk say “na only my MX fit send for this domain; reject every other one”. Start DMARC with p=none so you fit monitor reports without bouncing your own mail; tighten am to p=quarantine, then p=reject after you confirm say alignment dey work. Two records still dey missing on purpose: DKIM, wey mailcow go generate for you for Step 6, and PTR, wey you go set now for your provider panel.
Set the PTR (reverse DNS) for 10.0.0.10 to mail.example.com, the exact value of MAILCOW_HOSTNAME. Na this single record plenty people dey forget, and big providers dey reject mail because of am. If your panel no get rDNS field, raise a ticket.
Step 3, Install Docker
mailcow need Docker Engine 24.0 or newer and Docker Compose 2.0 or newer. The Compose plugin (docker compose) na the easiest way, but mailcow still support the standalone docker-compose binary. Use Docker official convenience script instead of Ubuntu docker.io package, because e no get Compose plugin at all:
curl -fsSL https://get.docker.com | sudo sh
sudo docker compose versionYou suppose see one Docker Compose version line wey dey report v2.x or v5.x. Compose v5, wey release for 2025, dey work the same way as v2, and either one meet mailcow requirement. If docker compose version print docker: 'compose' is not a docker command, Docker Engine don install but Compose plugin no dey. Install the plugin from Docker repository, run the script above again, or follow our Docker Compose basics guide, wey set up both from Docker own apt repository.
Step 4, Clone mailcow and generate the config
cd /opt
sudo git clone https://github.com/mailcow/mailcow-dockerized
cd mailcow-dockerized
umask
sudo ./generate_config.shTo check say umask dey print 0022, follow mailcow own install steps. Dem set umask 0022 before cloning. mailcow no dey test the value, and fresh Ubuntu 24.04 root shell already gives you 0022. The script then ask the one thing wey matter: the fully qualified hostname. Enter mail.example.com. The value must match your A record and PTR exactly. E write mailcow.conf, the single environment file wey the whole stack dey read. Open am if you need change the web ports (HTTP_PORT, HTTPS_PORT) or disable ClamAV for small box:
MAILCOW_HOSTNAME=mail.example.com
HTTP_PORT=80
HTTPS_PORT=443
SKIP_CLAMD=n # set to y to drop the virus scanner on a <2.5 GiB boxSKIP_FTS=y na the other control for low-RAM box. Full-text search na the second memory hog wey mailcow docs mention. If you skip am, na only body-text search for webmail you go lose.
Leave HTTP_PORT=80 and HTTPS_PORT=443 unless another thing for the host already dey use dem. mailcow built-in ACME client dey use HTTP-01 challenge by default. This one need port 80 to dey reachable from internet before e fit obtain the certificate. (mailcow 2026-03 and later fit use DNS-01 challenge instead with ACME_DNS_CHALLENGE=y. This one no need port 80, but e need API access to your DNS host.) Na why the simple layout no dey run separate nginx-plus-Certbot setup for the same box. mailcow dey issue and renew its own TLS internally, and another service wey occupy 80/443 go break am. mailcow document the alternative too: bind its web listeners locally with HTTP_BIND=127.0.0.1 HTTP_PORT=8080 HTTPS_BIND=127.0.0.1 HTTPS_PORT=8443, put reverse proxy for 80 and 443, and, if you want local Certbot to own the certificates, point mailcow to dem and add renewal hook. This one get more moving parts for mail server. So the same trade-off apply to any other service wey want public web front end: a build like Halcyon wey reskin Jellyfin library as 90s video store, wey expect its own reverse proxy for 443, belong for another host unless you move mailcow behind that proxy. The exception na service wey never ask for public web port at all: self-hosted RustDesk relay dey listen only on its own 21115 to 21119 range. So e fit share the box, as long as you get enough extra bandwidth.
Step 5, Start the stack and log in
sudo docker compose pull
sudo docker compose up -d
sudo docker compose psThe pull go fetch around two dozen images, so give am few minutes. When docker compose ps show say every container running (or healthy), open https://mail.example.com/admin for browser. The default admin login na username admin and password moohoo. Change that password immediately for the admin UI under System → Configuration, then Access → Administrators. If the browser warn say NET::ERR_CERT_AUTHORITY_INVALID, ACME certificate never issue yet. Check the ACME failure section below before you conclude say something break. Self-signed placeholder dey normal for the first minute or two.
Step 6, Add a domain, a mailbox, and publish DKIM
For the admin UI, open E-Mail → Configuration. Under the Domains tab, click Add domain and enter example.com. Then under Mailboxes, click Add mailbox to create you@example.com with a password. That mailbox dey already ready and reachable over IMAP.
Now make the DKIM key. Go to System → Configuration, then Options → ARC/DKIM keys. mailcow fit don generate key when you add the domain. If e never generate am, generate one there, choose the domain, leave the selector as dkim, choose 2048-bit, then click Add. Copy the long TXT value wey e show and publish am as:
dkim._domainkey.example.com. TXT "v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqh...long-key...QAB"mailcow Domains page get DNS button wey list every record wey e expect. E also show green tick or red cross based on wetin actually dey published. Use am as your checklist, and make every row green before you test deliverability. Red DKIM row after publishing usually mean say you split the key wrongly across TXT chunks. 2048-bit key pass the 255-character limit for one TXT string, so paste am as one logical value and make your DNS host split am into chunks.
Step 7, Test deliverability and chase 10/10
Go mail-tester.com, copy the random address wey e show, then send message go am from your new mailbox. Log in to SOGo webmail for https://mail.example.com/SOGo and send from there. Then click "Then check your score".
Target na 10/10. Na these things dey commonly reduce the score, and na wetin dem mean:
- SPF no align, your
MX/SPF record dey miss, or the sending IP no dey covered. Check the SPF TXT again. - DKIM signature no verify,
dkim._domainkeyTXT dey miss, e still dey propagate, or e don spoil. Na this one dey cause the most common failure. - No PTR / PTR mismatch, reverse DNS no resolve to
mail.example.com. Fix am for the provider. - Dem list am for blocklist, your IP get bad reputation from before. Request delisting, or ask for cleaner IP.
No send real mail go Gmail or Outlook until the result show 10/10. Low score plus new IP fit make dem flag your domain from day one.
Step 8, Connect real mail client
Point Thunderbird, Apple Mail, or your phone go the server with these settings. The server host na mail.example.com for all of dem:
- IMAP: port 993, SSL/TLS (or 143 with STARTTLS)
- SMTP submission: port 465, SSL/TLS (or 587 with STARTTLS)
- Username: the full address,
you@example.com - Password: the mailbox password wey you set
Never send client mail through port 25. Na server-to-server traffic only for that port. mailcow no offer authenticated submission there, and e go refuse client wey point to am. If client report Relay access denied, e dey try send through 25 or without authentication; switch am to 465 or 587 with your mailbox credentials.
Step 9, Back up wetin really matter
mailcow get backup script wey dey take snapshot of every stateful volume. Run am go external disk or mounted remote:
sudo MAILCOW_BACKUP_LOCATION=/opt/mailcow-backups \
./helper-scripts/backup_and_restore.sh backup allall dey capture six things, and if any one lost, data go lost: vmail (the real mailboxes), crypt (the keys wey dey decrypt vmail; without am, dem useless), mysql (the MariaDB wey dey hold domains, users, aliases and settings), redis (queue and cache state), rspamd (spam/ham learning), and postfix (the mail queue). E dey run inside helper container wey dey write compressed archives, so backups dey consistent even while stack still dey run. Automate am with nightly cron job, and add --delete-days 14 to prune old sets. Cron job wey stop working fit fail silently, so send the script exit status go somewhere wey you go actually notice am; self-hosted ntfy server fit take one-line curl from the same cron entry and put the failure for your phone that night. Restore na the same script with restore. E go list the snapshots and let you choose wetin to bring back. Backup wey you never test-restore na hope, no be backup. Do one dry run go scratch VPS.
I step 10, Make updates follow schedule
mailcow dey update through its own script. The script dey pull new code, migrate mailcow.conf, prefetch images, and restart containers in the correct order:
cd /opt/mailcow-dockerized
sudo ./update.sh --check # reports whether an update exists, changes nothing
sudo ./update.sh # applies itBack up first (Step 9), because e hard to reverse schema migration. Updates dey come often and dem include security fixes for daemons wey face internet, so no let mail server stay for months without update. If update ever make one container become unhealthy, sudo docker compose logs --tail=50 <service>-mailcow go show the daemon wey fail to come back.
Hardening bad note
mailcow dey run im own netfilter service (netfilter-mailcow) wey dey ban IPs wey dey hammer mail and webmail ports, so mail side get protection out of the box. But e no cover SSH for the host itself. SSH still dey exposed and attackers still dey use brute-force attack against am. Pair this setup with Fail2ban wey dey monitor SSH auth log and key-only login. Use strong password protect mailcow admin UI. If possible, keep am off public internet or put am behind VPN.
Failure modes, with the exact strings
Mail queues and never delivers. Run sudo docker compose exec postfix-mailcow postqueue -p, or read the admin UI mail queue; entries dey deferred for up to five days before Postfix bounce dem, with:
status=deferred (connect to gmail-smtp-in.l.google.com[142.250.x.x]:25: Connection timed out)Na almost always outbound port 25 wey your provider block (Step 1). Outbound rule for your own host firewall fit produce the same timeout, so rule that one out first; if the host clean, no config fix am, open a ticket. E no be DNS and e no be TLS; the sign na the word timed out against remote MX for port 25.
Gmail marks everything spam, or bounces it. Open the message for Gmail, select "Show original", then read the auth results. dkim=fail or dkim=none mean say your dkim._domainkey TXT record dey missing, mangled, or not yet propagated. Republish exactly wetin the ARC/DKIM page show, then wait for the TTL. spf=fail mean say the SPF/MX records no cover your IP. Alignment na everything; one check wey fail don dey enough to send am go spam.
Rejected by large providers on connect. Bounces or Postfix logs carry Gmail PTR rejection:
550-5.7.25 [10.0.0.10] The IP address sending this message does not have a PTR
550-5.7.25 record setup, or the corresponding forward DNS entry does not match
550 5.7.25 the sending IP. As a policy, Gmail does not accept messages from IPs
550 5.7.25 with missing PTR records.The 550 5.7.25 code mean say reverse DNS dey missing or e no match. Set the PTR for your IP to mail.example.com for the provider (Step 2). Forward (A) and reverse (PTR) must agree, and both must name the same host wey mailcow dey greet other servers as.
Browser shows a certificate warning that never clears. The acme-mailcow container fail to get real cert. Check the log:
sudo docker compose logs acme-mailcow | tail -n 40Line like Cannot validate any hostnames, skipping Let's Encrypt for 1 hour. or challenge failure mean say port 80 no dey reachable from the internet, or the A record no point to this server. If you publish an AAAA record, check am too: mailcow dey validate against the AAAA record first and e skip the A check when one dey, so wrong AAAA or IPv6 path wey no fit reach port 80 go make validation fail even when the A record correct. Confirm say mail.example.com resolve to the box, open 80 and 443 through any host firewall, and make sure say nothing else dey bind those ports. After you fix the cause, restart the client with sudo docker compose restart acme-mailcow instead of waiting for the hour-long back-off.
FAQ
Self-hosting email really worth am?
If you want make your data remain your own, get unlimited aliases, and control everything, yes, mailcow gives you professional stack for VPS price. But deliverability na work wey you must continue do: IP reputation, DNS alignment, and blocklist monitoring no dey ever finish completely. For business address wey critical well-well, where one day for person spam folder fit cost you, managed provider na the practical choice. Use self-hosting when control matter pass convenience and you go really maintain am. If wetin you really want na to keep your data away from another person server, instead of email specifically, start with service wey nobody else fit decide for: photo library no get deliverability problem at all, and our PhotoPrism and Immich comparison cover the RAM minimums and backup commands for that setup on the same kind VPS.
How I fit know whether outbound port 25 dey blocked?
Run nc -vz -w 5 gmail-smtp-in.l.google.com 25 from the server. "succeeded!" mean say the port dey open; if you see timed out after the pause, the port dey blocked. For server wey you no set outbound firewall rule for, na your provider block am. This na the most common reason self-hosted server fit receive mail but never send am. If provider block the port, only the provider fit open am. No local setting fit change that.
Why my mail still dey enter Gmail spam folder?
Almost every time, na broken authentication chain cause am. Use "Show original" for Gmail and look for spf=pass, dkim=pass, and dmarc=pass. dkim=fail mean say dkim._domainkey TXT record dey missing or spoil. PTR mismatch or new IP wey never get sending history fit also cause problem. First make mail-tester.com reach 10/10. Then warm the IP slowly: send few messages per day and increase the number gradually, instead of sending plenty mail on the first day.
Exactly wetin I need back up?
Run backup_and_restore.sh backup all and keep the complete backup set outside the server. E capture vmail (the mailboxes), crypt (the keys wey decrypt dem), MariaDB database (domains, users, aliases, settings), Redis, Rspamd learned data, and Postfix queue. The crypt volume na the one wey people often forget. Without am, vmail backup na ciphertext wey nobody fit read. Test restore onto scratch box at least once.
I fit run mailcow for 2 GB VPS?
E no go run comfortably. generate_config.sh offer to disable ClamAV below about 2.5 GiB. Even when ClamAV dey off, Rspamd, Dovecot, MariaDB, and the rest of the stack go dey compete for memory. You go hit swap and OOM kills under any real workload. Treat 6 GiB plus 1 GiB swap as the minimum for stable single-user installation. Move to 8 GiB as soon as more than a couple people begin depend on am.