Skip to content
All articles
7 min read

Prisma P1001 "Can't Reach Database Server": Fix It

MD Rakibul Islam RakibMD Rakibul Islam RakibFull-stack developer, DevOps & Linux engineer
Prisma P1001 "Can't Reach Database Server": Fix It

Prisma P1001 means nothing answered at the host and port in DATABASE_URL: the database is down, the host is wrong (often localhost in Docker) or blocked.

P1001 is a network error, not a Prisma bug and not a password problem. Prisma tried to open a TCP connection to the address in your URL and got no answer in time. I've seen it on every kind of setup: a local Postgres that wasn't started, a Docker Compose app pointing at localhost, a CI job running migrations against a private database, and a serverless database that was asleep. The API behind this site runs Prisma on PostgreSQL, and I use the steps below whenever a migration or a deploy throws P1001.

Key takeaways

  • Test the network first with nc -zv host 5432 from the same machine or container as the app. If that fails, Prisma will too.
  • In Docker, localhost is the container itself. Use the Compose service name, such as db, as the host.
  • Postgres listens only on localhost by default. Remote connections need listen_addresses, a pg_hba.conf rule and an open firewall port.
  • Managed databases often need sslmode=require, an IPv4 or pooler hostname, or a longer connect_timeout for a sleeping database.
  • P1000 is a different error: the server answered and rejected the login.

The error

Error: P1001: Can't reach database server at `localhost:5432`

Please make sure your database server is running at `localhost:5432`.

Read the host and port in the message carefully. Half the time they aren't what you expected, because a different .env file, a CI secret or a shell variable won. Prisma prints exactly what it used.

Step 1: Can this machine reach that address at all?

Run the check from where Prisma runs: your laptop, the server, inside the container, or in the CI job.

nc -zv db.example.com 5432
# Connection to db.example.com 5432 port [tcp/postgresql] succeeded!

# inside a container that has no nc:
docker compose exec api node -e "require('net').connect(5432,'db').on('connect',()=>{console.log('ok');process.exit()}).on('error',e=>console.log(e.code))"
  • ECONNREFUSED / "Connection refused": the host is up but nothing listens on that port. Postgres is stopped or listens elsewhere.
  • Hangs, then times out: a firewall is dropping packets, or the host is unreachable from this network.
  • ENOTFOUND: the hostname doesn't resolve here. Typo, a private DNS name, or an IPv6-only name on an IPv4 network.

Cause 1: Postgres isn't running

sudo systemctl status postgresql
sudo ss -tlnp | grep 5432
docker compose ps db

If it's stopped, start it and read why it stopped: sudo journalctl -u postgresql -n 50 or docker compose logs db. A full disk is a common reason Postgres refuses to start, see No space left on device.

Cause 2: localhost inside Docker

This is the one I see most. Each container has its own network namespace, so localhost inside the app container is the app container, not the database. Use the service name from docker-compose.yml:

services:
  db:
    image: postgres:17
  api:
    environment:
      DATABASE_URL: postgresql://app:pw@db:5432/app   # not localhost
    depends_on:
      db:
        condition: service_healthy

The reverse catches people too: running prisma migrate from your laptop against a database in Compose needs localhost and a published port (ports: ["127.0.0.1:5432:5432"]), while the app inside Compose needs db. Keep two URLs, one per context.

The condition: service_healthy part matters as well. Without a healthcheck on db, the app can start and run migrations before Postgres accepts connections, and you get P1001 only on the first boot.

api container db container postgres :5432 listening localhost:5432 P1001: loops to itself db:5432 Same Compose network, reached by service name
Inside a container, localhost means that container. The app has to use the Compose service name to reach the database container on the shared network.

Cause 3: Postgres only listens on localhost

If the app or your migration runs on another machine, Postgres has to accept outside connections. Three things must all be true:

# 1. postgresql.conf (needs a restart, not a reload)
listen_addresses = 'localhost,10.0.0.5'     # this server's private IP

# 2. pg_hba.conf: allow only the app's network
host  app  app  10.0.0.0/24  scram-sha-256

# 3. firewall: open 5432 to that network only
sudo ufw allow from 10.0.0.0/24 to any port 5432 proto tcp
sudo systemctl restart postgresql

