Deploy a website with nginx
Serve a static site or a web app behind nginx — from a one-line container to HTTPS with free Let's Encrypt certificates.
Serving a website is the most common first container. This guide covers the
full ladder: a static site on nginx, a custom config with a reverse proxy,
and HTTPS with Let's Encrypt.
1. Static site (nginx)
Put your files in a directory and mount them into nginx:
mkdir -p /data/www/mysite
echo "<h1>Hello from cardinal!</h1>" > /data/www/mysite/index.html
cardinal run -d --restart always \
-n mysite -p 80:80 \
-v /data/www/mysite:/usr/share/nginx/html \
--memory 256m --cpus 0.5 --disk 1G \
nginx:alpine
curl http://localhost:80
Edits to /data/www/mysite are served instantly — no rebuild, no restart.
2. Custom nginx config
For a server name, a reverse proxy, or custom locations, mount a config
directory:
mkdir -p /data/nginx-conf
cat > /data/nginx-conf/default.conf << 'EOF'
server {
listen 80;
server_name mysite.example.com;
root /usr/share/nginx/html;
index index.html;
location / { try_files $uri $uri/ =404; }
location /api/ { proxy_pass http://backend:3000; }
}
EOF
cardinal run -d --restart always \
-n mysite -p 80:80 \
-v /data/www/mysite:/usr/share/nginx/html \
-v /data/nginx-conf:/etc/nginx/conf.d \
nginx:alpine
3. HTTPS with Let's Encrypt
Get a certificate on the host, then mount both the certs and the SSL config:
apt-get install -y certbot
certbot certonly --standalone -d mysite.example.com
mkdir -p /data/nginx-ssl
cat > /data/nginx-ssl/default.conf << 'EOF'
server {
listen 80;
server_name mysite.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name mysite.example.com;
ssl_certificate /etc/letsencrypt/live/mysite.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mysite.example.com/privkey.pem;
root /usr/share/nginx/html;
index index.html;
}
EOF
cardinal run -d --restart always \
-n mysite -p 80:80 -p 443:443 \
-v /data/www/mysite:/usr/share/nginx/html \
-v /data/nginx-ssl:/etc/nginx/conf.d \
-v /data/letsencrypt:/etc/letsencrypt \
nginx:alpine
The -v /data/letsencrypt:/etc/letsencrypt mount keeps the certbot renewal
renewal files visible to nginx without rebuilding.
4. Python or Node apps behind nginx
An app container needs no host port — nginx reaches it over the bridge:
# Flask app (no -p needed — accessed via nginx)
cardinal run -d --restart always \
-n flask-backend \
-v /data/flask-app:/app \
--workdir /app \
--startup "pip install -r /app/requirements.txt && gunicorn -w 4 -b 0.0.0.0:5000 app:app" \
python:3.11-slim
# Find its bridge IP, then point nginx at it
cardinal inspect flask-backend | grep IP
The same pattern works for Express (node:20 with
--startup "npm install && npm start"), FastAPI (uvicorn), Django, and
Go. Each cardinal run mounts the project live, so deploying is editing
files and restarting.
5. Resource limits
Add --memory 512m --cpus 0.5 --disk 2G to any of the commands above so a
runaway app can't exhaust the VPS. Good defaults: sites 512m/0.5, bots
256m/0.25, databases 1g/1.
6. Production checklist
--restart alwaysfor auto-recovery after crashes and reboots.--cap-drop ALL --cap-add NET_BIND_SERVICEto strip privileges.- HTTPS via Let's Encrypt (or Caddy if you want it automatic).
cardinal port add/cardinal port removeto change published ports on a
running container without recreating it.
- Keep images updated with
cardinal update.