vps-deploy
# VPS Deploy
Deploy any application to any VPS — from bare server to production with SSL — in one session.
## When to Use
- User has a VPS (Ubuntu/Debian) and wants to deploy an application
- User says "deploy to VPS", "set up my server", "go to production"
- User has a running app locally and wants it live on a server
- User needs SSL, Nginx, Docker setup on a VPS
## When NOT to Use
- Deploying to Vercel, Netlify, or other managed platforms (use their CLIs)
- Deploying to Kubernetes (use `/k8s-deploy` if it exists)
- The user just wants to push code (use `git push`)
## Prerequisites
- SSH access to a VPS (IP address + root or sudo credentials)
- A domain pointed to the VPS IP (for SSL — can skip SSL if no domain)
- The app must have a Dockerfile or be deployable via Docker
## Execution Flow
### Phase 1: Gather Information
Ask the user for (skip what you can detect):
1. **VPS IP address** and **SSH credentials** (root password or key path)
2. **Domain name** (optional — needed for SSL)
3. **Application type** — detect from the current directory if possible:
- Look for `package.json` (Node.js/Next.js)
- Look for `requirements.txt` / `pyproject.toml` (Python)
- Look for `go.mod` (Go)
- Look for `Dockerfile` (any)
4. **Port** the app runs on (detect from Dockerfile EXPOSE, or ask)
5. **Environment variables** needed (check `.env.local`, `.env.example`)
6. **Database requirements** (Postgres, MySQL, Redis, MongoDB — detect from dependencies)
### Phase 2: Server Setup (SSH into VPS)
Run these via `ssh root@<IP>` commands. Chain with `&&` for safety.
#### 2a. System Update
```bash
apt update && apt upgrade -y
```
#### 2b. Create Deploy User
```bash
adduser --disabled-password --gecos "" deploy
usermod -aG sudo docker deploy
echo "deploy ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/deploy
```
#### 2c. SSH Hardening
```bash
# 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.
#### 2d. Firewall (UFW)
```bash
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
```
#### 2e. Install Docker
```bash
curl -fsSL https://get.docker.com | sh
usermod -aG docker deploy
```
#### 2f. Install Docker Compose (v2)
```bash
apt install -y docker-compose-plugin
# Verify
docker compose version
```
### Phase 3: Application Deployment
#### 3a. Generate Dockerfile (if none exists)
Detect the stack and generate an appropriate multi-stage Dockerfile:
**Node.js / Next.js:**
```dockerfile
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):**
```dockerfile
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.
#### 3b. Generate docker-compose.yml
Generate a **production-grade** compose file. Include:
```yaml
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:
- **Always** bind app port to `127.0.0.1` (Nginx handles external traffic)
- **Always** include health checks
- **Always** include resource limits
- **Always** include logging limits
- **Always** use `restart: unless-stopped`
- **Never** expose database ports to the host
#### 3c. Transfer Files to VPS
```bash
# 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>/
```
#### 3d. Build and Start
```bash
ssh deploy@<IP> "cd ~/apps/<app-name> && docker compose up -d --build"
```
### Phase 4: Nginx Reverse Proxy
```bash
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
```
### Phase 5: SSL Certificate (Let's Encrypt)
Skip if no domain provided.
```bash
apt install -y certbot python3-certbot-nginx
certbot --nginx -d <domain> --non-interactive --agree-tos -m <email>
# Auto-renewal is configured automatically by certbot
```
### Phase 6: Verification
Run these checks and report results:
```bash
# 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:
- Container status (running/healthy)
- HTTP response code
- SSL expiry date
- URL to visit
## Safety Rules
- **NEVER** disable the firewall without asking
- **NEVER** expose database ports to the internet
- **ALWAYS** verify SSH access with the deploy user BEFORE disabling root login
- **ALWAYS** back up existing Nginx configs before overwriting
- **ALWAYS** run `nginx -t` before reloading Nginx
- **ALWAYS** ask before running destructive commands (rm, drop, etc.)
- If anything fails, stop and tell the user what went wrong. Don't chain workarounds.
## Troubleshooting
- **Port already in use:** `lsof -i :<port>` to find what's using it
- **Docker build fails:** Check Dockerfile, show build logs
- **SSL fails:** Verify domain DNS points to VPS IP (`dig <domain>`)
- **App crashes:** `docker compose logs -f` to show logs
- **Can't SSH as deploy user:** Don't disable root login yet, fix keys first
## Post-Deploy Checklist
Tell the user to:
1. Set up monitoring (suggest Uptime Kuma)
2. Configure automated backups for database volumes
3. Set up log rotation (already configured in compose)
4. Consider a CI/CD pipeline (suggest `/ci-gen` skill)
5. Add the domain to their DNS if not done
标签
skill
ai