Don't open 5432 to the whole internet. Bots scan it constantly. For a one-off connection from your laptop, use an SSH tunnel instead: ssh -L 5433:127.0.0.1:5432 user@server, then connect to localhost:5433. If you run Postgres in Docker on a server with ufw, read why Docker bypasses ufw before publishing the port.

Cause 4: Managed database quirks

  • SSL required. Most managed Postgres services reject plain connections. Add ?sslmode=require to the URL.
  • IPv6-only hostnames. Supabase's direct connection hostname resolves to IPv6 unless you buy the IPv4 add-on. Many VPS and CI networks have no IPv6 route, so it hangs. Use the connection pooler hostname from the dashboard instead.
  • Sleeping databases. Neon and similar services suspend idle compute. The first connection wakes it, which can take longer than Prisma's default 5 second connect timeout. Add &connect_timeout=15.
  • Private hostnames. Railway's *.railway.internal and other private-network names only resolve inside that platform. Running migrations from GitHub Actions needs the public URL.
  • IP allowlists. Some providers only accept listed IPs. CI runners change IP every run, so either allow the runner's range or migrate from a host with a fixed IP.

Cause 5: The wrong URL won

Prisma connected to the address it printed, so check where that came from:

echo "$DATABASE_URL"            # a shell variable overrides .env
grep -rn DATABASE_URL .env* prisma.config.ts 2>/dev/null

In Prisma 7 the CLI reads the URL from prisma.config.ts, and .env isn't loaded unless you import dotenv/config there. If the URL is undefined you get a different error; my Prisma 7 upgrade guide covers that setup. At runtime, Prisma 7 connects through a driver adapter, so the same network problem may show up as the driver's ECONNREFUSED or timeout instead of P1001. The fixes are identical.

Once it connects

  • P1000 means the network works and the login was rejected. Follow my password authentication failed guide.
  • Run migrations in the deploy with prisma migrate deploy, from a machine that can reach the database, before switching traffic to the new release.
  • Size your pool. More app instances means more connections; see too many clients already.

Frequently asked questions

What does Prisma error P1001 mean?

Prisma couldn't open a network connection to the database host and port in your URL. Either the database isn't running, the host or port is wrong for the place Prisma runs, or a firewall or network rule blocks the connection.

Why do I get P1001 in Docker but not locally?

Your URL probably uses localhost. Inside a container, localhost is the container itself. Replace it with the database's Compose service name, for example db, and make the app wait for the database healthcheck.

How do I fix P1001 with Supabase?

The direct database hostname is IPv6 by default, and many networks can't reach IPv6. Use the pooler connection string from the Supabase dashboard, which works over IPv4, and keep sslmode=require.

What's the difference between P1001 and P1000?

P1001 means Prisma couldn't reach the server at all. P1000 means it reached the server and the server rejected the username or password. Fix the network for P1001 and the credentials or pg_hba.conf for P1000.

Can I increase Prisma's connection timeout?

Yes. Add connect_timeout in seconds to the connection string, for example ?connect_timeout=15. It helps with databases that wake from sleep, but it won't fix a blocked port or wrong host.

Want this fixed and deployed properly?

I fix broken Node.js, Next.js and NestJS apps and set up Prisma, Postgres and Docker so deploys and migrations just run. See my bug fixing service, all web development services, or contact me with the full error.

MD Rakibul Islam Rakib

Written by

MD Rakibul Islam Rakib

Full-stack developer, DevOps engineer and Linux system administrator with 5+ years of production experience. I deploy, harden and fix servers and web apps for clients worldwide, and everything in this article runs on real servers I manage, including this site.

  • Prisma P1001
  • Can't reach database server
  • DATABASE_URL
  • Docker Compose Postgres
  • Supabase
  • Neon
  • PostgreSQL

Keep reading

WordPress to Next.js Migration Without Losing SEO
Website DesignOct 9, 2026

WordPress to Next.js Migration Without Losing SEO

