Case study · Linux server build
One server, one domain, 33 passing checks
One rented Linux server can run a website, an API, a database and email for a domain. Do four things by hand: rent a VPS with a static IP and open port 25, register the domain, set the DNS records (A, MX, SPF, DKIM, DMARC, CAA and reverse DNS), and lock SSH down to keys only. Then let an AI coding assistant install the rest under strict rules, have it write a setup script and a verify script, and check the result from outside. Only SSH, the web server and mail face the internet; the API and the database listen on localhost.
Built Oct 16, 20267 steps
How a single rented Ubuntu server went from an order at a hosting company to a live site, API, database and mail server, with an AI assistant doing most of the installs. It runs this site.
A whole stack on one box
The goal was simple to state: host a website, a C# API, a PostgreSQL database and email for one domain, all on one rented Ubuntu server. The site is AIWebCenter.com; its real IP address is replaced here with the example address 203.0.113.10.
It turned out much easier than it looks. An AI assistant ran most of the installs, explained each command and checked its own results. The human job was to make the decisions and approve the commands.
Website, API, database and mail for one domain, without exposing anything that doesn't need to be public.
Four short steps by hand, then hand the installs to Claude Code with strict rules and a verify script.
Live HTTPS and mail on one VPS. The verify script reported 33 checks passed, 0 failed.
| Layer | What the build uses |
|---|---|
| Hosting | Bluehost VPS |
| Operating system | Ubuntu 26.04 LTS |
| Registrar and DNS | Network Solutions |
| Web server and TLS | Nginx with Let's Encrypt certificates |
| API | ASP.NET Core on .NET 10, localhost only |
| Database | PostgreSQL 18, localhost only |
| docker-mailserver with SPF, DKIM and DMARC | |
| Workstation | VS Code and Claude Code |
The build ran in seven steps. Steps 1 to 4 set up the server and its address, by hand. Steps 5 and 6 set up the workstation and hand the installs to the AI. Step 7 puts the site live. Each step tells what happened on this build, then how to do it yourself; swap example.com and 203.0.113.10 for your own.
Choosing a hosting provider
The server is a Bluehost VPS. It shipped with Ubuntu 25.10, a short-term release, and had to be upgraded in place to Ubuntu 26.04 LTS. Picking an LTS image when ordering would have skipped that.
Rent a virtual private server instead of running a box at home: home internet usually changes your IP, blocks mail on port 25 and won't set reverse DNS. Look for:
- A static public IPv4 address, with IPv6 as a bonus
- Root access and a current Ubuntu LTS or Debian image
- 2 or more CPU cores, 4 to 8 GB of memory and 80 GB of SSD for .NET, PostgreSQL and mail
- Outgoing port 25 open, since many cloud hosts block it by default
- A way to set reverse DNS (PTR), by control panel or support ticket
- Snapshots or backups you can restore in minutes
Registering the domain
The domain is registered at Network Solutions, and its DNS stayed there. So every record in step 3 went into the Network Solutions panel, not the hosting company's.
Buy the domain from a registrar, then turn on auto-renew, registrar lock, WHOIS privacy and two-factor sign-in. Decide where DNS lives: only one set of name servers is in charge, and edits anywhere else do nothing. Check which servers answer:
dig NS example.com +shortSetting up DNS
Reverse DNS couldn't be set from any DNS panel: it took a support ticket to point the PTR record at the mail host. The DKIM selector is mail, and the key in DNS matches the key on the server.
DNS records point the domain at the server and tell other mail servers to trust its mail. A message reaches the inbox only when these records match the server that sent it:
@ IN A 203.0.113.10
www IN A 203.0.113.10
mail IN A 203.0.113.10
@ IN CAA 0 issue "letsencrypt.org"
@ IN MX 10 mail.example.com.
@ IN TXT "v=spf1 ip4:203.0.113.10 ~all"
mail._domainkey IN TXT "v=DKIM1; k=rsa; p=<public key from the mail server>"
_dmarc IN TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com"| Record | What it does |
|---|---|
| A for @ and www | Send the domain and www to the server |
| A for mail | Gives the mail server its own name, which the MX record needs |
| CAA | Lets only Let's Encrypt issue TLS certificates for the domain |
| MX | Routes incoming email to the mail host |
| TXT SPF | Lists the IP addresses allowed to send mail for the domain |
| TXT DKIM | Publishes the key that checks the signature on each message |
| TXT DMARC | Tells receivers what to do when SPF or DKIM fails, and where to report |
| PTR (reverse DNS) | Set by the hosting company, not the registrar |
- Keep the trailing dot on the MX value, or some panels append the domain twice.
- Add the DKIM record after step 7: the mail server generates the key.
- Start SPF at
~alland DMARC atp=none; tighten to-allandp=quarantineonce test mail passes. - Changes take from a few minutes up to the record's TTL to show up.
Locking down SSH
This is the one step worth doing by hand, first, before any AI tool touches the server: it decides who can get in at all, and a mistake made later by a script could lock you out.
Key-based SSH replaces the root password as the only way in. Only the public key goes to the server; each login proves you hold the private key without sending it.
Create a key pair on the workstation (Windows 11 ships OpenSSH, so this runs in PowerShell).
ssh-keygen -t ed25519 -C "you@workstation"Sign in as root with the provider's password and create a regular user with sudo rights.
adduser you usermod -aG sudo youCopy the public key to the new user (Windows has no ssh-copy-id, so pipe it).
type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh you@203.0.113.10 "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"Turn off password and root logins in a new file.
PermitRootLogin no PasswordAuthentication no KbdInteractiveAuthentication noRestart SSH and open the firewall for only the ports in use.
sudo systemctl restart ssh sudo ufw allow OpenSSH sudo ufw allow 80,443/tcp sudo ufw allow 25,465,587,993/tcp sudo ufw enable
Keep the first session open until a new login works from a second window. If it fails, the open session is your way back in.
Setting up the workstation
VS Code and Claude Code run on the workstation and on the server. The server's remote desktop listens on 127.0.0.1 only and is reached through an SSH tunnel, which keeps port 3389 off the internet.
The workstation needs three tools to start; the AI installs the rest in step 6. On Windows 11:
winget install Git.Git
winget install Microsoft.VisualStudioCode
irm https://claude.ai/install.ps1 | iexThe last line installs Claude Code (on macOS or Linux: curl -fsSL https://claude.ai/install.sh | bash). In VS Code, add the Claude Code, Remote - SSH and C# Dev Kit extensions; Remote - SSH opens a terminal and the file tree on the server itself.
Letting the AI install the stack
A setup script and a verify script record the whole build. On Oct 16, 2026, the verify script read 33 OK and 0 FAIL.
Ask for a script and a verify check, not a "done" message.
A good prompt names the machine, the exact versions, the limits and the proof you want back. Then approve each command as it runs.
This is an Ubuntu 26.04 LTS server for example.com.
Install the ASP.NET Core 10 runtime, PostgreSQL 18, Node.js LTS, Nginx and certbot.
Rules:
- Bind PostgreSQL to localhost only. Never open port 5432 in the firewall.
- Don't change sshd settings or remove any ufw rule.
- Wait for unattended-upgrades to release the apt lock before installing.
- Keep secrets in a root-only file with mode 600, never in the chat.
Write every command to /root/setup.sh so it can run again on a fresh server.
Then write /root/verify.sh that prints OK or FAIL for each service, port,
certificate and DNS record, and run it.To reach the server's database from the workstation, tunnel to it instead of opening the port; any Postgres client then connects to localhost:5433.
ssh -L 5433:localhost:5432 you@203.0.113.10Going live
AIWebCenter.com went live on this setup, with Let's Encrypt certificates for the website and for mail. Mail runs in docker-mailserver with rspamd spam filtering and the postmaster, abuse and dmarc aliases.
Nginx faces the internet on 80 and 443. It serves the built React files and passes API calls to the .NET app on localhost, so Nginx is the only part anyone reaches.
[Unit]
Description=example.com API
After=network.target postgresql.service
[Service]
User=www-data
WorkingDirectory=/opt/app/api
ExecStart=/usr/bin/dotnet /opt/app/api/Api.dll --urls http://127.0.0.1:5000
Restart=always
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.targetserver {
server_name example.com www.example.com;
root /var/www/example.com;
location / { try_files $uri /index.html; }
location /api/ { proxy_pass http://127.0.0.1:5000; }
}sudo certbot --nginx -d example.com -d www.example.comThen the mail server: it generates the DKIM key, published as the mail._domainkey record from step 3. Check everything from outside:
| Check | How | Passes when |
|---|---|---|
| Website | Open https://example.com | It loads with a valid lock |
| API | curl https://example.com/api/health | HTTP 200 |
| Certificate renewal | sudo certbot renew --dry-run | All renewals succeed |
| Database stays private | nc -zv 203.0.113.10 5432 | Refused or timed out |
| Mail authentication | Send to Gmail or Yahoo, read the headers | spf, dkim and dmarc pass |
| Mail reputation | Send a message to mail-tester.com | 9 or more out of 10 |
Five lessons that carry over
- Pick the LTS image when you order. The in-place upgrade from Ubuntu 25.10 to 26.04 LTS cost time a fresh LTS image would have saved.
- Wait out the apt lock. Ubuntu's daily update job grabbed the lock mid-install. pgrep -f unattended-upgrade also matches a helper that never exits; match bin/unattended-upgrade( |$) instead.
- Reverse DNS goes through the host. The PTR record took a hosting support ticket, not a DNS panel edit.
- Edit DNS only where it's in charge. Records entered at the hosting company do nothing while the registrar's name servers are authoritative.
- Keep scripts, not memory. The setup and verify scripts turned a one-time build into one that can be rerun and checked.
Finishing touches
Once the site is live and test mail arrives, these are worth checking and tightening on any server like this one.
dkim=pass in the headers~all to -all once mail passesp=none to p=quarantine once the reports look cleansudo sshd -T shows permitrootlogin no and passwordauthentication noOne VPS now carries a website, an API, a database and a mail server for one domain. The four steps by hand took the most care; after that, the AI did the installs and the verify script showed what worked. Want one set up and looked after for your own site, API or portal? Tell us about the project.