Tutorial

Provision a server and connect your domain

From "I have nothing" to "https://yourdomain.com serves my app." Pick a provider, rent a Linux box, point a domain at it, add HTTPS, and hand it to Magic Deploy.

This guide assumes you can open a terminal and you own (or are about to buy) a domain name. No prior server admin experience required. Every command in here is copy-paste; every UI step names the button you're clicking. Total time on a clean coffee: ~30–45 minutes.

Why this exists. Magic Deploy's "Your own server" target wants an SSH-reachable Linux box and asks for the host, user, and SSH key. The scripts cookbook covers what to run on that box — but not how to get one in the first place. This page closes that gap.

1. Pick a provider

For a first server hosting a small web app, side project, or static site, optimize for: cheap, simple control panel, sane defaults, easy DNS. Performance differences at the bottom tier are tiny — the operational overhead matters far more.

ProviderCheapest planWhy pick itSkip if
DigitalOcean $4/mo Droplet (512 MB) Cleanest UI for first-timers. One-click Marketplace images (LAMP, MEAN, Docker, etc.). DNS hosting included free. You're already paying for AWS / GCP and want one bill.
AWS Lightsail $3.50/mo (512 MB) Wraps EC2 in a DO-style flat-rate UI. Same datacenter as the rest of AWS — useful if you'll later add S3, RDS, etc. You want autoscaling, VPCs, IAM-per-resource, or other "real" AWS features. Use EC2 instead.
AWS EC2 ~$5/mo (t4g.nano) The full AWS toolkit — VPC, security groups, IAM, autoscaling, AMIs. Required if you'll grow beyond one box. You just want one box. Lightsail is the same thing with less paperwork.
Hetzner Cloud ~€4/mo (CX22, 4 GB) Best price/performance in Europe by a wide margin. Also has US-East / US-West. You need a region near non-EU/US users.
Vultr / Linode (Akamai) $5–6/mo (1 GB) Wide region selection. Linode now has a proper DNS manager. Vultr has more locations. No strong preference — DigitalOcean is friendlier.

If you're stuck, pick DigitalOcean. Its UI labels match what tutorials say, its DNS panel is right next to its server panel, and its $200 / 60-day signup credit means your first server is effectively free. The walkthrough below uses DO; the Lightsail and EC2 sections cover the AWS deltas.

2. Path A — DigitalOcean droplet

2.1 Create an SSH key (one-time, on your Mac)

SSH keys replace passwords. You generate a keypair locally; the public half goes onto the server, the private half stays on your laptop. Anyone with the private half can log in, so don't share it.

# Skip if you already have ~/.ssh/id_ed25519
ssh-keygen -t ed25519 -C "[email protected]"
# Press Enter at every prompt (default path, empty passphrase) for the simplest setup.
# For better security, set a passphrase — macOS will remember it in Keychain.

# Print the public key so you can paste it into DigitalOcean:
cat ~/.ssh/id_ed25519.pub

The output starts with ssh-ed25519 AAAA…. That whole line is what you'll paste.

2.2 Create the droplet

  1. Sign up at digitalocean.com. They'll ask for a card; the $200 signup credit is applied automatically.
  2. Click CreateDroplets.
  3. Region: pick the one closest to your users. NYC3, SFO3, FRA1, SGP1 are popular.
  4. Image: Ubuntu 24.04 LTS x64. (LTS = "Long Term Support", patched until 2029.)
  5. Size: Basic → Regular → $4/mo (1 vCPU, 512 MB RAM, 10 GB SSD). Upgrade later if you need to; downgrading is harder.
  6. Authentication: SSH KeyNew SSH Key. Paste the ssh-ed25519 … line from step 2.1, give it a name like "MacBook," click Add SSH Key. Make sure the box for the key is checked.
  7. Hostname: anything memorable, e.g. web-prod-1. Cosmetic.
  8. Create Droplet. ~30 seconds later you'll see a public IPv4 address. Copy it.

2.3 Test SSH

# Replace 203.0.113.42 with your droplet's IPv4 address
ssh [email protected]

First connection asks "Are you sure you want to continue connecting?" — type yes. You should land at a root@web-prod-1:~# prompt. Type exit for now — we'll come back after a security pass in section 6.

Permission denied (publickey)? Either you didn't tick the SSH key box during droplet creation, or your ~/.ssh/id_ed25519 isn't the same key whose public half you pasted. Run cat ~/.ssh/id_ed25519.pub and confirm it matches what's listed under Settings → Security → SSH Keys on DigitalOcean. If it doesn't, you can add the right key via the droplet's Recovery Console or the DO docs.