Moving WordPress to Next.js makes a site faster and safer, but only if you keep every URL, redirect old ones with 301s and carry over titles and meta tags. Business owners usually ask me about this after one of three things: the WordPress site got hacked, it fails Core Web Vitals no matter how many caching plugins they add, or the plugin and hosting bills keep growing. Next.js fixes all three well. This site is a Next.js 16 app, and I've rebuilt WordPress sites this way. But a migration done carelessly can wipe out years of Google rankings in a week. This guide covers when it's worth it, the two ways to do it, and the SEO steps that keep your traffic. Key takeaways Migrate for a reason: speed, security, custom features or cost. If your team lives in the WordPress editor and the site is fine, a headless setup or a tune-up may be better. Two paths: headless (keep WordPress as the editor, Next.js as the website) or full move (content goes into Markdown, a headless CMS or your own database). Crawl the old site first and keep a list of every URL. That list is your redirect map and your checklist. Keep URLs identical where you can. Where they must change, add one 301 redirect per old URL, never a blanket redirect to the homepage. Carry over titles, meta descriptions, alt text and structured data , then watch Search Console for a few weeks. Is it worth moving off WordPress? Good reasons to migrate: Speed. A Next.js site sends pre-rendered HTML and only the JavaScript a page needs. Page-builder themes often load large CSS and script bundles on every page. Faster pages help rankings and conversions; see my guides on fixing slow LCP and INP . Security. Most WordPress hacks I've cleaned up came through an outdated plugin or theme. A Next.js site has no public admin login and no plugin folder to attack. Custom features. Booking, dashboards, customer portals and integrations are normal code in Next.js, not a stack of plugins fighting each other. Running costs. No premium plugin licences to renew, and a fast site runs on a small VPS. My hosting cost comparison shows the options. Reasons to wait: non-technical editors who rely on the WordPress editor daily, a WooCommerce shop with many extensions you'd have to replace, or a site that already performs well. Changing technology doesn't fix weak content or an unclear offer. Path 1: Headless WordPress WordPress stays where your team writes, and Next.js becomes the public website. Next.js reads posts and pages from the built-in REST API ( /wp-json/wp/v2/posts ) or from the WPGraphQL plugin: // app/blog/[slug]/page.tsx import { notFound } from 'next/navigation'; const WP = 'https://cms.example.com/wp-json/wp/v2'; export default async function Post({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const res = await fetch(`${WP}/posts?slug=${slug}&_embed`, { next: { revalidate: 300 } }); const [post] = await res.json(); if (!post) notFound(); return <article dangerouslySetInnerHTML={{ __html: post.content.rendered }} />; } Editors keep their workflow, and the slow, plugin-heavy front end disappears. Move WordPress to a subdomain such as cms.example.com , block it from search engines, and protect its login. The catch: you now run two systems, and page-builder layouts don't come across, because Next.js renders the content, not the builder. Path 2: Full migration Export the content once and switch WordPress off. Content goes into Markdown or MDX files in the repo (great for small, developer-run sites), a headless CMS such as Sanity, Strapi or Payload, or your own database with an admin dashboard, which is how this site works. Pull everything through the REST API with a script, convert the HTML, download the media, and keep each post's slug, date, title, excerpt and SEO fields. You end up with one system, fewer moving parts, and nothing left to patch every week. Every URL from the old site needs a destination: the same path, or a 301 redirect to its new address. Old URLs left without one return 404s, and the rankings they earned disappear with them. The SEO checklist that protects your rankings 1. Inventory every URL before you touch anything Export the old site's URLs from three places: the XML sitemap (Yoast and Rank Math put it at /sitemap_index.xml ; core WordPress at /wp-sitemap.xml ), a crawl with a tool like Screaming Frog, and Search Console's Pages and Performance reports. Mark the pages that get traffic or backlinks. Those must not break. 2. Keep the same URLs Next.js routes can match almost any WordPress structure. If posts lived at /my-post/ , use an app/[slug]/page.tsx route. If you want cleaner URLs, change them on purpose and redirect, not by accident. Watch trailing slashes: WordPress adds them, Next.js doesn't by default. Set trailingSlash: true in next.config.ts if you keep the old style. 3. Redirect what changes // next.config.ts export default { async redirects() { return [ { source: '/:year(\\d{4})/:month(\\d{2})/:slug', destination: '/blog/:slug', permanent: true }, { source: '/category/:cat', destination: '/blog?topic=:cat', permanent: true }, { source: '/feed', destination: '/rss.xml', permanent: true }, ]; }, }; permanent: true sends a 308, which search engines treat like a 301. For hundreds of one-off URLs, keep them in a map file or do them in Nginx. Never redirect everything to the homepage. Google treats that as a soft 404 and drops the rankings anyway. 4. Carry over the SEO data Export the title and meta description from your SEO plugin for every page, and set them with generateMetadata . Keep image alt text, canonical tags, Open Graph images, and structured data such as Article, FAQ, Product and LocalBusiness. Generate a sitemap with app/sitemap.ts and a robots.ts . 5. Don't forget the hidden features Contact forms (and their spam protection and email delivery, see emails going to spam ), the search box, comments, RSS, analytics, cookie consent and newsletter embeds. List every plugin and decide what replaces each one. 6. Launch, then watch Switch DNS when traffic is lowest, keep the old server running for a few days, then submit the new sitemap and test key URLs in Search Console. Crawl the old URL list against the new site and expect a 200 or a single 301 for every one, never a 404 or a redirect chain. My zero-downtime migration guide covers the DNS cutover, and redesign without losing SEO has the full checklist. Frequently asked questions Is Next.js better than WordPress for SEO? Next.js makes it easier to build fast pages with clean HTML, which helps Core Web Vitals. But rankings come from content, links and technical basics. A well-optimised WordPress site can outrank a careless Next.js one, so the migration must preserve URLs and metadata. Will I lose rankings when moving from WordPress to Next.js? Not if you keep URLs the same or 301-redirect every changed one, keep titles and meta descriptions, and keep the content. A short wobble for a few weeks is normal while Google recrawls. Big drops come from missing redirects. Can I keep using WordPress as a CMS with Next.js? Yes. That's headless WordPress: editors use the normal admin, and Next.js reads content through the REST API or WPGraphQL and renders the public site. Keep the WordPress admin off the public domain and updated. How long does a WordPress to Next.js migration take? A small business site of 10 to 30 pages is usually a few weeks including design, content transfer, redirects and testing. Large blogs, multilingual sites and WooCommerce shops take longer because there's more content and functionality to rebuild. What about WooCommerce? You can run WooCommerce headless or move to a commerce platform with an API, but checkout, payments, shipping and extensions all need to be rebuilt or replaced. Plan and price that part separately; it's usually most of the work. Thinking about leaving WordPress? I rebuild WordPress sites in Next.js with every URL, redirect and meta tag carried over, so you get the speed without losing your Google traffic. See my business website design and development service, all website design services , or send me your site for an honest opinion on whether it's worth it.

