SSH Deployment
Planned Feature
The forge deploy:ssh command is planned for a future release. The deployment workflow described below documents the intended design. In the meantime, use the generated deployment scripts (forge deploy:scripts) for manual SSH deployments.
FORGE will be able to deploy your application directly to any Linux server via SSH. This approach gives you full control over the server environment without requiring Docker or Kubernetes. It is well suited for traditional VPS setups, bare-metal servers, or environments where containerization is not an option.
Direct Deploy
Deploy to a remote server with a single command:
forge deploy:ssh --host=production.myapp.com --user=deployFORGE connects to the server via SSH, pulls the latest code, builds the application, runs migrations, and restarts services.
Command Options
| Option | Type | Default | Description |
|---|---|---|---|
--host | string | required | Server IP address or hostname |
--user | string | deploy | SSH username |
--key | string | ~/.ssh/id_rsa | Path to SSH private key |
--port | number | 22 | SSH port |
--env | string | production | Target environment |
--branch | string | main | Git branch to deploy |
--dry-run | flag | false | Preview commands without executing |
Examples
forge deploy:ssh \
--host=production.myapp.com \
--user=deployforge deploy:ssh \
--host=staging.myapp.com \
--user=deploy \
--env=staging \
--branch=developforge deploy:ssh \
--host=1.2.3.4 \
--user=deploy \
--key=~/.ssh/deploy_rsa \
--port=2222# Preview what would happen without executing
forge deploy:ssh \
--host=production.myapp.com \
--user=deploy \
--dry-runWhat Happens During Deployment
When you run forge deploy:ssh, the following steps execute on the remote server:
LOCAL MACHINE REMOTE SERVER
───────────── ─────────────
1. Connect via SSH ────────────────► Authenticate with key
2. ────────────────────────────────► cd /var/www/myapp
3. ────────────────────────────────► git fetch origin
git checkout main
git pull origin main
4. ────────────────────────────────► Build backend
cargo build --release
5. ────────────────────────────────► Build frontend
cd apps/web && npm ci && npm run build
cd apps/admin && npm ci && npm run build
6. ────────────────────────────────► Run database migrations
./target/release/forge-cli migrate
7. ────────────────────────────────► Restart services
systemctl restart myapp-api
pm2 restart myapp-web
pm2 restart myapp-admin
8. ────────────────────────────────► Health check
curl http://localhost:8080/health
9. ◄──────────────────────────────── Report statusEach step is logged in real time. If any step fails, the deployment stops and reports the error.
Dry run first
Always run with --dry-run before your first deployment to a new server. This shows every command that would be executed, letting you verify the deployment plan before making changes.
Security Best Practices
SSH Security
Follow these practices to keep your deployment pipeline secure.
Recommended
Use SSH agent -- Add your key to the SSH agent so the key file is never passed directly to FORGE:
bashssh-add ~/.ssh/deploy_rsa forge deploy:ssh --host=myapp.com --user=deployUse a dedicated deploy user -- Create a user with only the permissions needed for deployment:
bash# On the server sudo adduser --disabled-password deploy sudo usermod -aG www-data deployUse key-based authentication -- Disable password authentication on the server:
bash# /etc/ssh/sshd_config PasswordAuthentication no PubkeyAuthentication yesRestrict deploy user capabilities -- Limit sudo access to only the required commands:
bash# /etc/sudoers.d/deploy deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart myapp-api deploy ALL=(ALL) NOPASSWD: /bin/systemctl status myapp-api
Avoid
- Passing private key content directly (use file path instead)
- Using the root user for deployments
- Storing SSH keys in environment variables
- Deploying from shared or public machines
How FORGE handles keys
FORGE passes the key file path to the standard SSH client. The key is never read into FORGE's memory, stored, or logged. The SSH connection uses the same authentication flow as running ssh manually.
Generate Deployment Scripts
If you prefer to manage deployments manually rather than using forge deploy:ssh, generate standalone shell scripts:
forge deploy:scripts --env=productionThis creates three scripts:
deploy/production/
├── deploy.sh # Main deployment script
├── rollback.sh # Rollback to previous version
└── setup-server.sh # Initial server setupdeploy.sh
The main deployment script handles the full build and deploy cycle:
#!/bin/bash
set -euo pipefail
APP_NAME="myapp"
APP_DIR="/var/www/${APP_NAME}"
BRANCH="${1:-main}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
echo "==> Deploying ${APP_NAME} (branch: ${BRANCH})"
# Save current version for rollback
cd ${APP_DIR}
git rev-parse HEAD > .last-deploy-commit
# Enter maintenance mode
touch ${APP_DIR}/.maintenance
# Pull latest code
git fetch origin
git checkout ${BRANCH}
git pull origin ${BRANCH}
# Backend (Rust)
echo "==> Building API..."
cd ${APP_DIR}/apps/api
cargo build --release
# Frontend (Next.js Web)
echo "==> Building web app..."
cd ${APP_DIR}/apps/web
npm ci --production=false
npm run build
# Frontend (Next.js Admin)
echo "==> Building admin app..."
cd ${APP_DIR}/apps/admin
npm ci --production=false
npm run build
# Run migrations
echo "==> Running migrations..."
cd ${APP_DIR}
./target/release/forge-cli migrate
# Restart services
echo "==> Restarting services..."
sudo systemctl restart ${APP_NAME}-api
pm2 restart ${APP_NAME}-web
pm2 restart ${APP_NAME}-admin
# Exit maintenance mode
rm -f ${APP_DIR}/.maintenance
# Health check
echo "==> Running health check..."
sleep 5
if curl -sf http://localhost:8080/health > /dev/null; then
echo "==> Deployment successful!"
else
echo "==> Health check failed! Rolling back..."
./rollback.sh
exit 1
fi
echo "==> Deploy completed at ${TIMESTAMP}"rollback.sh
Rolls back to the previous deployment:
#!/bin/bash
set -euo pipefail
APP_NAME="myapp"
APP_DIR="/var/www/${APP_NAME}"
echo "==> Rolling back ${APP_NAME}..."
cd ${APP_DIR}
# Get previous commit
if [ ! -f .last-deploy-commit ]; then
echo "ERROR: No previous deployment found"
exit 1
fi
PREV_COMMIT=$(cat .last-deploy-commit)
echo "==> Reverting to commit: ${PREV_COMMIT}"
# Enter maintenance mode
touch ${APP_DIR}/.maintenance
# Revert code
git checkout ${PREV_COMMIT}
# Rebuild
cd ${APP_DIR}/apps/api && cargo build --release
cd ${APP_DIR}/apps/web && npm ci --production=false && npm run build
cd ${APP_DIR}/apps/admin && npm ci --production=false && npm run build
# Rollback migrations (if needed)
cd ${APP_DIR}
./target/release/forge-cli migrate:rollback
# Restart services
sudo systemctl restart ${APP_NAME}-api
pm2 restart ${APP_NAME}-web
pm2 restart ${APP_NAME}-admin
# Exit maintenance mode
rm -f ${APP_DIR}/.maintenance
echo "==> Rollback complete!"setup-server.sh
One-time server setup script for provisioning a fresh server:
#!/bin/bash
set -euo pipefail
APP_NAME="myapp"
APP_DIR="/var/www/${APP_NAME}"
DOMAIN="myapp.com"
echo "==> Setting up server for ${APP_NAME}..."
# Update system
sudo apt update && sudo apt upgrade -y
# Install dependencies
sudo apt install -y \
curl git build-essential \
postgresql postgresql-contrib \
redis-server \
nginx certbot python3-certbot-nginx \
nodejs npm
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source $HOME/.cargo/env
# Install PM2 for Node.js process management
sudo npm install -g pm2
# Create application directory
sudo mkdir -p ${APP_DIR}
sudo chown deploy:deploy ${APP_DIR}
# Clone repository
git clone git@github.com:yourorg/${APP_NAME}.git ${APP_DIR}
# Setup PostgreSQL
sudo -u postgres createuser ${APP_NAME}
sudo -u postgres createdb ${APP_NAME} -O ${APP_NAME}
sudo -u postgres psql -c \
"ALTER USER ${APP_NAME} WITH PASSWORD 'SET_STRONG_PASSWORD';"
# Setup systemd service for API
sudo tee /etc/systemd/system/${APP_NAME}-api.service << 'EOF'
[Unit]
Description=FORGE API Server
After=network.target postgresql.service
[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/myapp/apps/api
ExecStart=/var/www/myapp/target/release/myapp-api
Restart=always
RestartSec=5
Environment=DATABASE_URL=postgres://myapp:SET_STRONG_PASSWORD@localhost/myapp
Environment=RUST_LOG=info
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable ${APP_NAME}-api
# Setup PM2 for frontend apps
cd ${APP_DIR}/apps/web && pm2 start npm --name "${APP_NAME}-web" -- start
cd ${APP_DIR}/apps/admin && pm2 start npm --name "${APP_NAME}-admin" -- start
pm2 save
pm2 startup
echo "==> Server setup complete!"
echo "==> Next steps:"
echo " 1. Set a strong database password"
echo " 2. Configure SSL with certbot"
echo " 3. Run: ./deploy.sh"Running Scripts Remotely
Execute the generated scripts on your server:
# Initial server setup (run once)
ssh deploy@myapp.com 'bash -s' < deploy/production/setup-server.sh
# Deploy
ssh deploy@myapp.com 'bash -s' < deploy/production/deploy.sh
# Deploy a specific branch
ssh deploy@myapp.com 'bash -s' < deploy/production/deploy.sh develop
# Rollback
ssh deploy@myapp.com 'bash -s' < deploy/production/rollback.shServer Requirements
The target server needs the following:
| Requirement | Minimum | Recommended |
|---|---|---|
| OS | Ubuntu 22.04 / Debian 12 | Ubuntu 24.04 LTS |
| CPU | 1 vCPU | 2+ vCPU |
| RAM | 2 GB | 4+ GB |
| Disk | 20 GB | 40+ GB SSD |
| PostgreSQL | 15+ | 16 |
| Redis | 7+ | 7 |
| Node.js | 18+ | 20 LTS |
| Rust | Latest stable | Latest stable |
Maintenance Mode
The deployment scripts create a .maintenance file during deployments. You can configure your reverse proxy (Nginx or Caddy) to serve a maintenance page when this file exists:
# Nginx maintenance mode check
location / {
if (-f /var/www/myapp/.maintenance) {
return 503;
}
# ... normal proxy rules
}
error_page 503 @maintenance;
location @maintenance {
root /var/www/myapp/public;
rewrite ^(.*)$ /maintenance.html break;
}Next Steps
- Docker Compose Deployment -- Containerized single-server deployment
- Kubernetes Deployment -- Auto-scaling cluster deployment
- SSL Certificates -- Configure HTTPS for your server