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.

Last updated

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.

1 serverA Bluehost VPS
Ubuntu 26.04 LTSUpgraded in place from 25.10
3 public entry pointsSSH, Nginx and mail
0 open database portsPostgreSQL on localhost only
The brief

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.

Challenge

Website, API, database and mail for one domain, without exposing anything that doesn't need to be public.

Approach

Four short steps by hand, then hand the installs to Claude Code with strict rules and a verify script.

Result

Live HTTPS and mail on one VPS. The verify script reported 33 checks passed, 0 failed.

The finished layout. The firewall opens only SSH, Nginx and mail. The API and database listen on localhost, and the workstation reaches them through SSH tunnels.
LayerWhat the build uses
HostingBluehost VPS
Operating systemUbuntu 26.04 LTS
Registrar and DNSNetwork Solutions
Web server and TLSNginx with Let's Encrypt certificates
APIASP.NET Core on .NET 10, localhost only
DatabasePostgreSQL 18, localhost only
Emaildocker-mailserver with SPF, DKIM and DMARC
WorkstationVS 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.

By hand · one time

Choosing a hosting provider

On this build

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.

Do it yourself

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
By hand · one time

Registering the domain

On this build

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.

Do it yourself

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:

workstation
dig NS example.com +short
By hand · registrar panel

Setting up DNS

On this build

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.

Do it yourself

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:

dns zone
@                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"
RecordWhat it does
A for @ and wwwSend the domain and www to the server
A for mailGives the mail server its own name, which the MX record needs
CAALets only Let's Encrypt issue TLS certificates for the domain
MXRoutes incoming email to the mail host
TXT SPFLists the IP addresses allowed to send mail for the domain
TXT DKIMPublishes the key that checks the signature on each message
TXT DMARCTells receivers what to do when SPF or DKIM fails, and where to report
PTR (reverse DNS)Set by the hosting company, not the registrar
Details that trip people up
  • 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 ~all and DMARC at p=none; tighten to -all and p=quarantine once test mail passes.
  • Changes take from a few minutes up to the record's TTL to show up.
By hand · before any AI

Locking down SSH

On this build

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.

Do it yourself

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.

  1. Create a key pair on the workstation (Windows 11 ships OpenSSH, so this runs in PowerShell).

    workstation
    ssh-keygen -t ed25519 -C "you@workstation"
  2. Sign in as root with the provider's password and create a regular user with sudo rights.

    server · root
    adduser you
    usermod -aG sudo you
  3. Copy the public key to the new user (Windows has no ssh-copy-id, so pipe it).

    workstation · powershell
    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"
  4. Turn off password and root logins in a new file.

    /etc/ssh/sshd_config.d/99-hardening.conf
    PermitRootLogin no
    PasswordAuthentication no
    KbdInteractiveAuthentication no
  5. Restart SSH and open the firewall for only the ports in use.

    server
    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
Don't lock yourself out

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.

You · three installs

Setting up the workstation

On this build

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.

Do it yourself

The workstation needs three tools to start; the AI installs the rest in step 6. On Windows 11:

workstation · powershell
winget install Git.Git
winget install Microsoft.VisualStudioCode
irm https://claude.ai/install.ps1 | iex

The 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.

AI assistant · you approve

Letting the AI install the stack

On this build

A setup script and a verify script record the whole build. On Oct 16, 2026, the verify script read 33 OK and 0 FAIL.

Do it yourself

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.

paste into claude code · on the server
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.

workstation
ssh -L 5433:localhost:5432 you@203.0.113.10
AI assistant · then verify

Going live

On this build

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.

Do it yourself

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.

/etc/systemd/system/app-api.service
[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.target
/etc/nginx/sites-available/example.com
server {
    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; }
}
server
sudo certbot --nginx -d example.com -d www.example.com

Then the mail server: it generates the DKIM key, published as the mail._domainkey record from step 3. Check everything from outside:

CheckHowPasses when
WebsiteOpen https://example.comIt loads with a valid lock
APIcurl https://example.com/api/healthHTTP 200
Certificate renewalsudo certbot renew --dry-runAll renewals succeed
Database stays privatenc -zv 203.0.113.10 5432Refused or timed out
Mail authenticationSend to Gmail or Yahoo, read the headersspf, dkim and dmarc pass
Mail reputationSend a message to mail-tester.com9 or more out of 10
Takeaways

Five lessons that carry over

  1. 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.
  2. 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.
  3. Reverse DNS goes through the host. The PTR record took a hosting support ticket, not a DNS panel edit.
  4. Edit DNS only where it's in charge. Records entered at the hosting company do nothing while the registrar's name servers are authoritative.
  5. Keep scripts, not memory. The setup and verify scripts turned a one-time build into one that can be rerun and checked.
After it's live

Finishing touches

Once the site is live and test mail arrives, these are worth checking and tightening on any server like this one.

CheckSend test mail to Gmail and Yahoo and confirm dkim=pass in the headers
CheckScore the server at mail-tester.com
TightenSPF from ~all to -all once mail passes
TightenDMARC from p=none to p=quarantine once the reports look clean
CheckRoot and password logins are off: sudo sshd -T shows permitrootlogin no and passwordauthentication no

One 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.