GitLab CI SMTP Notifications: 2026 Setup Guide
GitLab CI SMTP setup with DKIM. Stop missing failed builds.
GitLab CI SMTP setup with DKIM. Stop missing failed builds.
The merge request got approved. The pipeline ran. And failed. Nobody noticed for two days because GitLab's email notification got filtered, the Slack integration was pointing at a channel nobody watches anymore, and the deploy script's built-in notification sent from a no-reply address that landed in spam.
I've lived this. More than once.
Look, GitLab has built-in notifications. They work fine for merge request comments and standard pipeline events. But they come from GitLab's servers, they look like GitLab's emails, and you don't control the content or recipients. When you need pipeline failures to land in your ops inbox — with your branding, from your domain, formatted how you want — you need to wire up your own SMTP.
By the end of this, you'll have GitLab CI jobs that send email notifications through JustEmails SMTP. Pipeline fails? You'll know. Deploy succeeds? Optional notification, your call. And the emails will actually arrive, because we're doing authentication properly.
A GitLab CI/CD configuration that:
If you need to set up phone notifications for critical failures, VeloCalls integrates with CI pipelines for voice alerts. We'll use curl for SMTP (no extra dependencies) and I'll show an msmtp alternative if you prefer a cleaner syntax.
One thing: if you're self-hosting GitLab and your runners are behind a corporate firewall, confirm port 587 is open outbound. I've seen runners that could hit anything on 443 but literally nothing else. Spent two hours debugging "connection refused" before realizing it was a firewall rule from 2019 that nobody remembered existed. Usually not a problem on GitLab.com shared runners.
Log into justemails.app and grab:
smtp.justemails.app587 (STARTTLS)ci@yourdomain.comCreate a dedicated mailbox for CI. Keeps things isolated. When you inevitably rotate passwords because someone pasted credentials into a ticket, you don't have to touch your other integrations. (This will happen. Plan for it.)
If you're also using the transactional API for application emails, the SMTP vs API comparison breaks down when each makes sense. For CI notifications, SMTP is usually simpler — no SDK, no dependencies.
Never put passwords in .gitlab-ci.yml. Ever. Git history is forever, and someone will grep through it eventually.
Go to your project → Settings → CI/CD → Variables → Add variable.
Create three variables:
| Variable | Value | Settings |
|---|---|---|
SMTP_HOST | smtp.justemails.app | Protected, Masked |
SMTP_USER | ci@yourdomain.com | Protected, Masked |
SMTP_PASS | Your password | Protected, Masked |
Protected means these only get injected into pipelines running on protected branches (usually main or master). Good for production credentials. If you want these available on feature branches too, uncheck Protected — but think about whether you actually need deployment notifications on every branch.
Masked hides the value in job logs. If your script accidentally echoes $SMTP_PASS, GitLab replaces it with [MASKED]. Not foolproof, but catches mistakes. For teams tracking ad spend, tools like ClickzProtect also rely on secure CI/CD pipelines for fraud detection deployments.
For group-wide CI (multiple projects sharing credentials), add these at the group level instead: Group → Settings → CI/CD → Variables. All projects in the group inherit them.
Here's a minimal .gitlab-ci.yml that sends failure notifications:
stages:
- build
- test
- deploy
- notify
build:
stage: build
script:
- echo "Building..."
# Your actual build commands
test:
stage: test
script:
- echo "Running tests..."
# Your actual test commands
deploy:
stage: deploy
script:
- echo "Deploying..."
# Your actual deploy commands
only:
- main
notify-failure:
stage: notify
when: on_failure
script:
- |
curl --ssl-reqd \
--url "smtp://${SMTP_HOST}:587" \
--user "${SMTP_USER}:${SMTP_PASS}" \
--mail-from "${SMTP_USER}" \
--mail-rcpt "team@yourdomain.com" \
--upload-file - <<EOF
From: CI Bot <${SMTP_USER}>
To: team@yourdomain.com
Subject: Pipeline FAILED: ${CI_PROJECT_NAME} (${CI_COMMIT_REF_NAME})
Pipeline failed for ${CI_PROJECT_NAME} on branch ${CI_COMMIT_REF_NAME}.
Commit: ${CI_COMMIT_SHA}
Author: ${CI_COMMIT_AUTHOR}
Message: ${CI_COMMIT_MESSAGE}
View pipeline: ${CI_PIPELINE_URL}
View job: ${CI_JOB_URL}
EOF
The when: on_failure clause means this job only runs if any previous job in the pipeline failed. It runs after everything else, captures the failure, and fires off an email.
What those curl flags do:
--ssl-reqd — requires TLS, refuses to send over plaintext--url "smtp://..." — the SMTP server with port (curl speaks SMTP natively since 7.20)--user — SMTP authentication credentials--mail-from / --mail-rcpt — envelope sender and recipient--upload-file - — reads the email content from stdin (the heredoc)The heredoc includes GitLab's predefined CI variables. CI_PIPELINE_URL gives you a direct link to the failed pipeline. CI_JOB_URL links to the specific failed job. Click, debug, fix. No hunting through GitLab's UI.
Not everyone wants an email for every green build. Honestly, I find success emails annoying — they train you to ignore your inbox. I only notify on failures and first-success-after-failure. But if you want deploy confirmations:
notify-success:
stage: notify
when: on_success
script:
- |
curl --ssl-reqd \
--url "smtp://${SMTP_HOST}:587" \
--user "${SMTP_USER}:${SMTP_PASS}" \
--mail-from "${SMTP_USER}" \
--mail-rcpt "team@yourdomain.com" \
--upload-file - <<EOF
From: CI Bot <${SMTP_USER}>
To: team@yourdomain.com
Subject: Deploy succeeded: ${CI_PROJECT_NAME} to ${CI_ENVIRONMENT_NAME}
Deployment completed for ${CI_PROJECT_NAME}.
Environment: ${CI_ENVIRONMENT_NAME}
Branch: ${CI_COMMIT_REF_NAME}
Commit: ${CI_COMMIT_SHORT_SHA}
Author: ${CI_COMMIT_AUTHOR}
Pipeline: ${CI_PIPELINE_URL}
EOF
rules:
- if: $CI_COMMIT_BRANCH == "main"
The rules section limits this to main branch only. You probably don't want 47 success emails from feature branches during active development.
curl's SMTP support is fine, but the syntax is ugly. Like, really ugly. If you're doing anything more complex — HTML emails, attachments, multiple recipients with CC — msmtp is cleaner.
Install msmtp in your CI job and use a config:
notify-failure-msmtp:
stage: notify
when: on_failure
image: alpine:latest
before_script:
- apk add --no-cache msmtp
- |
cat > /tmp/msmtprc <<EOF
defaults
auth on
tls on
tls_starttls on
tls_certcheck off
account default
host ${SMTP_HOST}
port 587
from ${SMTP_USER}
user ${SMTP_USER}
password ${SMTP_PASS}
EOF
script:
- |
cat <<EOF | msmtp -C /tmp/msmtprc team@yourdomain.com
Subject: Pipeline FAILED: ${CI_PROJECT_NAME}
From: CI Bot <${SMTP_USER}>
To: team@yourdomain.com
Build failed. Check ${CI_PIPELINE_URL}
EOF
The tls_certcheck off is only for Alpine's minimal CA bundle. On Ubuntu-based images, use tls_trust_file /etc/ssl/certs/ca-certificates.crt instead.
Yeah, I wish msmtp's TLS defaults were saner too. The documentation is thorough but somehow manages to make simple things confusing. Personal gripe.
Trigger a failure on purpose. Easiest way:
test-failure:
stage: test
script:
- exit 1 # Always fails
Push, wait for pipeline, check your inbox. If the email arrived, check authentication:
SPF: PASS, DKIM: PASS, DMARC: PASSAll three should pass. If DKIM shows FAIL, your DNS records need the DKIM entry from JustEmails. Our DMARC enforcement guide covers the full verification process.
Look, something will break. It always does. Here's the stuff I see most often.
curl: (67) Access denied: 535
Authentication failed. Check your SMTP_USER is the full email address (ci@yourdomain.com, not just ci). Check SMTP_PASS for trailing whitespace — copy fresh from your dashboard.
curl: (7) Failed to connect to smtp.justemails.app port 587
Network block. Shared GitLab.com runners have port 587 open, but self-hosted runners or enterprise setups might block it. Test with nc -zv smtp.justemails.app 587 in a job. If it times out, talk to your network team.
Emails arrive but go to spam
Missing authentication. Run mail-tester.com — send your test email to their address, get a deliverability report. Below 8/10 means work to do.
Usually it's a missing SPF include or DKIM record that hasn't propagated yet. DNS propagation is one of those things that's supposed to take "up to 48 hours" but somehow always takes exactly as long as your patience lasts. For browser-based email testing and debugging, JustBrowser sessions can help inspect raw email headers.
Variable not set / empty SMTP_HOST
The variable exists but isn't being injected. Check if Protected is checked — if so, your pipeline must be running on a protected branch. Or the variable scope doesn't include your branch. Temporarily uncheck Protected to debug.
curl: (60) SSL certificate problem
TLS verification failed. On some minimal Docker images, the CA bundle is missing. Add --insecure temporarily to confirm it's a cert issue (don't leave this in production). Proper fix is to install ca-certificates in your image.
Different people care about different failures. Here's a pattern that routes backend test failures to the backend team and frontend failures to frontend:
notify-backend-failure:
stage: notify
when: on_failure
rules:
- if: $CI_JOB_NAME =~ /backend/
script:
- # email to backend-team@yourdomain.com
notify-frontend-failure:
stage: notify
when: on_failure
rules:
- if: $CI_JOB_NAME =~ /frontend/
script:
- # email to frontend-team@yourdomain.com
The rules match against the failed job's name. Regex patterns work. Gets noisy if you have dozens of jobs, but for smaller pipelines it's a quick way to route alerts. (I tried building a fancy routing engine once. Spent a week on it. Turned out regex matching on job names did 90% of what I needed. Lesson learned.)
Pipeline notifications are wired up. Now what?
If you're measuring DORA metrics, pipeline emails are the raw signal. JustAnalytics can help track pipeline performance trends over time alongside your other engineering metrics.
Add a job that uses curl, msmtp, or a sendmail wrapper to dispatch email through an SMTP server. Store your SMTP host, username, and password as protected CI/CD variables in your project or group settings. Reference them in your .gitlab-ci.yml and trigger the job on pipeline success, failure, or both using rules or when: conditions.
The sending domain lacks proper authentication. SPF, DKIM, and DMARC records must be configured for the domain in your From address. If you're sending from ci@yourdomain.com, that domain needs DNS records pointing to your SMTP provider. JustEmails auto-configures DKIM signing, but you still need to verify the records are in place.
GitLab's built-in notifications work for standard events like merge request comments and pipeline status — but they come from GitLab's servers, not your domain. Custom SMTP lets you send from your own domain, change what the message says, pick recipients per-job, and plug pipeline events into whatever alerting you've already got running. Use built-in for standard stuff, custom SMTP when you want control.
Go to your project or group Settings → CI/CD → Variables. Add SMTP_HOST, SMTP_USER, and SMTP_PASS as protected, masked variables. Protected means they're only available to pipelines on protected branches. Masked means the values are hidden in job logs. Reference them in your pipeline as $SMTP_HOST, $SMTP_USER, and $SMTP_PASS.
$49/year flat. Unlimited domains, unlimited email accounts, 10 GB storage, full IMAP/SMTP. Built for agencies, freelancers, and anyone juggling email across more than one domain.