Skip to content

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:

bash
forge deploy:ssh --host=production.myapp.com --user=deploy

FORGE connects to the server via SSH, pulls the latest code, builds the application, runs migrations, and restarts services.

Command Options

OptionTypeDefaultDescription
--hoststringrequiredServer IP address or hostname
--userstringdeploySSH username
--keystring~/.ssh/id_rsaPath to SSH private key
--portnumber22SSH port
--envstringproductionTarget environment
--branchstringmainGit branch to deploy
--dry-runflagfalsePreview commands without executing

Examples

bash
forge deploy:ssh \
  --host=production.myapp.com \
  --user=deploy
bash
forge deploy:ssh \
  --host=staging.myapp.com \
  --user=deploy \
  --env=staging \
  --branch=develop
bash
forge deploy:ssh \
  --host=1.2.3.4 \
  --user=deploy \
  --key=~/.ssh/deploy_rsa \
  --port=2222
bash
# Preview what would happen without executing
forge deploy:ssh \
  --host=production.myapp.com \
  --user=deploy \
  --dry-run

What 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 status

Each 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.

  • Use SSH agent -- Add your key to the SSH agent so the key file is never passed directly to FORGE:

    bash
    ssh-add ~/.ssh/deploy_rsa
    forge deploy:ssh --host=myapp.com --user=deploy
  • Use 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 deploy
  • Use key-based authentication -- Disable password authentication on the server:

    bash
    # /etc/ssh/sshd_config
    PasswordAuthentication no
    PubkeyAuthentication yes
  • Restrict 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:

bash
forge deploy:scripts --env=production

This creates three scripts:

deploy/production/
├── deploy.sh          # Main deployment script
├── rollback.sh        # Rollback to previous version
└── setup-server.sh    # Initial server setup

deploy.sh

The main deployment script handles the full build and deploy cycle:

bash
#!/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:

bash
#!/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:

bash
#!/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:

bash
# 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.sh

Server Requirements

The target server needs the following:

RequirementMinimumRecommended
OSUbuntu 22.04 / Debian 12Ubuntu 24.04 LTS
CPU1 vCPU2+ vCPU
RAM2 GB4+ GB
Disk20 GB40+ GB SSD
PostgreSQL15+16
Redis7+7
Node.js18+20 LTS
RustLatest stableLatest 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
# 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

Released under the MIT License.