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, an...
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)