The uptime field guide
Keep your website steady.
Learn to prevent common outages, recover faster, and measure the result. Five practical courses take you from a single server to a more resilient deployment.
01 / VPSKeep a VPS running
Service recovery, capacity, and safe releases
Before you start: An Ubuntu server using systemd, sudo access, and a working application. The example assumes your executable is at /opt/my-web/server; adapt paths and the start command to your app.
1. Give the process a supervisor
Create a dedicated webapp service account, give it access to the app and its data directory, and put your runtime settings in /etc/my-web.env. Save this as /etc/systemd/system/my-web.service:
[Unit]
Description=My web application
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=exec
User=webapp
WorkingDirectory=/opt/my-web
EnvironmentFile=/etc/my-web.env
ExecStart=/opt/my-web/server
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now my-web
sudo systemctl status my-web
sudo journalctl -u my-web -n 50 --no-pager
The restart policy recovers a crashed process. A process that hangs while still running needs a health check and an alert. Fix repeated crashes instead of hiding them in a restart loop. systemd service reference
2. Watch the limits before they cause an outage
Check df -h, df -i, free -h, and uptime. Investigate growing logs, exhausted inodes, memory pressure, and slow responses. Set capacity alerts with enough headroom to act, rotate logs, and schedule operating system updates and required reboots.
3. Make every release reversible
Keep the previous release and a tested data backup. Validate configuration, deploy to staging, then check a meaningful health endpoint before directing traffic to the new version. Plan database migrations so the previous app can still run, or prepare a separate recovery procedure.
02 / CloudDesign for failure
Health checks, redundancy, and recovery
Before you start: A cloud account and a staging application. Use your provider's equivalent load balancer, availability zones, metrics, and backup tools; check costs before creating resources.
1. Find your single points of failure
Draw the request path: DNS → load balancer → app → database. Identify which component can bring the whole site down. Where the availability target justifies it, use at least two app instances in separate failure zones, health checks that remove unhealthy instances, and enough spare capacity to lose one instance.
2. Separate app recovery from data recovery
Keep sessions and uploads out of disposable app disks. Choose storage with a recovery plan, encrypted backups, and restore tests. Multiple app replicas do not make a single database highly available. This app's SQLite file belongs on persistent local storage; do not put one WAL database on a shared network filesystem to connect replicas.
Set a recovery time objective (how long recovery can take) and a recovery point objective (how much recent data can be lost). Measure both during a restore drill. See the AWS reliability guidance and SQLite WAL constraints.
3. Verify the path your visitors use
Monitor the public hostname from outside the deployment. A cached landing page can stay green while the app fails: use a non-cached health route that reflects the dependencies you need. Watch latency and errors during gradual releases, and roll back when your thresholds are crossed.
03 / Self hostingMake home hosting reliable
Power, containers, and tested backups
Before you start: A home server or small dedicated machine, access to its router, and persistent storage for application data.
1. Include the network and the power supply
Reserve the server's LAN address, prefer wired networking, and plan for power loss. A UPS should cover the server and network equipment, with a tested graceful shutdown. Check ISP restrictions and CGNAT; an outbound tunnel can publish a service without router port forwarding. Keep an external monitor so a local power or Internet outage can still be reported.
2. Restart the app without losing its data
For Docker Compose, use a persistent volume for data and an appropriate restart policy. Uptime Medics already includes both in its Compose file. Check the stack and its logs:
docker compose up -d
docker compose ps
docker compose logs --tail=100 uptime-medics
restart: unless-stopped restarts a container after it exits, unless it was deliberately stopped. A Docker health check marks a container unhealthy; it does not itself restart a running container. Investigate that state and add supervised recovery if needed. Docker restart policy reference
3. Prove that backups can restore service
Back up configuration, secrets, and database data to a separate machine with restricted access. For a live SQLite database, use its backup mechanism rather than copying just the main file while WAL writes are active. On a host with the SQLite CLI and access to the database, adapt these paths:
sqlite3 /path/to/uptime_medics.db ".backup '/path/to/backup.db'"
sqlite3 /path/to/backup.db "PRAGMA integrity_check;"
A successful integrity check should return ok. Also restore the backup into a separate test instance and confirm that monitors and history are present. For the Compose named volume, run the backup from a tool with access to that volume. SQLite backup reference
04 / NetworkingSet up Cloudflare Tunnel
Publish an app on a VPS or home server
Before you start: A domain managed in Cloudflare, a Linux server with sudo access, and an app reachable at http://127.0.0.1:8080 from that server. Here, cloudflared runs on the host.
1. Create a persistent tunnel
In Cloudflare's dashboard, open Networking → Tunnels, create a tunnel, and select your server's operating system and architecture. Run the provided cloudflared installation and service commands. Keep the tunnel token private. Confirm that the connector becomes healthy. Cloudflare setup instructions
curl --fail http://127.0.0.1:8080/api/health
sudo systemctl status cloudflared
sudo journalctl -u cloudflared -n 50 --no-pager
2. Route your hostname to the app
Under the tunnel's Routes, add a Published application. Map status.example.com to http://127.0.0.1:8080, replacing the example hostname with your own. This gives visitors an HTTPS URL. A temporary Quick Tunnel is for development; use your named tunnel for a stable hostname.
Bind the app's host port to loopback when only cloudflared needs it. For Docker, publish 127.0.0.1:8080:8080. If cloudflared runs inside a container, use the app's service name on a shared Docker network instead of localhost.
3. Keep the connector and origin available
Allow outbound TCP/UDP port 7844 to Cloudflare. Preserve your management access when changing firewall rules. For an HTTPS origin, configure the correct origin certificate name and keep certificate verification enabled. A second connector on another host improves connector resilience only if it can also reach a working app. Tunnel availability and firewall guide
Set this app's PUBLIC_BASE_URL to the public HTTPS URL and restart it. Check that monitoring traffic can reach the health route without an unexpected login page or cache. Protect private applications with an access policy suited to their users.
https://status.example.com/api/health from another network. Reboot a staging host, verify the tunnel reconnects automatically, and inspect the 24-hour history. If you see a 502, check the app's local address, port, and logs first.05 / HTTPSNginx + Certbot on a VPS
Serve a public HTTPS site and test certificate renewal
Before you start: A fresh Ubuntu VPS with sudo access and snapd installed, a domain, and an app listening at 127.0.0.1:8080. Replace status.example.com throughout. This course uses direct DNS to the VPS; the tunnel course is another way to publish your app.
1. Prepare DNS and the reverse proxy
Point the domain's A record to the VPS; add an AAAA record only if IPv6 is configured. For this setup, use DNS-only mode if the zone is on Cloudflare. Allow inbound TCP 80 and 443 through both the provider firewall and the host firewall, keeping SSH access. Bind the application port to loopback.
sudo apt update
sudo apt install nginx
sudo systemctl enable --now nginx
Save the following as /etc/nginx/sites-available/my-web:
server {
listen 80;
server_name status.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
For IPv6, also add listen [::]:80;. The forwarded client address above assumes Nginx is the only public proxy. Enable the site, validate it, and reload only when validation succeeds:
sudo ln -s /etc/nginx/sites-available/my-web /etc/nginx/sites-enabled/my-web
sudo nginx -t && sudo systemctl reload nginx
curl --fail http://status.example.com/api/health
A reload lets existing requests finish. If validation fails, correct the configuration before applying it. Nginx reverse proxy guide
2. Issue a trusted HTTPS certificate
On this fresh server, install Certbot through snap and use its Nginx plugin. If Certbot is already installed another way, follow its migration instructions first.
sudo snap install --classic certbot
sudo /snap/bin/certbot --nginx --redirect -d status.example.com
curl --fail https://status.example.com/api/health
Follow the email and terms prompts. Certbot updates the matching Nginx server block. Keep port 80 reachable for HTTP-01 renewals. When inbound port 80 cannot be used, choose an automated DNS-01 integration instead. Set Uptime Medics' PUBLIC_BASE_URL=https://status.example.com and restart the app. Ubuntu certificate setup and renewal
3. Test renewal and monitor the result
sudo systemctl status snap.certbot.renew.timer
sudo /snap/bin/certbot renew --dry-run
sudo nginx -t
Confirm the timer is active and the simulated renewal succeeds. If it fails, inspect /var/log/letsencrypt/letsencrypt.log, DNS, and firewall access. Repeat the test after DNS or proxy changes, and monitor the public HTTPS endpoint.