3. Path B — AWS Lightsail

Lightsail is AWS's answer to DigitalOcean: flat monthly pricing, simple UI, the same Linux underneath. If you're already in the AWS ecosystem (using S3, SES, Route 53, etc.), this lets you keep one bill.

3.1 Create the instance

  1. Sign in to lightsail.aws.amazon.com. (You need a regular AWS account; Lightsail just shares it.)
  2. Click Create instance.
  3. Region: pick the closest one. Note that this is the AWS region (e.g. us-east-1) — useful later when wiring S3 or RDS in the same region.
  4. Platform: Linux/Unix.
  5. Blueprint: OS Only → Ubuntu 24.04 LTS. (The "Apps + OS" blueprints are pre-installed stacks; we want a clean slate.)
  6. Instance plan: $3.50/mo (512 MB) for testing, $5/mo (1 GB) if your app uses Node / Rails / Django. The first 3 months on the $3.50/mo plan are free.
  7. SSH key pair: click Change SSH key pairUpload new. Paste the contents of ~/.ssh/id_ed25519.pub from section 2.1. Don't use AWS's default key — the rest of this guide assumes id_ed25519.
  8. Create instance. Wait until status flips from Pending to Running.

3.2 Static IP (important)

Lightsail instances get a public IP that changes if you stop/start them. Attach a static IP so DNS doesn't break:

  1. Click your instance → Networking tab → Create static IP.
  2. Attach it to the instance you just created. Static IPs are free while attached; they're billed only if you detach and leave them dangling.
  3. That static IP is the address you'll point your domain at.

3.3 Open ports for HTTP / HTTPS

Lightsail's firewall opens SSH (22) by default. To serve websites you need to open 80 and 443:

  1. Instance → NetworkingIPv4 FirewallAdd rule.
  2. Add: HTTP (TCP, port 80) and HTTPS (TCP, port 443).

3.4 Test SSH

ssh ubuntu@<your-static-ip>

Lightsail Ubuntu instances use the ubuntu user (not root). Otherwise the rest of this guide is the same — when later sections say ssh root@…, use ssh ubuntu@… and prefix admin commands with sudo.

4. Path C — AWS EC2

EC2 is the full AWS compute primitive. More setup than Lightsail; necessary if you'll later want autoscaling groups, custom AMIs, multi-AZ, IAM roles per resource, or VPC peering.

