Skip to content
All articles
8 min read

Uptime Kuma Setup: Free Website Monitoring on a VPS

MD Rakibul Islam RakibMD Rakibul Islam RakibFull-stack developer, DevOps & Linux engineer
Uptime Kuma Setup: Free Website Monitoring on a VPS

Uptime Kuma is a free, self-hosted monitor. Run louislam/uptime-kuma:2 in Docker on a second VPS, add HTTP and SSL checks, and get alerts on Telegram or email.

Most small businesses find out their website is down from a customer. That's the worst way to find out: the customer already left, and you don't know how long it's been broken. A monitor checks your site every minute and tells you within a minute or two. Uptime Kuma does that for free on your own server, with a clean dashboard, dozens of notification channels and a public status page. Here's how I set it up for clients, including the parts guides usually skip: where to run it, the WebSocket proxy settings, and push monitors for cron jobs and backups.

Key takeaways

  • Run the monitor somewhere else. A monitor on the same server as your site goes down with it and can't tell you anything.
  • Use the version 2 image (louislam/uptime-kuma:2). Version 2.0 came out in October 2025; v1 installs need a one-way database migration when upgrading.
  • Bind it to 127.0.0.1 and put Nginx with HTTPS in front. The dashboard uses WebSockets, so the proxy needs the Upgrade headers.
  • Monitor more than the homepage: a keyword check on a key page, the API health endpoint, SSL expiry, and push monitors for backups and cron jobs.
  • Test every notification channel before you trust it.

Where should Uptime Kuma run?

Not on the server it watches. If that server runs out of memory or loses its network, the monitor dies too and you get silence instead of an alert. Good options:

  • A small, separate VPS, ideally with a different provider or region from your main server. Uptime Kuma is light; the smallest plan most providers sell is enough for dozens of monitors.
  • A server you already have for something else, as long as it's not the one being monitored.
  • Two monitors watching each other if you run several servers: each one's Uptime Kuma checks the other.

Install Uptime Kuma with Docker Compose

On a fresh Ubuntu server with Docker installed, create a folder and a compose.yaml:

mkdir -p ~/uptime-kuma && cd ~/uptime-kuma
cat > compose.yaml <<'EOF'
services:
  uptime-kuma:
    image: louislam/uptime-kuma:2
    container_name: uptime-kuma
    restart: unless-stopped
    ports:
      - "127.0.0.1:3001:3001"
    volumes:
      - ./data:/app/data
EOF
docker compose up -d
docker compose logs -f --tail=50

Note 127.0.0.1:3001. If you publish the port as plain 3001:3001, Docker opens it to the whole internet even when UFW says it's blocked. I explained why in Docker bypasses UFW. The ./data folder holds the SQLite database with all your monitors and history, so it's the one thing to back up.

Put Nginx and HTTPS in front

Point a subdomain such as status.example.com at the monitoring server, then add an Nginx site:

