Deploy a local project to a remote Linux server with one click. Automatically identifies the project type and adapts the corresponding build, run, and process management solution.
Step Zero: Project Identification and Information Collection
0a. Automatically Identify Project Type
Scan the project root directory and determine the type according to the following rules:
| Marker File | Project Type | Runtime | Process Management |
|---|---|---|---|
package.json |
Node.js | node | PM2 |
Cargo.toml |
Rust | Build Artifact | systemd |
go.mod |
Go | Build Artifact | systemd |
pyproject.toml / requirements.txt / setup.py |
Python | python3 | systemd / gunicorn |
Dockerfile / docker-compose.yml |
Docker | docker | docker compose |
index.html(no other markers) |
Static Site | None | Hosted directly by Nginx |
If multiple marker files are detected (such as package.json + Dockerfile), use AskUserQuestion to let the user choose the deployment method.
0b. Collect Information
Use AskUserQuestion to collect the following information (skip what is already in the context):
-
SSH Connection(header: "Server")
- Dynamically generate options based on the context (such as previously used server IPs)
- Fallback option "Other Server"
-
Project Path(header: "Project")
- Automatically infer based on the current working directory
- If uncertain, ask the user
-
Domain(header: "Domain")
- Existing domain (let the user enter it)
- Access by IP only (skip domain and SSL configuration)
-
Running Port(header: "Port")
- Automatic detection:
- Node.js: extract from package.json scripts or .env
- Python: extract from main.py / app.py / manage.py
- Rust/Go: extract from main.rs / main.go or configuration files
- If it cannot be detected, let the user specify
- Do not use 3000, 3001, 4000, 5000, 5173, 8000, 8080, 8888
- Automatic detection:
0c. Variable Definitions
| Variable | Description |
|---|---|
$SSH_TARGET |
SSH connection string (such as root@192.168.1.1) |
$PROJECT_DIR |
Absolute path of the local project |
$PROJECT_NAME |
Project name |
$PROJECT_TYPE |
Project type:nodejs / python / rust / go / docker / static |
$REMOTE_DIR |
Remote deployment directory (default /opt/$PROJECT_NAME) |
$PORT |
Application running port |
$DOMAIN |
Domain (optional) |
Step One: SSH Connection and Key Configuration
1a. Check the Local SSH Public Key
cat ~/.ssh/id_rsa.pub 2>/dev/null || cat ~/.ssh/id_ed25519.pub 2>/dev/null
If there is no public key, generate one:ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519
1b. Test Passwordless Login
ssh -o ConnectTimeout=5 -o BatchMode=yes $SSH_TARGET "echo ok" 2>/dev/null
If it fails, log in with a password and add the public key:
sshpass -p '<PASSWORD>' ssh -o StrictHostKeyChecking=no $SSH_TARGET \
"mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '<PUBLIC_KEY>' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
1c. Verify Passwordless Login
ssh -o ConnectTimeout=5 $SSH_TARGET "echo '免密登录成功' && uname -a"
Step Two: Server Environment Check
Get all information with a single command:
ssh $SSH_TARGET "echo '=== 系统 ===' && cat /etc/os-release | grep PRETTY_NAME && uname -m && echo '=== CPU ===' && nproc && echo '=== 内存 ===' && free -h && echo '=== 磁盘 ===' && df -h / && echo '=== 包管理器 ===' && which apt yum dnf pacman 2>/dev/null && echo '=== Node.js ===' && node -v 2>/dev/null || echo '未安装' && echo '=== Python ===' && python3 --version 2>/dev/null || echo '未安装' && echo '=== Rust ===' && rustc --version 2>/dev/null || echo '未安装' && echo '=== Go ===' && go version 2>/dev/null || echo '未安装' && echo '=== Docker ===' && docker --version 2>/dev/null || echo '未安装' && echo '=== Nginx ===' && nginx -v 2>&1 || echo '未安装'"
Check Points:
- Whether disk space is sufficient
- Memory: Node.js ≥ 512MB, Rust compilation ≥ 1GB (or local cross-compilation)
- Architecture:
x86_64/aarch64(Rust/Go must match the compilation target) - Record missing components for unified installation later
Step 3: Install the runtime environment
Based on $PROJECT_TYPE , install the corresponding runtime. Only install missing components.
Node.js project
# 安装 Node.js 20(如未安装)
ssh $SSH_TARGET "curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs"
# 安装 PM2
ssh $SSH_TARGET "npm install -g pm2"
Python project
# 安装 Python3 + pip + venv(如未安装)
ssh $SSH_TARGET "apt-get install -y python3 python3-pip python3-venv"
Rust project
Two options, choose based on server resources:
Option A: Compile on the server (memory ≥ 1GB)
ssh $SSH_TARGET "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source ~/.cargo/env"
Option B: Local cross-compilation (recommended for low-spec servers)
# 本地编译(需安装对应 target)
rustup target add x86_64-unknown-linux-gnu # 或 aarch64-unknown-linux-gnu
cargo build --release --target x86_64-unknown-linux-gnu
# 仅上传编译产物
Go project
Also two options:
Option A: Compile on the server
ssh $SSH_TARGET "wget -q https://go.dev/dl/go1.22.0.linux-amd64.tar.gz && tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz && echo 'export PATH=\$PATH:/usr/local/go/bin' >> ~/.bashrc"
Option B: Local cross-compilation (recommended)
GOOS=linux GOARCH=amd64 go build -o $PROJECT_NAME .
# 仅上传二进制文件
Docker project
# 安装 Docker(如未安装)
ssh $SSH_TARGET "curl -fsSL https://get.docker.com | sh && systemctl enable docker && systemctl start docker"
Nginx (required when there is a domain name)
ssh $SSH_TARGET "apt-get install -y nginx && systemctl enable nginx && systemctl start nginx"
Step 4: Upload the project
4a. Exclusions (by project type)
| Project type | Excluded directories/files |
|---|---|
| Node.js | node_modules/, .next/, dist/, .git/ |
| Python | __pycache__/, .venv/, venv/, *.pyc, .git/ |
| Rust | target/, .git/(Option B only uploads target/release/$BINARY) |
| Go | vendor/(if any), .git/(Option B only uploads the binary) |
| Docker | .git/(Dockerfile needs to be uploaded) |
| Static site | .git/, node_modules/ |
4b. rsync upload
Note: SSH may disconnect during large file transfers. Use keepalive parameters:
rsync -avz --timeout=300 \
-e "ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=10" \
--exclude='.git' --exclude='<TYPE_SPECIFIC_EXCLUDES>' \
$PROJECT_DIR/ $SSH_TARGET:$REMOTE_DIR/
If the file is very large (>500MB): upload the code first, then upload the large file separately.
If SSH frequently disconnects: adjust the server SSH configuration:
ssh $SSH_TARGET "sed -i 's/#ClientAliveInterval.*/ClientAliveInterval 60/' /etc/ssh/sshd_config && sed -i 's/#ClientAliveCountMax.*/ClientAliveCountMax 120/' /etc/ssh/sshd_config && systemctl reload ssh || systemctl reload sshd"
4c. Install dependencies + build
Run in the background with nohupto prevent interruption from disconnection:
| Project type | Command |
|---|---|
| Node.js | nohup bash -c 'npm install && npm run build' > /tmp/build.log 2>&1 & |
| Python | nohup bash -c 'python3 -m venv .venv && .venv/bin/pip install -r requirements.txt' > /tmp/build.log 2>&1 & |
| Rust (Option A) | nohup bash -c 'source ~/.cargo/env && cargo build --release' > /tmp/build.log 2>&1 & |
| Go (Option A) | nohup bash -c 'go build -o $PROJECT_NAME .' > /tmp/build.log 2>&1 & |
| Docker | nohup bash -c 'docker compose build' > /tmp/build.log 2>&1 & |
| Static site | No build required |
Wait and then check the build result:ssh $SSH_TARGET "tail -20 /tmp/build.log"
Step 5: Start the application and process management
Choose the corresponding process management solution based on the project type.
Node.js → PM2
ssh $SSH_TARGET "cd $REMOTE_DIR && pm2 start npm --name $PROJECT_NAME -- start && pm2 startup && pm2 save"
Verify:ssh $SSH_TARGET "pm2 list && curl -s -o /dev/null -w '%{http_code}' http://localhost:$PORT"
Python → systemd
ssh $SSH_TARGET "cat > /etc/systemd/system/$PROJECT_NAME.service << EOF
[Unit]
Description=$PROJECT_NAME
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=$REMOTE_DIR
ExecStart=$REMOTE_DIR/.venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port $PORT
# 或 gunicorn: ExecStart=$REMOTE_DIR/.venv/bin/gunicorn -w 4 -b 0.0.0.0:$PORT main:app
Restart=on-failure
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload && systemctl enable $PROJECT_NAME && systemctl start $PROJECT_NAME"
Note: adjust ExecStart according to the framework:
- FastAPI/Starlette:
uvicorn main:app- Flask:
gunicorn -w 4 main:app- Django:
gunicorn -w 4 project.wsgi:application- Pure script:
python main.py
Verify:ssh $SSH_TARGET "systemctl status $PROJECT_NAME && curl -s -o /dev/null -w '%{http_code}' http://localhost:$PORT"
Rust / Go → systemd
ssh $SSH_TARGET "cat > /etc/systemd/system/$PROJECT_NAME.service << EOF
[Unit]
Description=$PROJECT_NAME
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=$REMOTE_DIR
ExecStart=$REMOTE_DIR/$BINARY_NAME
Restart=on-failure
RestartSec=5
Environment=PORT=$PORT
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload && systemctl enable $PROJECT_NAME && systemctl start $PROJECT_NAME"
Rust binary path:
$REMOTE_DIR/target/release/$PROJECT_NAME(Option A) or$REMOTE_DIR/$PROJECT_NAME(Option B) Go binary path:$REMOTE_DIR/$PROJECT_NAME
Verify:ssh $SSH_TARGET "systemctl status $PROJECT_NAME && curl -s -o /dev/null -w '%{http_code}' http://localhost:$PORT"
Docker → docker compose
ssh $SSH_TARGET "cd $REMOTE_DIR && docker compose up -d"
Configure startup on boot: the Docker service starts with systemd by default; for docker compose, use restart: unless-stopped .
Verify:ssh $SSH_TARGET "docker compose ps && curl -s -o /dev/null -w '%{http_code}' http://localhost:$PORT"
Static site → hosted directly by Nginx
No process management required; Nginx points directly to the static file directory (see Step 6).
Step 6: Nginx reverse proxy
If the user chooses "access by IP only", skip this step and the subsequent SSL steps.
6a. Dynamic application (HTTP reverse proxy)
ssh $SSH_TARGET "cat > /etc/nginx/sites-available/$PROJECT_NAME << 'NGINX'
server {
listen 80;
server_name $DOMAIN www.$DOMAIN;
client_max_body_size 50M;
location / {
proxy_pass http://127.0.0.1:$PORT;
proxy_http_version 1.1;
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_set_header Upgrade \$http_upgrade;
proxy_set_header Connection \"upgrade\";
}
}
NGINX
ln -sf /etc/nginx/sites-available/$PROJECT_NAME /etc/nginx/sites-enabled/$PROJECT_NAME
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx"
6b. Static site (direct hosting)
ssh $SSH_TARGET "cat > /etc/nginx/sites-available/$PROJECT_NAME << 'NGINX'
server {
listen 80;
server_name $DOMAIN www.$DOMAIN;
root $REMOTE_DIR;
index index.html;
location / {
try_files \$uri \$uri/ /index.html;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 30d;
add_header Cache-Control \"public, immutable\";
}
}
NGINX
ln -sf /etc/nginx/sites-available/$PROJECT_NAME /etc/nginx/sites-enabled/$PROJECT_NAME
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx"
6c. HTTPS version (when there is an SSL certificate)
Based on the corresponding server block:
- Add to the port 80 server
return 301 https://$host$request_uri; - Add a new port 443 server block, add ssl_certificate and security headers
server {
listen 443 ssl;
server_name $DOMAIN www.$DOMAIN;
ssl_certificate /etc/ssl/cloudflare/fullchain.pem;
ssl_certificate_key /etc/ssl/cloudflare/key.pem;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# ... location 块同上 ...
}
6d. Nginx security hardening
ssh $SSH_TARGET "sed -i 's/# server_tokens off;/server_tokens off;/' /etc/nginx/nginx.conf && nginx -t && systemctl reload nginx"
Step 7: DNS and SSL configuration
Only execute when the user has a domain name.
7a. DNS configuration guide
Tell the user to add DNS records in the domain registrar's backend:
| Record type | Name | Content |
|---|---|---|
| A | @ |
<服务器IP> |
| A | www |
<服务器IP> |
If using Cloudflare, it is recommended to enable the orange cloud (proxy mode).
7b. SSL Option Selection
Use AskUserQuestion to ask:
- Cloudflare Origin Certificate (recommended) — DNS hosted on Cloudflare, certificate valid for 15 years
- Let's Encrypt — Free public certificate, requires periodic renewal
- Do not configure SSL for now
Cloudflare Origin Certificate process:
-
Tell the user what to do: Cloudflare → SSL/TLS → Origin Server → Create Certificate
- Ensure the hostname includes
$DOMAINand*.$DOMAIN - Private key type: RSA, Validity: 15 years
- Ensure the hostname includes
-
After the user provides the certificate and private key, write them to the server and concatenate the certificate chain:
ssh $SSH_TARGET "mkdir -p /etc/ssl/cloudflare"
# 写入 cert.pem 和 key.pem(chmod 600)
# 下载 Origin CA 根证书并拼接
ssh $SSH_TARGET "curl -s -o /etc/ssl/cloudflare/origin-ca-rsa-root.pem https://developers.cloudflare.com/ssl/static/origin_ca_rsa_root.pem && cat /etc/ssl/cloudflare/cert.pem /etc/ssl/cloudflare/origin-ca-rsa-root.pem > /etc/ssl/cloudflare/fullchain.pem"
- The certificate must be verified:
ssh $SSH_TARGET "openssl x509 -in /etc/ssl/cloudflare/cert.pem -noout -text | grep -E 'DNS:|Subject:|Issuer:|Extended'"
DNS:includes the domainExtended Key UsageincludesTLS Web Server Authentication- If missing, tell the user to recreate it
- Cloudflare SSL mode: first set Full (Strict), if a 526 error occurs, downgrade to Full
Let's Encrypt process:
ssh $SSH_TARGET "apt-get install -y certbot python3-certbot-nginx && certbot --nginx -d $DOMAIN -d www.$DOMAIN --non-interactive --agree-tos --email <EMAIL>"
You need to temporarily disable the Cloudflare proxy (orange → gray), and you can re-enable it after completion.
Extended Reference
Detailed material starting at ## 第八步:安全加固 has been moved to reference/extended.md to keep this skill concise. Load that reference when the task requires the moved examples, command catalogs, checklists, platform details, or implementation templates.