4.1 Launch the instance

  1. EC2 Console → Launch instance.
  2. AMI: Ubuntu Server 24.04 LTS (free-tier eligible).
  3. Instance type: t4g.nano (ARM, ~$3/mo) or t3.micro (x86, free tier for new accounts the first 12 months).
  4. Key pair: Create new key pair if you don't have one in this region — type: ED25519, format: .pem. Save the .pem to ~/.ssh/aws-ec2.pem and run chmod 400 ~/.ssh/aws-ec2.pem (EC2 refuses to use world-readable keys). Or pick Use existing key pair if you uploaded one earlier.
  5. Network settings: Allow SSH from My IP, plus Allow HTTP and Allow HTTPS from anywhere. (Allowing SSH from "anywhere" works but exposes port 22 to the world; you'll harden it later.)
  6. Storage: 8 GB gp3 is plenty for a starter app.
  7. Launch instance.

4.2 Elastic IP

EC2's default public IP also changes on stop/start. Allocate an Elastic IP and associate it with the instance — same reason as Lightsail's static IP. Elastic IPs are free while associated with a running instance; AWS charges if you let one sit unattached.

EC2 → Network & Security → Elastic IPs → Allocate, then Associate with your instance.

4.3 Test SSH

ssh -i ~/.ssh/aws-ec2.pem ubuntu@<elastic-ip>

Note the -i flag — EC2 doesn't use your default ~/.ssh/id_ed25519 unless you specifically uploaded that public key during launch. To stop typing -i every time, add this to ~/.ssh/config:

Host my-ec2
    HostName <elastic-ip>
    User ubuntu
    IdentityFile ~/.ssh/aws-ec2.pem

Then ssh my-ec2 just works.

5. Path D — Hetzner / Vultr / Linode / generic VPS

The flow is identical to DigitalOcean, with different button labels. Across providers:

Concrete provider docs: Hetzner, Vultr, Linode. Once your instance is up and SSH works, jump to section 6.

6. First login & basic hardening

You now have an SSH-reachable box. Three things before installing anything: update packages, create a non-root user, turn on the firewall. ~5 minutes.

6.1 Update everything

ssh root@<your-ip>     # or ssh ubuntu@<your-ip> on Lightsail/EC2

apt update && apt upgrade -y
apt install -y ufw fail2ban

ufw = uncomplicated firewall, friendlier wrapper around iptables. fail2ban auto-bans IPs that brute-force SSH.

6.2 Create a non-root user

Logging in as root every time is a footgun — one bad rm -rf wipes the box. Create a regular user and only escalate when needed.

adduser deploy                     # set a password when prompted
usermod -aG sudo deploy            # give them sudo
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

Test from a second terminal (don't close the first one yet):

ssh deploy@<your-ip>
sudo whoami    # should print "root"

If that works, you're done. From now on always SSH in as deploy.

6.3 Disable root SSH login (optional, recommended)

Edit /etc/ssh/sshd_config:

sudo sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl restart ssh

Now: only key-based logins, only as deploy (or ubuntu on AWS). Brute-forcing SSH is now impossible without your private key.

6.4 Turn on the firewall

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp        # HTTP
sudo ufw allow 443/tcp       # HTTPS
sudo ufw enable              # answer 'y'
sudo ufw status              # confirm all three rules are listed

UFW will not block your existing SSH session — but if you ever forget allow OpenSSH before enable, you can lock yourself out. Always allow SSH first.

7. Install nginx and serve a test page

sudo apt install -y nginx
sudo systemctl enable --now nginx

Open http://<your-ip> in a browser. You should see "Welcome to nginx!". If not: check sudo ufw status (port 80 allowed?) and on Lightsail/EC2 check the cloud-side firewall too.

7.1 Replace the default page

nginx serves /var/www/html by default. Drop a real index file in:

sudo bash -c 'cat > /var/www/html/index.html <<EOF
<!doctype html>
<title>hello</title>
<h1>It works.</h1>
<p>Served from $(hostname) at $(date).</p>
EOF'

# Make sure the deploy user can write here later (for rsync)
sudo chown -R deploy:deploy /var/www/html

Refresh the browser. You should see your new page.

8. Point your domain at the server

You need to tell the internet that yourdomain.com = your-server-ip. That's a DNS A record. Where you set it depends on which company hosts your DNS — usually either your registrar (Namecheap, Google Domains successor, Porkbun, GoDaddy) or a third-party host (Cloudflare, Route 53, DigitalOcean DNS).

Strong recommendation: put your DNS on Cloudflare. Free, fast, and adds a CDN + DDoS layer at no cost. The rest of this section assumes Cloudflare; the same A-record concept works at your registrar with different button labels.

8.1 Move DNS to Cloudflare (one-time)

  1. Sign up at cloudflare.com.
  2. Add a site → enter your domain → pick the Free plan.
  3. Cloudflare will scan and import your existing records. Skim them — anything weird? Usually just MX (mail) records and the existing A record. Click Continue.
  4. Cloudflare gives you two nameservers like aria.ns.cloudflare.com, ben.ns.cloudflare.com.
  5. Log in to your registrar (where you bought the domain). Find Nameservers for that domain and replace the existing ones with Cloudflare's.
  6. Wait. Propagation is usually ~10 minutes; can be up to 24 hours. Cloudflare will email when it's live.

8.2 Add the A record

Once Cloudflare is live for your domain:

  1. Cloudflare dashboard → your domain → DNS → Records.
  2. Add record:
    • Type: A
    • Name: @ (this means the apex, i.e. yourdomain.com itself). Use a subdomain name like app if you want app.yourdomain.com.
    • IPv4 address: your server's IP from earlier.
    • Proxy status: DNS only (gray cloud) for now. We'll switch this back to Proxied (orange cloud) after certbot runs — Let's Encrypt's HTTP-01 challenge can struggle through Cloudflare's proxy.
    • TTL: Auto.
  3. Save. Add a second record for www pointing to the same IP if you want both yourdomain.com and www.yourdomain.com to work.

8.3 Verify DNS

dig +short yourdomain.com
# should print your server's IP

curl -I http://yourdomain.com
# should return HTTP/1.1 200 OK from nginx

If dig shows nothing or the wrong IP, wait a few more minutes and try again. DNS caches can be sticky.

Other DNS hosts. Same idea, different UI: Route 53 → Hosted zones → your domain → Create record (Type A); DigitalOcean → Networking → Domains → A record; Namecheap → Domain List → Manage → Advanced DNS → Add new record (A Record). The data is always: type=A, host=@ (or subdomain), value=server-IP.

9. Add HTTPS with Let's Encrypt

Browsers shame plain HTTP. Let's Encrypt issues free certificates; certbot is the official client and auto-renews them every 60 days.

sudo apt install -y certbot python3-certbot-nginx

# Replace yourdomain.com with the actual hostname.
# Add -d www.yourdomain.com if you set up the www record too.
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot will:

Visit https://yourdomain.com — there's the lock icon. Run sudo certbot renew --dry-run to confirm auto-renewal works.

9.1 Re-enable Cloudflare proxy (optional)

If you want Cloudflare's CDN + DDoS protection, switch the A record back to Proxied (orange cloud) now. In Cloudflare, set SSL/TLS → Overview to Full (strict) — this makes Cloudflare verify your origin's Let's Encrypt cert. Don't use Flexible; it's a redirect loop trap.

10. Hand it to Magic Deploy

You now have a server at a real domain serving real HTTPS. The last step is wiring it into LingCode's Magic Deploy popover so a deploy is one click.

  1. In LingCode, click the Magic Deploy button in the toolbar.
  2. Under Saved servers, click + Add server.
  3. Fill in:
    • Host: yourdomain.com (or the IP — both work)
    • User: deploy (or ubuntu on AWS)
    • Port: 22
    • Auth: SSH key → pick ~/.ssh/id_ed25519 (or your AWS .pem)
    • Remote directory: /var/www/html for a static site, or /srv/<app> for a Node/Python app — your choice.
  4. Optional: paste before-launch and after-deploy scripts from the Scripts cookbook. For a static site: Before launch = npm run build (if applicable); After deploy = empty (nginx serves the files directly).
  5. Save. The new server appears in the popover. Tick its checkbox and click Deploy.

The popover shows progress: preflight → rsync → after-deploy. On success it prints the public URL. Subsequent deploys are one click.

11. When things go wrong

"Connection refused" on SSH

Either the server isn't running (check the provider dashboard), the firewall blocks port 22 (the cloud-side firewall on Lightsail/EC2 is separate from ufw — both must allow SSH), or you typed the wrong IP. Try nc -vz <ip> 22 from your laptop; if that fails, the firewall is the problem.

"Permission denied (publickey)"

Your local key isn't the one the server expects. ssh -v deploy@<ip> shows which keys are tried. If your key isn't on that list, run ssh-add ~/.ssh/id_ed25519. If the server doesn't have your public key, paste it manually via the provider's recovery console into ~deploy/.ssh/authorized_keys.

Domain doesn't resolve

dig +short yourdomain.com @1.1.1.1 queries Cloudflare's resolver directly, bypassing your local cache. If that shows the right IP but your browser doesn't, your laptop is caching — sudo dscacheutil -flushcache on Mac, then retry.

"403 Forbidden" or empty page after pointing DNS

nginx is serving but the document root is empty or has wrong permissions. Check ls -la /var/www/html. The owner should be deploy:deploy (or whatever user your deploy script writes as), and the files should be readable by www-data (group www-data on the directory works, or world-readable 0644 files inside a 0755 directory).

certbot fails: "DNS problem: NXDOMAIN" / "Failed authorization procedure"

Most common cause: Cloudflare proxy is on (orange cloud) and intercepts the HTTP-01 challenge. Switch to DNS only (gray cloud) and retry. Second most common: DNS hasn't fully propagated yet — dig from a few different resolvers and wait if results disagree.

certbot succeeds but browser still warns "Not secure"

Browser cache. Hard-refresh (⌘+Shift+R on Mac), or open in a private window. Check https://<yourdomain>/ with curl -vI — the cert chain should show "Let's Encrypt" as the issuer.

"This site can't be reached" but server is up

Provider-side firewall, again. Lightsail's IPv4 Firewall tab and EC2's Security Group are the most-forgotten settings. Both 80 and 443 must be open from 0.0.0.0/0 (anywhere) for public web traffic.

Have a server. Have a domain on it. Have HTTPS. Now drop in the right Before-launch + After-deploy scripts for your stack and you're done — every push to your repo is one Magic Deploy click away from production.

Related guides