Modules/Infrastructure
07

Infrastructure

5 servers. Hetzner, AWS. Docker, Nginx, PM2, SSL, domains. Email infrastructure. How I deploy and keep things running.

DockerNginxServersDevOps
What IS infrastructure?

Infrastructure is everything that makes your code available to the world. You can build the most amazing app on your laptop, but if it only runs on your laptop, nobody else can use it. Infrastructure is the servers, the domain names, the SSL certificates, the process managers, the reverse proxies — all the invisible stuff that takes your code from "works on my machine" to "anyone can open a browser and use this."

I did not learn any of this in advance. I learned it because I needed it. Every single piece of infrastructure knowledge I have came from a real project that needed to be accessible to someone other than me.
The journey: from laptop to servers

First I coded on my laptop. Scripts, small tools, things only I used. Then my team needed to use CampaignPulse. That meant it had to run somewhere they could access it — not on my MacBook that goes to sleep when I close the lid. That meant a server.

Then Quality Monitoring needed to be available 24/7 for the whole QA team across different time zones. Then SMTPCloud Dashboard needed to handle real users, real data, real uptime requirements. Then email delivery needed dedicated relay servers.

One project at a time, I went from zero servers to five. Each one taught me something new. And every time, the process was the same: I told Claude what I needed, and Claude walked me through it.
What is a VPS?

A VPS (Virtual Private Server) is a computer in a data center that you rent. It is always on, always connected to the internet, and you have full control over it. You do not physically touch it — you connect to it remotely and manage everything through the terminal.

Think of it like renting an apartment vs. owning a house. You do not own the physical hardware, but everything inside is yours. You install whatever software you want, run whatever applications you need, and configure it however you like.

My Hetzner servers cost around 5-10 euros per month each. For that, I get a machine with enough CPU, RAM, and storage to run multiple applications. It is absurdly cheap for what you get.
SSH: how you connect to your server

SSH (Secure Shell) is how you talk to your server. It is like the Terminal on your laptop, but for a remote machine. You type one command and suddenly you are "inside" a computer that might be in a data center in Germany or Virginia.

`
ssh root@46.62.208.26
`

That is it. That command connects you to my Server 2 (well, it would if you had the password or SSH key). Once connected, everything you type runs on that remote server, not on your laptop. ls shows the server's files. node app.js runs the app on the server. pm2 list shows what is running on the server.

The first time I SSH'd into a server, it felt like hacking in a movie. It is not. It is just a remote terminal session. But it is genuinely cool that you can sit on your couch and manage a computer across the world.
Hetzner vs AWS — when and why each one

I use both Hetzner and AWS, and they serve different purposes.

Hetzner is a European hosting provider. It is significantly cheaper than AWS — I pay roughly 5-10 euros per month for a server that would cost $30-50 on AWS. The servers are fast, reliable, and come with good bandwidth. I use Hetzner for most of my projects because the cost difference is enormous and the performance is great.

AWS (Amazon Web Services) has more services — not just servers, but also managed databases, file storage (S3), queues, serverless functions, CDNs, and hundreds of other services. I use AWS when I need something specific that Hetzner does not offer, or when global latency matters.

My rule: Hetzner for simple stuff, AWS when I need specific services. Quality Monitoring runs on AWS because the team using it is global and AWS gives good latency worldwide. SMTPCloud backend runs on Hetzner because it is cheaper and the users are mostly in one region. For a beginner, start with Hetzner — it is simpler and cheaper.
Server 1 (37.27.5.172, Hetzner) — the SMTPCloud engine

This is the core business server. It runs:

- SMTPCloud backend — the Express.js API that powers the entire dashboard. All the campaign management, user authentication, email sending logic, analytics, and API key management runs here. This is the biggest and most critical application.
- RT Helper — a real-time automation tool that processes webhook events and triggers workflows.
- n8n — an open-source workflow automation platform (like Zapier but self-hosted). I use it for connecting different services, processing data, and automating repetitive tasks.

