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 always for auto-recovery after crashes and reboots.
  • --cap-drop ALL --cap-add NET_BIND_SERVICE to strip privileges.
  • HTTPS via Let's Encrypt (or Caddy if you want it automatic).
  • cardinal port add / cardinal port remove to change published ports on a

running container without recreating it.

  • Keep images updated with cardinal update.