Read article →
Website "Not Secure" With SSL? Fix Mixed Content
Website DesignOct 9, 2026

Website "Not Secure" With SSL? Fix Mixed Content

A site that shows "Not secure" with a valid SSL certificate is loading something over http://. Find it in the browser console, then switch those URLs to https. You installed an SSL certificate, the address starts with https:// , and the browser still shows "Not secure" or a broken padlock. Visitors notice. Some leave, and a contact or checkout form that looks unsafe loses leads. This is almost always mixed content : the page itself is secure, but it loads an image, script, font or form target over plain http:// . I fix this during server moves and redesigns all the time, and it takes three steps: find the insecure URLs, fix them at the source, then add a safety net so new ones can't slip in. Key takeaways Open DevTools (F12) → Console. Every insecure resource is listed as a "Mixed Content" warning with its exact URL. Scripts, stylesheets and iframes over http are blocked , which can break menus, sliders and forms, not only the padlock. The real fix is changing http:// to https:// where the URL lives: the database, theme files, CSS, or a hard-coded embed. A form that submits to http:// makes Chrome warn on submit. Check every form's action . Add upgrade-insecure-requests as a safety net, and redirect all http traffic to https. Step 1: Find every insecure resource Open the page in Chrome, press F12, and go to the Console tab. Reload. Mixed content shows up like this: Mixed Content: The page at 'https://example.com/' was loaded over HTTPS, but requested an insecure script 'http://example.com/wp-content/plugins/slider/slider.js'. This request has been blocked; the content must be served over HTTPS. The Security tab in DevTools gives a summary for the page. Check more than the homepage: blog posts, the shop, the contact page and checkout often load different files. To scan from a terminal, search the raw HTML for http:// in the attributes that load things: curl -s https://example.com/ | grep -oE '(src|href|action|srcset)="http://[^"]+"' | sort -u curl -s https://example.com/wp-content/themes/mytheme/style.css | grep -o 'url(http://[^)]*' | sort -u Plain links to other sites ( <a href="http://..."> ) don't cause mixed content, though it's still worth updating them. What matters is anything the page loads : images, scripts, styles, fonts, iframes, video, and form targets. Why browsers treat it differently Browsers split mixed content into two kinds. For images, audio and video, Chrome first tries to load the same URL over https automatically, and only blocks it if that fails. Scripts, stylesheets, iframes and fetch requests are blocked outright, because an attacker on the network could change them and take over the page. That's why mixed content can do more than remove the padlock: a blocked jQuery or stylesheet can break the whole layout. Browsers upgrade insecure images and media to https when they can, but block insecure scripts, styles and iframes completely. One http script can remove the padlock and break a menu or a form at the same time. Step 2: Fix the URLs at the source WordPress Old http:// URLs live in the database: in post content, widget settings, theme options and page builder data. First set both addresses under Settings → General to https:// . Then replace the rest with WP-CLI, which handles serialized data safely (a plain SQL replace can corrupt it): wp db export before-https.sql # backup first wp search-replace 'http://example.com' 'https://example.com' --skip-columns=guid --dry-run wp search-replace 'http://example.com' 'https://example.com' --skip-columns=guid wp cache flush Run the dry run first and read the counts. If you use a caching plugin or a CDN, purge it afterwards, or visitors keep seeing the old HTML. Hard-coded URLs in themes, CSS and templates Search the code for http:// and change your own domain's URLs to https or to relative paths ( /images/logo.png ): grep -rn "http://" --include=*.{php,html,css,js,tsx} ./theme ./src | grep -v "http://www.w3.org" For third-party files (an old widget, font or analytics script), switch to the provider's https URL. If a provider doesn't support https at all in 2026, replace it. It's also a security risk. Forms Check the action of every form, including newsletter embeds. A form posting to http:// makes Chrome show a warning when the visitor submits, which kills conversions on contact and checkout pages. Step 3: "Not secure" but no mixed content? If the console is clean, look at these instead: Your app thinks it's on http. Behind Nginx, a load balancer or Cloudflare, the app sees plain http and builds http:// links. Pass proxy_set_header X-Forwarded-Proto $scheme; in Nginx and make the app trust it. In WordPress behind a proxy, check $_SERVER['HTTPS'] handling in wp-config.php . Cloudflare "Flexible" SSL. Browser to Cloudflare is encrypted, Cloudflare to your server isn't, and it often causes redirect loops. Install a certificate on the origin and use "Full (strict)". My too many redirects guide covers this. The certificate itself. Expired, issued for a different name (www vs non-www), or missing its chain. Click the padlock area to see the reason, and see Certbot renewal failed if it expired. Step 4: Add a safety net Redirect all http traffic to https with one 301, and tell browsers to upgrade any leftover http requests. In Nginx: server { listen 80; server_name example.com www.example.com; return 301 https://www.example.com$request_uri; } # inside the https server block: add_header Content-Security-Policy "upgrade-insecure-requests" always; add_header Strict-Transport-Security "max-age=31536000" always; upgrade-insecure-requests makes the browser fetch every http:// resource on the page over https. It hides problems rather than fixing them, so do it after step 2, not instead of it. HSTS tells browsers to always use https for your domain. Start with a short max-age if you're not sure every subdomain has a certificate. Does mixed content hurt SEO? HTTPS is a Google ranking signal, and Google's page experience guidance expects pages served securely. More directly, a "Not secure" warning and broken scripts hurt trust and conversions. If you just moved to https, also check that canonical tags, the sitemap and internal links all use the https URLs. My redesign without losing SEO checklist covers the redirect side, and crawled, currently not indexed helps if pages drop out afterwards. Frequently asked questions Why does my website say "Not secure" when I have SSL? The page loads at least one resource over plain http, which is called mixed content, or the certificate has a problem such as being expired or issued for a different domain. The browser console lists any mixed content URLs. How do I find mixed content on my website? Open Chrome DevTools with F12, go to the Console and reload the page. Each insecure resource appears as a "Mixed Content" warning with its URL. Repeat on your main templates: home, blog post, product, contact and checkout. Is upgrade-insecure-requests enough to fix mixed content? It's a good safety net, but it only works if every resource is also available over https. Fix the URLs at the source first, then keep the header to catch anything you missed. How do I fix mixed content in WordPress? Set the WordPress and Site Address to https under Settings, then run wp search-replace from http to https with a backup and a dry run first. Fix any remaining hard-coded URLs in the theme or plugins and purge caches. Does mixed content affect Google rankings? HTTPS is a lightweight ranking signal, and mixed content weakens it. The bigger cost is lost trust: browser warnings and broken scripts make visitors leave and stop forms from converting. Want a secure, fast site that converts? I fix SSL, mixed content and redirect problems, and build business websites that are secure and SEO-ready from day one. See my technical SEO audit and fix , all website design services , or send me your URL .

