Docker Health Checks for VPS Deployments in 2026

Start with a container that tells you when it is healthy
Docker health checks give you a clear signal that your app is ready, still working, and safe to keep behind a reverse proxy. On a small VPS, that matters because uptime problems often show up first as slow responses, failed migrations, or a container that is running but not serving traffic. If you are planning a fresh deployment on Hostperl VPS hosting, health checks are one of the simplest ways to catch those failures before your customers do.
This tutorial walks through a complete setup on a fresh server: SSH login, OS detection, non-root access, Docker installation, a sample app with health checks, Docker Compose, Nginx reverse proxying, firewall rules, and final verification. If you want background reading on deployment choices, Hostperl also has a practical Docker app deployment guide and a Git-Based Docker deployment guide that fit the same operational workflow.
What you will build
You will deploy a small web app in Docker with a proper HEALTHCHECK, publish it through Nginx, and confirm that unhealthy containers are visible from both Docker and the browser. The example uses:
- Ubuntu/Debian or AlmaLinux/Rocky Linux
- Docker Engine and Docker Compose
- A non-root admin user called
deploy - Application files in
/opt/myapp - Port
8000for the app and port80for Nginx
If your customer work involves staging sites, launch cutovers, or agency-managed sites, this approach is safer than running an app container without a health endpoint. It also pairs well with Hostperl’s migration and support workflows for teams that need predictable recovery, not just a container that starts once.
Connect to the VPS and check the operating system
On your local computer:
ssh root@203.0.113.10Replace 203.0.113.10 with the real public IP assigned to your server. This reserved address is only an example.
If your provider gives you a default non-root login, use it instead:
ssh deploy@203.0.113.10After you log in, detect the OS before you run distribution-specific commands:
cat /etc/os-releaseYou should see either Ubuntu/Debian fields or AlmaLinux/Rocky Linux fields. Keep the first root session open until the new sudo user login is tested.
Create a non-root sudo user
On the VPS as root:
Ubuntu and Debian
adduser deploySet a password when prompted, then grant sudo access:
usermod -aG sudo deployCreate the SSH directory, then copy your public key from the root account if one already exists:
mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keysAlmaLinux and Rocky Linux
useradd -m deploy
passwd deploy
usermod -aG wheel deploy
mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keysNow open a second terminal on your local computer and test the new login:
ssh deploy@203.0.113.10Then test sudo from the new session:
sudo -vIf that works, keep using the deploy account for the rest of the tutorial. Only after the new login is confirmed should you consider tightening root access later.
Update packages and install Docker
On the VPS as the non-root sudo user:
Ubuntu and Debian
sudo apt update
sudo apt -y upgrade
sudo apt -y install ca-certificates curl gnupgInstall Docker from the official repository so you get current packages and Compose support:
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
. /etc/os-release
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginAlmaLinux and Rocky Linux
sudo dnf -y update
sudo dnf -y install dnf-plugins-core ca-certificates curl
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginEnable Docker and confirm the version:
sudo systemctl enable --now docker
docker --version
docker compose versionYou should see Docker Engine and Docker Compose plugin versions. If Docker cannot start, check the service immediately with systemctl status docker before moving on.
Open the firewall before you publish the app
Do this before you remove any access path. You need SSH, HTTP, and later HTTPS.
Ubuntu and Debian with UFW
sudo apt -y install ufw
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verboseAlmaLinux and Rocky Linux with firewalld
sudo dnf -y install firewalld
sudo systemctl enable --now firewalld
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-allIf SELinux is enforcing on AlmaLinux or Rocky Linux, keep it that way for now. We will use standard ports, so you should not need a policy change.
Build the app with a health endpoint
For this tutorial, use a small Python app because it keeps the health logic easy to see. The same pattern works for Node.js, Java, or PHP apps that sit behind Nginx.
On the VPS as the non-root sudo user:
sudo mkdir -p /opt/myapp/app
sudo chown -R deploy:deploy /opt/myappCreate the application file:
cd /opt/myapp/app
cat > app.py <<'EOF'
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/health":
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok")
else:
self.send_response(200)
self.end_headers()
self.wfile.write(b"Hello from Docker health checks\n")
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
EOFCreate the Dockerfile with a health command:
cat > Dockerfile <<'EOF'
FROM python:3.12-slim
WORKDIR /app
COPY app.py /app/app.py
EXPOSE 8000
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 CMD curl -fsS http://127.0.0.1:8000/health || exit 1
CMD ["python", "/app/app.py"]
EOFCreate the Compose file:
cat > /opt/myapp/docker-compose.yml <<'EOF'
services:
app:
build: ./app
container_name: myapp
ports:
- "127.0.0.1:8000:8000"
restart: unless-stopped
EOFThe app only binds to 127.0.0.1. Nginx will be the public entry point.
Start the container and check health status
On the VPS as the non-root sudo user:
cd /opt/myapp
sudo docker compose up -d --buildCheck that Docker sees the container as healthy after a short delay:
sudo docker ps
sudo docker inspect --format='{{.State.Health.Status}}' myappYou want healthy. If you see starting, wait 10-20 seconds and run the inspect command again. If you see unhealthy, jump to the troubleshooting section later in this guide.
Confirm the app responds locally on the VPS:
curl -i http://127.0.0.1:8000/
curl -i http://127.0.0.1:8000/healthYou should get an HTTP 200 response and the body ok from the health path.
Put Nginx in front of the container
On the VPS as the non-root sudo user:
Ubuntu and Debian
sudo apt -y install nginxAlmaLinux and Rocky Linux
sudo dnf -y install nginxCreate a reverse proxy config that forwards traffic to the container:
sudo tee /etc/nginx/conf.d/myapp.conf > /dev/null <<'EOF'
server {
listen 80;
server_name server.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
EOFTest the configuration before you reload Nginx:
sudo nginx -tIf the syntax is correct, enable and reload the service:
sudo systemctl enable --now nginx
sudo systemctl reload nginxReplace server.example.com with your real hostname after DNS points to the VPS. For now, you can test with the server IP from the client side.
Add HTTPS with Let’s Encrypt after DNS is ready
Once your domain points to the server and port 80 is reachable, issue a certificate.
Ubuntu and Debian
sudo apt -y install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.comAlmaLinux and Rocky Linux
sudo dnf -y install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.comReplace example.com with your real domain. Certbot should update Nginx automatically and install a renewal timer. Confirm it with:
sudo certbot renew --dry-runFor hosting customers, this is the point where support tickets usually drop. Once HTTPS is live, browser warnings and redirect mistakes stop becoming launch blockers.
Test Docker health checks from the browser and the server
On the VPS:
sudo docker logs myapp --tail 50
sudo systemctl status nginx --no-pager
sudo ss -tulpn | grep -E ':80|:8000'On your local computer:
curl -I http://203.0.113.10
curl http://203.0.113.10/healthReplace 203.0.113.10 with your public IP if you are not using DNS yet. You should see a 200 response from Nginx and the ok body through the proxy.
If you are using a browser, load the site and confirm that the response comes from the container, not the default Nginx page.
Make the deployment survive a reboot
Restart the server only after you have confirmed the app, Nginx, and Docker are all running.
On the VPS as the non-root sudo user:
sudo rebootAfter the VPS comes back, log in again and run:
sudo systemctl status docker --no-pager
sudo systemctl status nginx --no-pager
cd /opt/myapp && sudo docker compose ps
sudo docker inspect --format='{{.State.Health.Status}}' myappYou want Docker and Nginx active, the container running, and the health state still healthy. That tells you the app will come back cleanly after a maintenance reboot.
If you want to run Docker workloads on a VPS without guessing about the platform underneath, Hostperl keeps the setup practical: fast provisioning, responsive support, and room to grow when the app stops being a side project. A Hostperl VPS is a good fit when you want full control, while teams with heavier traffic can move to a dedicated server later without changing the deployment pattern.
That matters for agencies, SaaS launch teams, and small businesses that need predictable recovery more than clever infrastructure.
Troubleshooting the most common failures
Container shows unhealthy
Run:
sudo docker inspect myapp --format='{{json .State.Health}}'
sudo docker logs myapp --tail 100If the health command cannot reach /health, check the app code and confirm port 8000 is listening inside the container.
Nginx returns 502 Bad Gateway
Check the proxy target and whether the container is alive:
sudo docker ps
curl -i http://127.0.0.1:8000/health
sudo tail -n 50 /var/log/nginx/error.logIf the local curl fails, restart the app container with sudo docker compose up -d --build.
Firewall blocks the site
Confirm the rules are active:
sudo ufw status verbose
sudo firewall-cmd --list-allOpen port 80 and 443 if needed, then retest from your computer.
Certbot cannot issue a certificate
Check DNS and port 80 reachability:
dig +short example.com
curl -I http://example.comIf DNS points elsewhere or port 80 is blocked, fix that first. Certbot will not complete until the server answers publicly.
When to use this setup
This pattern works well for client sites, internal tools, demo apps, and small SaaS deployments that need a simple reliability signal. It is also a good fit when you want support teams to diagnose problems quickly: Docker shows the container state, Nginx handles public traffic, and the health endpoint gives you a fast yes-or-no answer.
For more background on deployment choices, you may also find Hostperl’s reverse proxy guide for Node.js, Python, and Java apps useful, especially if your app is not Python-based. If you are comparing runtime and database planning, the Docker Compose deployment article is a solid next step.
FAQ
Do Docker health checks replace monitoring?
No. They tell Docker whether the container is healthy, but you should still monitor HTTP response, disk space, memory use, and restart counts.
Can I use this with Node.js or Java instead of Python?
Yes. Keep the same layout: app on localhost, Nginx on port 80 or 443, and a health endpoint that returns HTTP 200 quickly.
Should I expose port 8000 publicly?
No. This tutorial binds the app to 127.0.0.1 so only Nginx can reach it from outside.
How do I know the container will survive a reboot?
Use restart: unless-stopped, enable Docker and Nginx with systemd, and test a reboot before the site goes live.
What if I need to move this to a bigger server later?
The Compose file, Nginx config, and health endpoint all move cleanly. That makes upgrades easier when your traffic grows or you shift to a stronger Hostperl plan.
