Install
openclaw skills install vps-deployDeploy a full-stack app to any VPS from zero to production in one command. Handles SSH hardening, firewall, Docker, Nginx reverse proxy, SSL certificates, and health verification. Works with any stack (Node.js, Python, Go, Next.js) and any VPS provider (Hostinger, DigitalOcean, Hetzner, Linode, Vultr). Use when the user says 'deploy to VPS', 'set up my server', 'deploy to production', 'configure my VPS', or needs to go from a bare Ubuntu/Debian server to a running production app.
openclaw skills install vps-deployDeploy any application to any VPS — from bare server to production with SSL — in one session.
/k8s-deploy if it exists)git push)Ask the user for (skip what you can detect):
package.json (Node.js/Next.js)requirements.txt / pyproject.toml (Python)go.mod (Go)Dockerfile (any).env.local, .env.example)Run these via ssh root@<IP> commands. Chain with && for safety.
apt update && apt upgrade -y
adduser --disabled-password --gecos "" deploy
usermod -aG sudo docker deploy
echo "deploy ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/deploy
# Copy root's authorized_keys to deploy user
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/ 2>/dev/null || true
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys 2>/dev/null || true
# Harden SSH config
sed -i 's/#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
systemctl restart sshd
CRITICAL: Before disabling root login, verify the deploy user can SSH in. Test in a separate connection. If the user doesn't have SSH keys set up, help them first.
apt install -y ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp # SSH
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
ufw --force enable
curl -fsSL https://get.docker.com | sh
usermod -aG docker deploy
apt install -y docker-compose-plugin
# Verify
docker compose version
Detect the stack and generate an appropriate multi-stage Dockerfile:
Node.js / Next.js:
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
Python (FastAPI/Flask):
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Adapt based on what you detect in the project.
Generate a production-grade compose file. Include:
services:
app:
build: .
restart: unless-stopped
ports:
- "127.0.0.1:${APP_PORT:-3000}:${APP_PORT:-3000}"
env_file: .env
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:${APP_PORT:-3000}/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# Add database services based on detection:
# postgres:
# image: postgres:16-alpine
# restart: unless-stopped
# volumes:
# - pgdata:/var/lib/postgresql/data
# environment:
# POSTGRES_DB: ${DB_NAME:-app}
# POSTGRES_USER: ${DB_USER:-app}
# POSTGRES_PASSWORD: ${DB_PASSWORD}
# healthcheck:
# test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-app}"]
# interval: 10s
# timeout: 5s
# retries: 5
# volumes:
# pgdata:
Key rules:
127.0.0.1 (Nginx handles external traffic)restart: unless-stopped# Create app directory
ssh deploy@<IP> "mkdir -p ~/apps/<app-name>"
# Copy project files (exclude node_modules, .git, etc.)
rsync -avz --exclude='node_modules' --exclude='.git' --exclude='.next' \
./ deploy@<IP>:~/apps/<app-name>/
ssh deploy@<IP> "cd ~/apps/<app-name> && docker compose up -d --build"
apt install -y nginx
# Generate site config
cat > /etc/nginx/sites-available/<domain> << 'EOF'
server {
listen 80;
server_name <domain>;
location / {
proxy_pass http://127.0.0.1:<APP_PORT>;
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-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400;
}
}
EOF
ln -sf /etc/nginx/sites-available/<domain> /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
Skip if no domain provided.
apt install -y certbot python3-certbot-nginx
certbot --nginx -d <domain> --non-interactive --agree-tos -m <email>
# Auto-renewal is configured automatically by certbot
Run these checks and report results:
# Check Docker containers are running
docker compose ps
# Check health status
docker inspect --format='{{.State.Health.Status}}' <container-name>
# Check Nginx is serving
curl -sI https://<domain> | head -5
# Check SSL certificate
echo | openssl s_client -servername <domain> -connect <domain>:443 2>/dev/null | openssl x509 -noout -dates
Report to the user:
nginx -t before reloading Nginxlsof -i :<port> to find what's using itdig <domain>)docker compose logs -f to show logsTell the user to:
/ci-gen skill)