All three applications run simultaneously on this one server, managed by PM2. Nginx routes traffic to the right app based on the domain name. This server handles the most traffic of all my servers because every SMTPCloud API call goes through it.
Server 2 (46.62.208.26, Hetzner) — the general-purpose workhorse

This is my Swiss Army knife server. It runs:

- LeadTool — the Python/FastAPI application for lead management. Runs with Uvicorn (Python's equivalent of PM2).
- learn.smtpcloud.io — the learning platform (yes, the site you might be reading this on).
- Postal tracking pixel service — a lightweight service that handles email open tracking via 1x1 pixel images.
- Several smaller apps and tools — various internal utilities that do not need their own server.

This server is a good example of how one VPS can run many things simultaneously. Each app runs on its own port, Nginx routes the traffic, and everything coexists peacefully. The key is that none of these apps are resource-intensive enough to need their own server.
Server 3 (52.202.26.53, AWS) — the enterprise stack

This AWS EC2 instance runs the enterprise-grade tools:

- Quality Monitoring — runs inside Docker Compose with three containers: PostgreSQL database, Fastify backend, and React frontend. Docker makes it easy to start the whole thing with one command and keeps the environments isolated.
- CampaignPulse — the QA automation tool, running as a simple static site served by Nginx.
- Nurturing Reports — CSV-to-Excel reporting system with a FastAPI backend and Next.js frontend, also containerized with Docker.

I chose AWS for this one because the QA team using Quality Monitoring is spread across multiple countries, and AWS data centers give consistent latency globally. The Docker setup also made it easy to reproduce the exact same environment during development on my laptop.
Relay servers — the email delivery infrastructure

These are specialized servers (like the one at 45.148.28.22) that do exactly one thing: send emails. They run Postfix (a mail transfer agent) with OpenDKIM (for cryptographically signing emails) and handle all the outbound email delivery for mail-warm.com.

Relay servers are different from application servers. They do not run web apps. They do not have dashboards. They receive emails from the application via SMTP protocol and deliver them to recipients' inboxes. Their whole purpose is to be configured perfectly for email deliverability — proper DKIM signatures, correct SPF records, DMARC policies, reverse DNS, and clean IP reputation.
Nginx: one server, many apps

Your server has one IP address but you might have 5 different apps running on it. How does the server know which app should handle which request? That is what Nginx does.

Nginx is a reverse proxy — it sits at the front door and routes incoming requests to the right application based on the domain name. Here is the concept:

- Someone visits learn.smtpcloud.io → Nginx sends the request to port 3001 (where the learning platform runs)
- Someone visits leadtool.smtpcloud.io → Nginx sends the request to port 8000 (where LeadTool runs)
- Someone visits api.smtpcloud.io → Nginx sends the request to port 3000 (where the Express API runs)

All three apps run on the same server, on different ports, and Nginx figures out who gets what.
Real Nginx config and what each line means

Here is a simplified version of a real Nginx config from my server:

`
server {
listen 443 ssl; # Listen for HTTPS traffic
server_name learn.smtpcloud.io; # Only handle requests for this domain

ssl_certificate /etc/letsencrypt/live/learn.smtpcloud.io/fullchain.pem; # SSL cert
ssl_certificate_key /etc/letsencrypt/live/learn.smtpcloud.io/privkey.pem; # SSL key

location / {
proxy_pass http://localhost:3001; # Forward everything to port 3001
proxy_set_header Host $host; # Pass the original domain name
proxy_set_header X-Real-IP $remote_addr; # Pass the visitor's real IP
}
}
`

That is the entire config for one app. Nginx listens for HTTPS requests to learn.smtpcloud.io and forwards them to port 3001 where the app is running. Claude wrote every Nginx config file for me. I just told Claude which domain should point to which port, and Claude generated the config.
SSL/HTTPS: encrypting traffic with Let's Encrypt

Without SSL, data between the user's browser and your server travels unencrypted — like sending a postcard instead of a sealed letter. Anyone along the way could read it. That includes passwords, personal data, everything.

Let's Encrypt gives free SSL certificates. Certbot is the tool that installs and auto-renews them. Setup is one command:

`
sudo certbot --nginx -d learn.smtpcloud.io
`

Certbot talks to Let's Encrypt, proves you own the domain, gets the certificate, and configures Nginx to use it. It even sets up auto-renewal so your certificates do not expire (they are valid for 90 days, but certbot renews them automatically before they expire).

Every domain on every server I run has HTTPS. There is no excuse not to — it is free, it takes 2 minutes to set up, and browsers mark non-HTTPS sites as "Not Secure." Claude walked me through this the first time, and now I do it from memory.
PM2: the babysitter for your Node.js apps

Here is the problem: you SSH into your server, run node app.js, and your app starts. Great. Now you close your SSH connection and... the app stops. Because it was running inside your terminal session, and when the session ends, so does the app. Also — what if the app crashes at 3 AM? Nobody is there to restart it.

PM2 solves both problems. It is a process manager that:
- Runs your app in the background (it does not need your terminal session)
- Automatically restarts it if it crashes
- Manages logs so you can see what happened
- Shows you CPU and memory usage
- Handles running multiple apps on the same server

Basic PM2 commands I use daily:
`
pm2 start app.js --name "smtpcloud-api" # Start an app
pm2 list # See all running apps
pm2 restart smtpcloud-api # Restart after deployment
pm2 logs smtpcloud-api # View logs
pm2 monit # Monitor CPU/memory in real-time
`

On my Hetzner servers, PM2 manages several Node.js processes each. A deployment is literally: pull the new code, build, pm2 restart app-name. Done.
Docker: packaging your app in a container

Instead of installing Node.js, PostgreSQL, and all your dependencies directly on the server, you can package everything into a container. A Docker container includes your app, its dependencies, and its configuration — everything it needs to run. The container works the same on your laptop, on a Hetzner server, on AWS, anywhere.

Think of it like shipping a product in a sealed box. Instead of giving someone the raw parts and an instruction manual ("install Node 20, then install these packages, then configure this, then..."), you give them a box that just works when they open it.

I do not use Docker for everything — PM2 is simpler for straightforward Node.js apps. But for more complex setups, Docker is invaluable.
Docker Compose: multiple containers with one command

Quality Monitoring runs 3 containers: PostgreSQL database, Fastify backend, and React frontend. Instead of starting each one separately, Docker Compose lets me define them all in one file and start everything together.

The docker-compose.yml looks something like:
`
services:
postgres:
image: postgres:15
environment:
POSTGRES_DB: quality_monitoring
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
backend:
build: ./backend
depends_on:
- postgres
ports:
- "3000:3000"
frontend:
build: ./frontend
ports:
- "3001:3001"
`

One command starts everything:
`
docker compose up -d
`

And one command stops everything:
`
docker compose down
`

Claude wrote the Dockerfiles and docker-compose.yml for me. It explained each line so I understood what was happening — which image to use, how to map ports, how to persist data with volumes.
Deployment workflow: step by step

Here is what deploying code actually looks like for me, depending on the project:

For Vercel frontends (SMTPCloud Dashboard frontend):
1. git push to GitHub
2. Done. Vercel automatically detects the push, builds, and deploys. Zero manual work.

For server-based Node.js apps (SMTPCloud backend, RT Helper):
1. Code and test on my laptop
2. git push to GitHub
3. ssh root@37.27.5.172 — connect to the server
4. cd /opt/smtpcloud-api && git pull — download the latest code
5. npm run build — build the project
6. pm2 restart smtpcloud-api — restart the app
7. Check the logs with pm2 logs to make sure it started cleanly

For Docker projects (Quality Monitoring):
1. Code and test locally with docker compose up
2. git push to GitHub
3. SSH to the server
4. git pull
5. docker compose up -d --build — rebuild and restart containers

It is not fancy. It does not need to be. The whole point is reliability, not elegance.
DNS and Cloudflare: pointing domains to servers

When someone types learn.smtpcloud.io in their browser, how does the browser know to go to my server at 46.62.208.26? That is what DNS (Domain Name System) does — it translates human-readable domain names into IP addresses.

I manage all my DNS through Cloudflare. The process is:
1. Buy a domain (I use smtpcloud.io, myvaultkeep.io, mail-warm.com, etc.)
2. Point the domain's nameservers to Cloudflare
3. In Cloudflare's dashboard, create an A record: learn.smtpcloud.io46.62.208.26
4. Done. Traffic to that domain now goes to that server.

Cloudflare also provides a CDN (caches your content closer to users for faster loading), DDoS protection (blocks attack traffic), and analytics (shows you who is visiting). All free on the basic plan. For the frontend deployments on Vercel, DNS just points to Vercel's servers instead.
Vercel for frontend: push and forget

For frontend deployments, Vercel is magic. You connect your GitHub repo, and every time you push code, Vercel automatically:
1. Detects the push
2. Installs dependencies
3. Builds the project
4. Deploys to their global CDN
5. Gives you a URL

SMTPCloud Dashboard's frontend deploys to Vercel. I push to GitHub, and 60 seconds later the new version is live. No SSH, no server management, no Nginx config. Vercel handles everything.

For personal projects or frontends that do not need a custom backend server, Vercel is genuinely the easiest deployment I have ever used. And it is free for personal projects.
Email infrastructure deep dive: Postal, Postfix, DKIM/SPF/DMARC

The email side of SMTPCloud deserves its own explanation because email infrastructure is surprisingly complex.

Postal is an open-source mail server I self-host. It is the brain — it manages email sending, tracks delivery, handles bounces, and provides webhooks for tracking events.

Postfix runs on the relay servers. It is the muscle — the thing that actually delivers emails via SMTP to recipients' mail servers.

DKIM (DomainKeys Identified Mail) is a digital signature added to every outgoing email. It proves the email was actually sent by your server and was not tampered with in transit. OpenDKIM on the relay servers signs each email with a private key.

SPF (Sender Policy Framework) is a DNS record that says "these IP addresses are allowed to send email on behalf of this domain." If an email comes from an IP not in the SPF record, receiving servers get suspicious.

DMARC (Domain-based Message Authentication) ties DKIM and SPF together and tells receiving servers what to do if authentication fails — quarantine the email, reject it, or let it through.

Getting all three set up correctly is the difference between your emails landing in the inbox and landing in spam. It took many iterations with Claude to get everything configured properly, but now it runs smoothly.
Monitoring: how to check if things are running

Things break. Servers run out of memory. Apps crash. Databases fill up. You need to know when something goes wrong. Here are the commands I use to check on things:

`
pm2 list # See all Node.js apps and their status
pm2 logs app-name # View recent logs for an app
docker ps # See all running Docker containers
docker logs container-name # View logs for a container
df -h # Check disk space
free -m # Check RAM usage
htop # Live view of CPU and memory
curl -I https://learn.smtpcloud.io # Quick check if a site is responding
`

For Quality Monitoring, I also have health check endpoints — simple API routes that return { status: 'ok' } so I can verify the backend is running without logging in. If curl to the health endpoint fails, something is wrong.

I do not have fancy monitoring dashboards (yet). But checking pm2 list and docker ps once or twice a day catches most problems before users notice.
The philosophy: learn by needing, not by studying

I want to be crystal clear about something: I learned ALL of this by needing it, not by studying it. I never read a book about Nginx. I never took a Docker course. I never watched a video about DNS. Every single piece of infrastructure knowledge I have came from a real project that needed to be deployed.

The first time I set up Nginx, I told Claude: "I have an Express app running on port 3000. I need people to access it at api.smtpcloud.io." Claude gave me the Nginx config and walked me through installing it.

The first time I used Docker, I told Claude: "Quality Monitoring needs PostgreSQL, a Node.js backend, and a React frontend. I want to run all of them together easily." Claude wrote the Dockerfile and docker-compose.yml.

The first time I configured SSL, I told Claude: "My site shows 'Not Secure' in the browser. How do I get HTTPS?" Claude told me about Let's Encrypt and certbot.

After doing each of these things 5-10 times across different projects, I just know how they work. Not because I memorized documentation, but because I have done it enough times that the commands and concepts are second nature. That is how you learn infrastructure — one deployment at a time.