server {
    server_name status.example.com;

    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Then sudo nginx -t && sudo systemctl reload nginx and get a certificate with sudo certbot --nginx -d status.example.com. Without the two WebSocket lines the dashboard loads but stays stuck on a spinner, which is the most common setup problem. Open the site, create the admin account straight away (the first visitor gets to create it), and turn on two-factor authentication in Settings → Security.

Which monitors to add

HTTP(s) with a keyword

A plain HTTP check passes as long as the server answers with a 2xx status. A broken app can still return 200 with an error page. Use the HTTP(s) - Keyword type and pick a word that only appears when the page really works, like your product name in the page footer or the "Add to cart" text. Check every 60 seconds and set retries to 2 or 3 so one slow response doesn't wake you up.

API health endpoint

If you have an API, monitor its health route separately (this site's API has /api/v1/health). The website can be up while the API behind it is down, and the error looks completely different to users.

SSL certificate expiry

In an HTTPS monitor's advanced settings, turn on certificate expiry notification. You'll get warned days before a certificate expires, which catches a broken Certbot renewal long before visitors see a browser warning. If that happens, my Certbot renewal fix guide covers the usual causes.

TCP port and ping

For things that aren't websites, such as a mail server on port 587 or a database that should only be reachable from your app server, a TCP port check tells you whether it's accepting connections.

Push monitors for cron jobs and backups

This is the most underused feature. A push monitor flips the direction: Uptime Kuma gives you a URL and expects it to be called on a schedule. If your nightly backup doesn't call it, you get an alert. Add a curl to the end of the job:

0 2 * * * /opt/scripts/backup.sh && curl -fsS -m 10 "https://status.example.com/api/push/AbC123?status=up&msg=OK" > /dev/null

Because of the &&, the ping only happens when the backup succeeds. Set the monitor's heartbeat interval slightly longer than the job's schedule. Backups that silently stopped months ago are a common discovery during an outage; this catches them the first night. If a cron job never runs at all, see why cron jobs don't run.

shop.example.com HTTP(s) - Keyword · every 60 s updown T Telegram · Uptime Kuma just now [shop.example.com] [🔴 Down] Keyword not found · retry 2/2 You hear about it from your phone, not from a customer.
Each bar is one check. When the keyword check fails past its retries, Uptime Kuma sends the alert straight away, usually within a minute or two of the problem starting.

Set up notifications that actually reach you

Under Settings → Notifications, add at least one channel and press Test. Popular choices:

  • Telegram: create a bot with @BotFather, paste the token and your chat ID. Fast and free; my default.
  • Email (SMTP): works with any mail provider. Fine for a second channel, but too easy to miss for urgent alerts.
  • Slack, Discord, Microsoft Teams: good when a team shares on-call duty.
  • ntfy or Pushover: phone push notifications that can override do-not-disturb.

Use the "Default enabled" and "Apply on all existing monitors" switches so new monitors don't end up with no alerts at all. Two channels are better than one: if the email provider has an outage, Telegram still works.

Add a public status page

Status Pages → New lets you publish a page with the monitors you choose. Point status.yourdomain.com at it and link it from your support page. During an incident it answers "is it just me?" for customers and cuts down support messages. You can post incident notes on it while you work on the fix.

Keep the monitor healthy

  • Back up the data folder. A nightly copy offsite is enough. My Linux backup guide shows how.
  • Update with docker compose pull && docker compose up -d. The :2 tag follows the latest 2.x release. Coming from v1, read the official v1 to v2 migration notes and back up first, because the database migration can't be undone.
  • Prune old data in Settings → Monitor History if the database grows large.
  • Watch the watcher: add the Uptime Kuma server itself as a monitor somewhere else, even a free external checker.

When an alert does fire, my website down troubleshooting checklist walks through the first ten minutes.

Frequently asked questions

Is Uptime Kuma free?

Yes. It's open source under the MIT licence and free to self-host. Your only cost is the small server it runs on.

How often should Uptime Kuma check my website?

Every 60 seconds with 2 or 3 retries is a good default for a business website. Shorter intervals add noise without helping much, and longer ones mean you hear about outages later.

Why is the Uptime Kuma dashboard stuck loading behind Nginx?

The dashboard uses WebSockets. Add proxy_http_version 1.1 and the Upgrade and Connection "upgrade" headers to the Nginx location that proxies to port 3001, then reload Nginx.

Can Uptime Kuma monitor cron jobs and backups?

Yes, with push monitors. The job calls a unique URL when it finishes successfully, and Uptime Kuma alerts you if the call doesn't arrive within the heartbeat interval.

Should I run Uptime Kuma on the same server as my website?

No. If that server goes down, the monitor goes down with it and can't send an alert. Run it on a separate server, ideally with a different provider.

Want monitoring set up for you?

I set up Uptime Kuma or another monitor on a separate server, with keyword checks, SSL expiry, backup heartbeats, a status page and tested alerts to your phone. See my server and website monitoring service, my Linux system admin services, or contact me with the sites and servers you want watched.

MD Rakibul Islam Rakib

Written by

MD Rakibul Islam Rakib

Full-stack developer, DevOps engineer and Linux system administrator with 5+ years of production experience. I deploy, harden and fix servers and web apps for clients worldwide, and everything in this article runs on real servers I manage, including this site.

  • Uptime Kuma
  • website monitoring
  • uptime monitoring
  • self-hosted monitoring
  • Docker Compose
  • push monitor
  • status page

Keep reading

Cloudflare in Front of Your VPS: Setup Checklist
DevOpsOct 7, 2026

Cloudflare in Front of Your VPS: Setup Checklist

Cloudflare in front of a VPS: use Full (strict) SSL, restore real visitor IPs in Nginx, and firewall the origin so only Cloudflare reaches ports 80 and 443. Cloudflare's free plan gives a small business site a global CDN, DDoS protection and free HTTPS at the edge, and setup looks like "change your nameservers, done". The problems show up later: an endless redirect loop, logs and rate limits that only ever see Cloudflare's IPs, attackers going around Cloudflare straight to the server's IP, or a checkout page cached for everyone. This is the checklist I use when I put a client's VPS behind Cloudflare, with the Nginx and UFW commands for Ubuntu. Key takeaways Use Full (strict), never Flexible. Flexible sends traffic to your server unencrypted and causes redirect loops with an HTTPS redirect on the origin. Give the origin a real certificate: Let's Encrypt or a free Cloudflare Origin CA certificate. Restore the visitor's IP with Nginx's real_ip module and the CF-Connecting-IP header, or fail2ban and rate limits see only Cloudflare. Lock the origin so ports 80 and 443 only accept Cloudflare's IP ranges, and don't leak the server IP through other DNS records. Cache static assets, not personal pages. Never cache HTML for logged-in users, carts or dashboards. 1. Add the site and check every DNS record When you add a domain, Cloudflare scans your existing DNS records. The scan misses things, so compare it against an export from your current DNS provider before switching nameservers: MX, TXT (SPF, DKIM, DMARC, verification records), CNAMEs for subdomains and any CAA records. Missing an MX record breaks your email the moment the nameservers change. Then decide which records are proxied (orange cloud) and which are DNS only (grey cloud): Proxied: the website and app hostnames: example.com , www , app . DNS only: mail servers (MX targets), anything using non-web ports like SSH or a database, and services that must see the client directly. Only HTTP and HTTPS on standard ports (plus a short list of alternates) go through the proxy on the free plan, so an SSH hostname must be grey-clouded. 2. Set SSL/TLS to Full (strict) This setting controls how Cloudflare talks to your server . The visitor always sees HTTPS either way, which is why the wrong choice can go unnoticed. Flexible: Cloudflare connects to your server over plain HTTP. Traffic between Cloudflare and your server is unencrypted, and if Nginx redirects HTTP to HTTPS you get ERR_TOO_MANY_REDIRECTS , because every request arrives as HTTP and gets redirected forever. Full: HTTPS to the origin, but any certificate is accepted, even expired or self-signed ones. Full (strict): HTTPS with a valid certificate checked. This is the one you want. Newer zones may show "Automatic SSL/TLS" instead, where Cloudflare picks the most secure mode your origin supports. That's fine as long as your origin has a valid certificate so it can choose Full (strict). You can check or override it under SSL/TLS → Overview. 3. Give the origin a certificate Two good options: Let's Encrypt with Certbot. The HTTP challenge works through the proxy in most setups. The more robust option is the DNS challenge with the Cloudflare plugin and a scoped API token (Zone → DNS → Edit for this zone only), which also works before DNS points at the server. Cloudflare Origin CA. Generate it under SSL/TLS → Origin Server, with a validity of up to 15 years, and install the certificate and key in Nginx. It's trusted by Cloudflare only, not by browsers, so it breaks if you ever switch a record to DNS only or pause Cloudflare. If renewals start failing later, my Certbot renewal guide covers the Cloudflare-specific causes. 4. Restore real visitor IPs in Nginx Behind the proxy, every request comes from a Cloudflare IP. Your access logs, fail2ban, rate limiting and any "block this IP" rule become useless, or worse, ban Cloudflare. Cloudflare sends the real IP in the CF-Connecting-IP header; Nginx's real IP module can use it, but only for requests from Cloudflare's ranges. Generate the config from Cloudflare's published lists: { for ip in $(curl -s https://www.cloudflare.com/ips-v4) $(curl -s https://www.cloudflare.com/ips-v6); do echo "set_real_ip_from $ip;" done echo "real_ip_header CF-Connecting-IP;" } | sudo tee /etc/nginx/conf.d/cloudflare-realip.conf sudo nginx -t && sudo systemctl reload nginx Reload a page and check the access log shows your own IP. Cloudflare's ranges change rarely, but they do change, so re-run this every few months or from a monthly cron job. Real traffic arrives through Cloudflare with the visitor's IP in a header Nginx can trust. Requests that skip Cloudflare and hit the server's IP directly are dropped by the firewall. 5. Lock the origin to Cloudflare Cloudflare only protects traffic that goes through it. If someone finds your server's IP, they can bypass the CDN, the WAF and DDoS protection completely. Allow web traffic only from Cloudflare's ranges with UFW: for ip in $(curl -s https://www.cloudflare.com/ips-v4) $(curl -s https://www.cloudflare.com/ips-v6); do sudo ufw allow proto tcp from "$ip" to any port 80,443 comment 'cloudflare' done sudo ufw delete allow 'Nginx Full' # or: sudo ufw delete allow 80,443/tcp sudo ufw status numbered Keep your SSH rule in place before changing anything. If any app runs in Docker with published ports, UFW rules don't apply to it; see Docker bypasses UFW . For stronger protection, enable Authenticated Origin Pulls so Nginx only accepts connections that present Cloudflare's client certificate. Also stop leaking the IP: don't point grey-clouded records like mail or ftp at the same server as the website, check that emails sent from the server don't reveal its IP in headers, and remember that old DNS history sites may still show your previous IP. After a serious attack, changing the server IP is sometimes the cleanest fix, and my zero-downtime migration guide shows how. 6. Cache the right things By default Cloudflare caches static files by extension (images, CSS, JS, fonts) and not HTML. That's a safe start. Then: Send good cache headers from your app. Next.js already marks /_next/static/ files as immutable; your own uploads and images should have a long Cache-Control too. Only cache HTML on purpose, with a cache rule for public pages, and bypass it for /admin , /account , /checkout , /api and any request with a session cookie. Purge after deploys if you cache HTML, or use short edge TTLs. Caching a personalised page is the most dangerous Cloudflare mistake: one customer's account page can be served to the next visitor. 7. Know the limits Upload size: the Free and Pro plans accept request bodies up to 100 MB. Larger uploads need a DNS-only hostname, chunked uploads or direct-to-storage uploads. Timeouts: if your server takes longer than about 125 seconds to respond, visitors see error 524. Long jobs should run in the background; my 504 Gateway Timeout guide explains how. WebSockets work through the proxy on all plans, so chat and live features keep working. 8. Turn on the useful free extras Always Use HTTPS and Automatic HTTPS Rewrites (you can then drop your own HTTP→HTTPS redirect, or keep it; with Full (strict) both are fine). HSTS , once you're sure every subdomain works over HTTPS. Bot Fight Mode and a WAF rule to challenge traffic to /wp-login.php or /admin from countries you don't serve. Under Attack mode : know where the switch is before you need it. Cloudflare is one layer. The server behind it still needs the basics in my Ubuntu hardening checklist . Frequently asked questions Why do I get "too many redirects" after enabling Cloudflare? Your SSL/TLS mode is probably Flexible while your server redirects HTTP to HTTPS. Cloudflare connects over HTTP, the server redirects to HTTPS, and the loop repeats. Install a certificate on the server and switch the mode to Full (strict). Should I use Flexible or Full SSL in Cloudflare? Full (strict). Flexible leaves traffic between Cloudflare and your server unencrypted and causes redirect loops. Full without strict accepts invalid certificates, so it protects less than it seems. How do I see real visitor IPs behind Cloudflare? Configure Nginx with set_real_ip_from for each Cloudflare IP range and real_ip_header CF-Connecting-IP , then reload. Logs, fail2ban and rate limits will then see the visitor's IP instead of Cloudflare's. Does Cloudflare hide my server's IP address? Only for proxied records, and only if the IP doesn't leak elsewhere: grey-clouded records on the same server, outgoing email headers, or old DNS history. Restrict ports 80 and 443 to Cloudflare's ranges so a leaked IP is useless. Is Cloudflare's free plan enough for a business website? For most small business sites, yes: CDN, free edge certificates, DDoS protection, basic WAF rules and caching are all included. Paid plans add more WAF features, image optimisation and support. Want Cloudflare and your server set up properly? I set up VPS servers with Nginx, HTTPS, Cloudflare, real-IP logging and a locked-down firewall, and deploy your app on them. See my VPS setup service , the server hardening and security audit , all DevOps services , or contact me with your domain and hosting details.

Read article →
Landing Page That Converts: 11 Things to Get Right
Website DesignOct 7, 2026

Landing Page That Converts: 11 Things to Get Right

A landing page converts when it has one goal, a headline that names the outcome, proof near the button, answers to objections, and loads in under 2.5 seconds. Most landing pages I'm asked to fix don't have a design problem. They have a clarity problem: three different buttons, a headline about the company instead of the customer, testimonials hidden at the bottom, and a hero image so heavy the page takes five seconds to show anything on a phone. Visitors decide fast whether a page is for them. This checklist covers the 11 things I check on every landing page I build or review, in the order a visitor experiences them. Key takeaways One page, one goal, one main button. Every extra choice lowers the chance of the one you want. The headline says what the visitor gets , for whom, in plain words. Your company name can wait. Put proof next to the ask: real reviews, client names, numbers you can back up. Answer objections on the page with a short FAQ: price, time, risk, what happens next. Speed is part of conversion. Aim for a Largest Contentful Paint under 2.5 seconds on mobile, and measure conversions so you know what works. Above the fold: the first five seconds 1. Pick one goal Decide the single action the page exists for: book a call, request a quote, start a trial, buy. Remove the main menu if you can, or at least keep it minimal, and make every button on the page do that one thing. A landing page from an ad with links to your blog, careers page and social profiles is leaking visitors. 2. Write a headline about the outcome Compare "Innovative Cloud Solutions" with "Your website back online in hours, not days." The second tells the visitor what they get. A good formula is outcome + for whom + without the pain : "Bookkeeping for small cafés, done in a day a month." Under it, one sentence explains how. 3. Make the button say what happens "Submit" and "Learn more" are weak. "Get my free quote", "Book a 15-minute call" or "Start fixing my site" set expectations. Show the main button above the fold on mobile, give it the strongest colour on the page, and repeat it after each major section. 4. Show the product or the result A real screenshot, a short demo, a before-and-after or a photo of your actual work beats an abstract illustration. Visitors want to see what they're buying. Size it properly: this image is often the page's Largest Contentful Paint, so it must be compressed and loaded first. Each section answers the next question in a visitor's head, in order. The same single button appears at the top and again at the end, after the objections are answered. Below the fold: earning the click 5. Proof, right where the decision happens Put your strongest proof immediately under the hero and again near the final button: short reviews with real names, client logos (with permission), a project count or rating you can back up, and case results. Specific beats glowing: "Fixed our checkout in a day, sales came back the same evening" persuades more than "Amazing service!". Never invent testimonials or numbers. Besides being dishonest, fake reviews break consumer protection rules in many countries, and visitors can usually tell. 6. Benefits first, features second Three short blocks, each a benefit in the customer's words with the feature that makes it true underneath. "Never lose a booking" (benefit) → "automatic SMS reminders 24 hours before" (feature). 7. Show how it works in three steps People hesitate when the next step is unclear. "1. Tell us about your project. 2. Get a fixed quote within a day. 3. We build it, you approve each milestone." Three steps make the decision feel small. 8. Answer objections with a short FAQ List the questions you hear on sales calls: How much does it cost? How long does it take? What if I'm not happy? Do I need to prepare anything? Answer each in two or three sentences. A FAQ also gives Google and AI answer engines clear question-and-answer text to show, which brings more qualified visitors. 9. Keep the form short Every field you add is a reason to leave. For a first contact, name, email and one open question ("What do you need help with?") is usually enough. Ask for budget, phone and company size later, or make them optional. Show what happens after submitting: "I reply within one working day." Under the hood: speed, mobile and measurement 10. Make it fast on a phone Google treats a Largest Contentful Paint under 2.5 seconds as good, and slow pages lose visitors before they've read the headline. Compress the hero image and load it with high priority, use at most two font families, and drop heavy sliders, chat widgets and tracking scripts you don't use. My guide to fixing a slow LCP walks through each step. Then test on a real phone: tap targets big enough, no text too small, no sideways scrolling, the form easy to fill with a thumb. 11. Measure conversions, then change one thing at a time Track the button clicks and form submissions as conversions (key events in Google Analytics 4, or your ad platform's conversion tracking). Without that, you're redesigning on opinions. Once you have a baseline, test one change at a time, headline first, because it has the biggest effect. Small sites rarely have the traffic for formal A/B tests; changing the headline for two weeks and comparing the conversion rate is still far better than guessing. Website builders or a custom page? Builders like Webflow, Framer and Wix can produce a good landing page if you follow the checklist above, and they're quick to edit. A custom page built in Next.js gives you full control over speed, tracking and integration with your app or CRM. I compared both in AI website builders vs a custom website . If you're replacing an existing site, read how to redesign without losing SEO first. Frequently asked questions What makes a landing page convert? A clear single goal, a headline that states the outcome for a specific visitor, one obvious button, proof near that button, answers to common objections, a short form, and a page that loads quickly on mobile. How long should a landing page be? As long as it takes to answer the visitor's questions. A free download can be short. An expensive service needs more proof and more answers. Keep the main button visible at the top and repeat it after each major section so long pages don't hide it. Should a landing page have a navigation menu? For ad and campaign pages, usually not, or only a minimal one, because every link away from the page is a lost conversion. A service page on your main website can keep the normal menu. How many form fields should a landing page form have? As few as you need to start the conversation, often three: name, email and a short message. Extra qualifying questions can be optional or asked in the follow-up. How do I know if my landing page is working? Track conversions as events in your analytics and divide them by the number of visitors to get a conversion rate. Compare it over time and after each change, rather than judging by how the page looks. Want a landing page built to convert? I design and build fast landing pages with clear copy structure, real proof, conversion tracking and a sub-2.5-second load on mobile. See my landing page design and build service , all website design services , or contact me with your current page and the one action you want visitors to take.

Read article →
Migrate a Website to a New Server With Zero Downtime
DevOpsOct 7, 2026

Migrate a Website to a New Server With Zero Downtime

Move a site to a new server with zero downtime: lower the DNS TTL early, copy and test, do a final data sync, switch DNS and keep the old server running. Server migrations have a bad reputation because the usual approach is "copy everything, change DNS, hope". Then emails stop arriving, uploads from the last hour are missing, the SSL certificate isn't there yet and half the visitors still land on the old server. I move client sites between providers regularly, often to cut hosting costs or get off an old, unpatched machine, and the plan below is how I do it without customers noticing. It works for Node.js, PHP and WordPress sites alike. Key takeaways Lower the DNS TTL to 300 seconds at least a day before the move, so the switch spreads in minutes instead of hours. Inventory everything first: sites, databases, uploads, cron jobs, environment files, email and every DNS record. Test the new server before switching DNS with curl --resolve or your hosts file. Sync data twice: a full copy while the old server is live, then a short final sync during a brief write freeze. Keep the old server running for a week as your rollback plan, and only then cancel it. Step 1: Inventory the old server Most migration problems are things nobody remembered existed. Before touching anything, write down: Sites and apps: ls /etc/nginx/sites-enabled/ (or Apache's), pm2 list , docker ps , systemctl list-units --type=service --state=running . Databases: names, sizes, users and versions. Moving PostgreSQL 14 to 17 or MySQL 5.7 to 8 is an upgrade too; test it. Files: app code, user uploads, generated files, and the .env or config files with secrets. Scheduled jobs: crontab -l for every user, sudo crontab -l , /etc/cron.d/ and systemd timers. DNS: export every record from your DNS provider. Note MX, SPF, DKIM and DMARC records, and any subdomains pointing at the old IP. Email: does the server send mail (contact forms, receipts) directly or through a provider? Does it receive mail? Things that trust the old IP: payment provider webhooks, API allowlists, a database firewall, an SMS gateway, a partner's IP whitelist. Step 2: Lower the DNS TTL The TTL tells resolvers how long to cache your DNS record. If it's 86400 (a day), some visitors keep going to the old server for up to a day after you switch. Change the TTL of the A/AAAA records you'll move to 300 seconds, then wait at least as long as the old TTL before migrating so every cache has picked up the short value. Check what the world sees with: dig +noall +answer www.example.com The number before IN A is the remaining TTL. If you use Cloudflare's proxy (orange cloud), the switch is near-instant anyway, because visitors only ever see Cloudflare's IPs. I cover that setup in the Cloudflare VPS checklist . Step 3: Build and secure the new server Set up the new server properly from the start rather than copying the old one's mistakes: a non-root sudo user, SSH keys only, a firewall, automatic security updates. My Ubuntu 24.04 hardening checklist is the list I follow. Install the same major versions of your runtime (Node, PHP, Python) and database, or plan and test the upgrade. Then set up Nginx and the app the way I describe in deploying Next.js on a VPS with PM2 and Nginx . Step 4: First full copy of files and databases Copy files with rsync over SSH. It preserves permissions and can be re-run later to copy only what changed: rsync -aHAX --info=progress2 \ old-server:/srv/app/shared/uploads/ /srv/app/shared/uploads/ For databases, take a logical dump and restore it on the new server: # PostgreSQL ssh old-server "pg_dump -U app -Fc appdb" > appdb.dump pg_restore -U app -d appdb --no-owner appdb.dump # MySQL / MariaDB ssh old-server "mysqldump --single-transaction appdb" | mysql appdb Copy the .env files and cron jobs, and point the app's config at the new database. Don't enable cron jobs that send emails or charge cards yet, or customers get everything twice. Step 5: Test the new server before anyone sees it You need HTTPS working before the DNS switch, but Let's Encrypt's normal HTTP check needs DNS to already point at the new server. Three ways around it: copy /etc/letsencrypt from the old server, use a DNS-01 challenge with your DNS provider's plugin, or use a Cloudflare Origin CA certificate if you're behind Cloudflare. Then test the real domain against the new IP without changing DNS: curl -sI --resolve www.example.com:443:203.0.113.20 https://www.example.com/ For clicking around in a browser, add 203.0.113.20 www.example.com to your computer's hosts file temporarily. Log in, submit forms, upload a file, run a test payment, check the logs for errors. Remove the hosts entry when you're done. With a short TTL set in advance, changing the A record moves visitors to the new server within minutes. The old server stays up, so anyone still on a cached record is served normally. Step 6: Final sync and the switch The only moment data can be lost is between the last copy and the DNS switch. Keep it short: Pick a quiet time and tell the client or team. Freeze writes on the old server: a maintenance page for logged-in actions, or stop the app's workers, so no new orders or uploads land there. Final sync: re-run the same rsync (fast, only changes) and take a fresh database dump and restore. Switch DNS to the new IP. Enable cron jobs and workers on the new server, and disable them on the old one. Watch both servers' access logs. Traffic on the old one should drop to almost nothing within minutes. For a site where even a few minutes of frozen writes is too much, you can set up database replication from old to new and promote the new one at switch time. It's more work and only worth it for busy stores and apps. Step 7: After the switch Update everything that trusted the old IP: webhooks, allowlists, SPF records if the server sends mail directly. Check email both ways. Send a contact-form test and a real email to the domain. Check renewals: run sudo certbot renew --dry-run on the new server. Set up backups and monitoring on the new server on day one, not "later". Keep the old server for about a week, powered on, as a rollback. Take a final snapshot before cancelling it. Raise the TTL back to an hour or more once everything is stable. Frequently asked questions How long does it take to migrate a website to a new server? Preparing, copying and testing usually takes a few hours to a couple of days, depending on the size of the data and the number of services. The actual switch, with a short TTL and a final sync, takes minutes. Will my website go down during migration? Not if you test the new server first, lower the TTL in advance and keep the old server running. Visitors with cached DNS still reach the old server, which keeps working until their cache expires. How do I get an SSL certificate before changing DNS? Copy the existing Let's Encrypt files from the old server, use a DNS-01 challenge through your DNS provider, or use a Cloudflare Origin CA certificate if your site is behind Cloudflare. The normal HTTP challenge only works after DNS points at the new server. How long does DNS propagation take? As long as the record's TTL. With a TTL of 300 seconds set a day ahead, most visitors move within five to ten minutes. A few badly behaved resolvers cache longer, which is why the old server should stay up for a while. Can I move from shared hosting to a VPS this way? Yes. Download files over SFTP or the host's backup tool, export the database from phpMyAdmin or the control panel, and follow the same test-then-switch steps. Pay special attention to email, which shared hosting often provides and a plain VPS doesn't. Want your site moved for you? I migrate websites and apps between servers and providers, including off Vercel, Heroku and shared hosting, with testing before the switch and the old server kept as a fallback. See my hosting migration service , the VPS setup service , all DevOps services , or contact me with what you're running now and where you want it.

Read article →