Read article →
Emails Going to Spam? Fix SPF, DKIM and DMARC (2026)
Linux System AdminOct 9, 2026

Emails Going to Spam? Fix SPF, DKIM and DMARC (2026)

Business emails land in spam mostly because SPF, DKIM or DMARC is missing or broken. Gmail's "Show original" shows which one fails; fix it in your DNS. When your quotes, invoices or contact-form replies go to spam, you lose customers without ever knowing it. Gmail, Yahoo and Outlook now expect every sender to prove the email really comes from their domain, and they're strict about it. I set up DNS and mail for the sites I build and host, and when a client tells me "customers say they never got our email", the cause is nearly always one of the authentication records below, or a contact form that pretends to send from the visitor's address. Here's how to check yours in five minutes and fix it. Key takeaways Check a real message first. In Gmail, open it, click the three dots, then "Show original". It shows PASS or FAIL for SPF, DKIM and DMARC. SPF lists which servers may send for your domain. One record only, and every service you send through must be in it. DKIM signs each email. Turn it on in every service that sends for you: Google Workspace, Microsoft 365, your newsletter tool, your app's email provider. DMARC ties them together and tells inboxes what to do with failures. Start with p=none and reports, then tighten. Contact forms must send from your own domain and put the visitor's address in Reply-To , never in From . Why inboxes got strict Since February 2024, Gmail and Yahoo require every sender to have SPF or DKIM, and bulk senders (about 5,000 or more messages a day to Gmail) to have SPF, DKIM and DMARC, one-click unsubscribe for marketing mail, and a spam complaint rate under 0.3%. Microsoft started enforcing similar rules for Outlook.com, Hotmail and Live in May 2025. You may not be a bulk sender, but the same checks decide where your emails land. Unauthenticated mail is the first thing filters push to spam. Step 1: Read the verdict on a real email Send an email from your business address to a Gmail account. Open it, then three dots → Show original . At the top you'll see: SPF: PASS with IP 209.85.220.41 DKIM: 'PASS' with domain example.com DMARC: 'PASS' Any FAIL, NEUTRAL or a missing line tells you where to start. Do this for every way your business sends email: your mailbox, your website's contact form, your shop's order emails, your newsletter. They're often different systems, and each needs its own setup. From a terminal you can read the records directly: dig +short TXT example.com | grep spf dig +short TXT _dmarc.example.com dig +short TXT google._domainkey.example.com # selector depends on the provider Step 2: Fix SPF SPF is a TXT record on your domain listing who may send for it. A typical one for Google Workspace plus a transactional email service: example.com. TXT "v=spf1 include:_spf.google.com include:amazonses.com ~all" Mistakes I find all the time: Two SPF records. Adding a second v=spf1 record for a new service breaks both. Merge them into one record. A sending service not listed. Your CRM or invoicing tool sends as your domain but isn't in the record. Check each tool's docs for its include: value. More than 10 DNS lookups. Each include can trigger several lookups, and past 10 SPF fails with a "permerror". Remove services you no longer use. Ending with +all , which lets anyone send as you. Use ~all (soft fail) or -all . Step 3: Turn on DKIM everywhere DKIM adds a digital signature to every email. The sending service gives you a public key to publish in DNS, usually as a TXT or CNAME record under selector._domainkey.example.com , and then you switch signing on in its admin panel. In Google Workspace it's under Apps → Gmail → Authenticate email; Microsoft 365 has it in the Defender portal. Google Workspace doesn't sign with your domain until you publish the record and click "Start authentication". That last click is the step people miss. Do the same for each service that sends as your domain. DKIM is what survives forwarding, so it matters more than SPF in practice. Receiving inboxes check that the sending server is allowed (SPF), that the message is signed by your domain (DKIM), and that both match the From address (DMARC). Pass all three and the email has a fair chance at the inbox. Step 4: Add DMARC DMARC tells receivers what to do when SPF and DKIM don't line up with the domain in the From address, and sends you reports. Start in monitor mode: _dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com" After two to four weeks of reports showing all your legitimate senders pass, move to p=quarantine , then p=reject . That also stops scammers from sending fake invoices in your name. The raw reports are XML; a free DMARC report viewer makes them readable. Step 5: Fix your website's contact form This one is behind most "website emails go to spam" complaints I see. The form sends a message to you from the visitor's address, for example From: jane@gmail.com , through your web server. Gmail checks Gmail's DMARC policy, sees a server that isn't Gmail, and rejects or junks it. The correct setup: From: Website <forms@example.com> # your domain, authenticated To: sales@example.com Reply-To: jane@gmail.com # the visitor, so "Reply" still works And send through a proper email API or SMTP service (Amazon SES, Postmark, Resend, Brevo, or your Google Workspace account) with SPF and DKIM set up for example.com , not through PHP's mail() on the web server. Step 6: If you send from your own VPS Running your own mail server is possible, but it's the hardest path. Check these before blaming DNS: Port 25 is often blocked for outgoing mail by cloud providers by default. You may need to request unblocking. Reverse DNS (PTR) for the server's IP must point to a hostname that points back to the same IP. Set it in your VPS provider's panel. IP reputation. A new or previously abused IP starts with a bad reputation. Check it against blocklists, and use TLS for every connection. For most small businesses, a mailbox provider for people and a transactional email service for the website is cheaper than the hours a self-hosted mail server costs. My Cloudflare VPS setup checklist covers the DNS side of a new server, including not proxying mail records. Content and list habits still matter Only email people who asked , and make unsubscribing one click. Complaints hurt more than anything else. Separate marketing from transactional mail , for example newsletters from news.example.com , so a campaign can't drag down your invoices. Avoid link shorteners and image-only emails , and keep the visible link text matching the real URL. Watch Google Postmaster Tools once you send regularly. It shows your spam rate and domain reputation at Gmail. Frequently asked questions Why are my emails going to spam? The most common reasons are missing or broken SPF, DKIM or DMARC records, a contact form sending as the visitor's address, a poor sending IP, or recipients marking your mail as spam. Gmail's "Show original" tells you which authentication check fails. Do I need SPF, DKIM and DMARC? Yes, all three. Gmail and Yahoo require at least SPF or DKIM from every sender and all three from bulk senders, and Microsoft enforces similar rules. Without them your email is much more likely to be filtered or rejected. Can I have two SPF records? No. A domain with two v=spf1 records fails SPF entirely. Combine all senders into one record with several include: entries, and stay under 10 DNS lookups. How long do DNS changes for SPF, DKIM and DMARC take? Usually minutes to a few hours, depending on the record's TTL. Test with dig or an online checker, then send a fresh email and check "Show original" again. Old emails won't change. Why do my website contact form emails go to spam? Usually the form sends from the visitor's email address through your web server, which fails their domain's DMARC check. Send from your own domain through an authenticated email service and put the visitor's address in Reply-To. Want your emails in the inbox? I set up domains, DNS, SPF, DKIM, DMARC and website email sending for small businesses, and fix contact forms that lose leads. See my business website service , all Linux system admin services , or send me your domain and I'll check your records.

Read article →