DEV Community

Cover image for The Simplest Way to Deploy a Rust App to DigitalOcean (No Docker, No K8s)
Mayuresh Smita Suresh
Mayuresh Smita Suresh Subscriber

Posted on

The Simplest Way to Deploy a Rust App to DigitalOcean (No Docker, No K8s)

Everyone talks about Docker, Kubernetes, CI/CD pipelines, and container orchestration. But sometimes you just want to deploy a damn Rust app.

Here's the simplest deployment setup I use. It's a single bash script that deploys to a $6/month DigitalOcean droplet in under 60 seconds. No Docker. No Kubernetes. No GitHub Actions. Just rsync, ssh, and cargo build.

The End Result

./deploy.sh
Enter fullscreen mode Exit fullscreen mode

That's it. Your Rust app is live.

Let me show you how to set this up from scratch.


Step 1: Create a DigitalOcean Droplet

  1. Go to DigitalOcean and create an account
  2. Click CreateDroplets
  3. Choose:

    • Image: Ubuntu 24.04 LTS
    • Size: Basic → $6/month (1GB RAM, 1 CPU) — enough for most Rust apps
    • Region: Closest to your users
    • Authentication: SSH Key (add your public key)
  4. Click Create Droplet and note the IP address (e.g., 206.189.125.249)

Cost: $6/month. That's it. No hidden fees, no surprise bills.


Step 2: Set Up the Droplet

SSH into your new server:

ssh root@206.189.125.249
Enter fullscreen mode Exit fullscreen mode

Now install Rust and essential build tools:

# Update system
apt update && apt upgrade -y

# Install build essentials
apt install -y build-essential pkg-config libssl-dev

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y

# Load Rust environment
source ~/.cargo/env

# Verify installation
rustc --version
cargo --version
Enter fullscreen mode Exit fullscreen mode

Create the app directory:

mkdir -p /opt/myapp
Enter fullscreen mode Exit fullscreen mode

That's the server ready. Exit the SSH session:

exit
Enter fullscreen mode Exit fullscreen mode

Step 3: The Deploy Script

Create deploy.sh in your project root:

#!/bin/bash
set -e

# Configuration
DROPLET="root@206.189.125.249"
REMOTE_DIR="/opt/myapp"
APP_NAME="myapp"

echo "📦 Syncing source to droplet..."
rsync -avz --delete \
    --exclude 'target' \
    --exclude '.git' \
    --exclude 'node_modules' \
    --exclude '.env' \
    --exclude '.env.local' \
    . $DROPLET:$REMOTE_DIR/

echo "🔨 Building on droplet..."
ssh $DROPLET << EOF
cd $REMOTE_DIR
source ~/.cargo/env

# Build release binary
cargo build --release

# Strip debug symbols (reduces binary size by ~50-70%)
strip target/release/$APP_NAME

# Stop existing process
pkill $APP_NAME || true

# Start new process
nohup ./target/release/$APP_NAME > /var/log/$APP_NAME.log 2>&1 &

echo "✅ Deployed and running!"
EOF

echo "🚀 Deployment complete!"
Enter fullscreen mode Exit fullscreen mode

Make it executable:

chmod +x deploy.sh
Enter fullscreen mode Exit fullscreen mode

Step 4: Deploy!

./deploy.sh
Enter fullscreen mode Exit fullscreen mode

You'll see:

📦 Syncing source to droplet...
sending incremental file list
src/
src/main.rs
Cargo.toml
...

🔨 Building on droplet...
   Compiling myapp v0.1.0 (/opt/myapp)
    Finished release [optimized] target(s) in 45.23s
✅ Deployed and running!

🚀 Deployment complete!
Enter fullscreen mode Exit fullscreen mode

Your app is now live at http://206.189.125.249:8080 (or whatever port you configured).


Let's Break Down the Script

The rsync Command

rsync -avz --delete \
    --exclude 'target' \
    --exclude '.git' \
    --exclude 'node_modules' \
    --exclude '.env' \
    . $DROPLET:$REMOTE_DIR/
Enter fullscreen mode Exit fullscreen mode
Flag Purpose
-a Archive mode (preserves permissions, symlinks, etc.)
-v Verbose output
-z Compress during transfer
--delete Remove files on server that don't exist locally
--exclude Skip these directories/files

We exclude target/ because we'll build on the server. This keeps the transfer fast (source code only, not compiled binaries).

The SSH Heredoc

ssh $DROPLET << EOF
cd $REMOTE_DIR
source ~/.cargo/env
cargo build --release
...
EOF
Enter fullscreen mode Exit fullscreen mode

This runs multiple commands on the remote server in a single SSH session. The EOF marker defines the start and end of the remote commands.

Why Build on the Server?

You could cross-compile locally, but building on the server:

  1. Guarantees compatibility — Binary is built for the exact target environment
  2. Simpler setup — No cross-compilation toolchain needed
  3. Fast enough — Rust builds are cached; subsequent builds only recompile changes

The strip Command

strip target/release/$APP_NAME
Enter fullscreen mode Exit fullscreen mode

This removes debug symbols from the binary, reducing size by 50-70%. A 20MB binary becomes 6MB. Smaller binary = faster startup.

The nohup Trick

nohup ./target/release/$APP_NAME > /var/log/$APP_NAME.log 2>&1 &
Enter fullscreen mode Exit fullscreen mode
Part Purpose
nohup Keep running after SSH session ends
> /var/log/$APP_NAME.log Redirect stdout to log file
2>&1 Redirect stderr to same log file
& Run in background

Improvements: Adding systemd

The nohup approach works, but using systemd is more robust. It:

  • Auto-restarts your app if it crashes
  • Starts your app on server reboot
  • Provides proper logging with journalctl

Create a systemd service file:

ssh root@206.189.125.249
Enter fullscreen mode Exit fullscreen mode
cat > /etc/systemd/system/myapp.service << EOF
[Unit]
Description=My Rust App
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/target/release/myapp
Restart=always
RestartSec=5
Environment=RUST_LOG=info

[Install]
WantedBy=multi-user.target
EOF
Enter fullscreen mode Exit fullscreen mode

Enable and start:

systemctl daemon-reload
systemctl enable myapp
systemctl start myapp
Enter fullscreen mode Exit fullscreen mode

Now update your deploy script to use systemd:

#!/bin/bash
set -e

DROPLET="root@206.189.125.249"
REMOTE_DIR="/opt/myapp"
APP_NAME="myapp"

echo "📦 Syncing source to droplet..."
rsync -avz --delete \
    --exclude 'target' \
    --exclude '.git' \
    --exclude 'node_modules' \
    --exclude '.env' \
    . $DROPLET:$REMOTE_DIR/

echo "🔨 Building on droplet..."
ssh $DROPLET << EOF
cd $REMOTE_DIR
source ~/.cargo/env

cargo build --release
strip target/release/$APP_NAME

# Restart via systemd
systemctl restart $APP_NAME

echo "✅ Deployed!"
EOF

echo "🚀 Deployment complete!"
Enter fullscreen mode Exit fullscreen mode

Check status anytime:

ssh root@206.189.125.249 "systemctl status myapp"
Enter fullscreen mode Exit fullscreen mode

View logs:

ssh root@206.189.125.249 "journalctl -u myapp -f"
Enter fullscreen mode Exit fullscreen mode

Improvements: Environment Variables

Don't sync your .env file. Instead, create it once on the server:

ssh root@206.189.125.249
Enter fullscreen mode Exit fullscreen mode
cat > /opt/myapp/.env << EOF
DATABASE_URL=postgres://user:pass@localhost/mydb
API_KEY=your_secret_key
RUST_LOG=info
EOF
Enter fullscreen mode Exit fullscreen mode

Update the systemd service to load it:

[Service]
EnvironmentFile=/opt/myapp/.env
Enter fullscreen mode Exit fullscreen mode

Reload:

systemctl daemon-reload
systemctl restart myapp
Enter fullscreen mode Exit fullscreen mode

Improvements: Adding Nginx (Optional)

If you want:

  • HTTPS with Let's Encrypt
  • Custom domain
  • Reverse proxy on port 80/443

Install Nginx:

apt install -y nginx certbot python3-certbot-nginx
Enter fullscreen mode Exit fullscreen mode

Create config:

cat > /etc/nginx/sites-available/myapp << EOF
server {
    listen 80;
    server_name myapp.com www.myapp.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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;
    }
}
EOF
Enter fullscreen mode Exit fullscreen mode

Enable and get SSL:

ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
nginx -t
systemctl restart nginx

# Get free SSL certificate
certbot --nginx -d myapp.com -d www.myapp.com
Enter fullscreen mode Exit fullscreen mode

Done. Your app is now at https://myapp.com with auto-renewing SSL.


The Complete Production Setup

Here's my full deploy.sh for production:

#!/bin/bash
set -e

# ============================================
# CONFIGURATION
# ============================================
DROPLET="root@206.189.125.249"
REMOTE_DIR="/opt/myapp"
APP_NAME="myapp"
SERVICE_NAME="myapp"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# ============================================
# FUNCTIONS
# ============================================
log_info() {
    echo -e "${GREEN}[INFO]${NC} $1"
}

log_warn() {
    echo -e "${YELLOW}[WARN]${NC} $1"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

# ============================================
# PRE-FLIGHT CHECKS
# ============================================
log_info "Running pre-flight checks..."

# Check if we can connect to the server
if ! ssh -q $DROPLET exit; then
    log_error "Cannot connect to $DROPLET"
    exit 1
fi

# Check if Cargo.toml exists
if [ ! -f "Cargo.toml" ]; then
    log_error "Cargo.toml not found. Run this from your project root."
    exit 1
fi

# ============================================
# DEPLOYMENT
# ============================================
START_TIME=$(date +%s)

log_info "📦 Syncing source to droplet..."
rsync -avz --delete \
    --exclude 'target' \
    --exclude '.git' \
    --exclude 'node_modules' \
    --exclude '.env' \
    --exclude '.env.local' \
    --exclude '.DS_Store' \
    --exclude '*.log' \
    . $DROPLET:$REMOTE_DIR/

log_info "🔨 Building on droplet..."
ssh $DROPLET << EOF
set -e
cd $REMOTE_DIR
source ~/.cargo/env

echo "Building release binary..."
cargo build --release 2>&1

echo "Stripping debug symbols..."
strip target/release/$APP_NAME

echo "Restarting service..."
systemctl restart $SERVICE_NAME

echo "Checking service status..."
sleep 2
if systemctl is-active --quiet $SERVICE_NAME; then
    echo "✅ Service is running!"
else
    echo "❌ Service failed to start!"
    journalctl -u $SERVICE_NAME -n 20 --no-pager
    exit 1
fi
EOF

END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))

log_info "🚀 Deployment complete in ${DURATION}s!"
log_info "Check logs: ssh $DROPLET \"journalctl -u $SERVICE_NAME -f\""
Enter fullscreen mode Exit fullscreen mode

When NOT to Use This Approach

This simple setup works great for:

  • ✅ Side projects
  • ✅ MVPs
  • ✅ Low-to-medium traffic apps
  • ✅ Single-server deployments
  • ✅ When you want full control

Consider alternatives when you need:

  • ❌ Zero-downtime deployments → Use Docker + load balancer
  • ❌ Auto-scaling → Use Kubernetes or managed services
  • ❌ Multi-region → Use a proper cloud setup
  • ❌ Team deployments → Use CI/CD pipelines

Cost Comparison

Approach Monthly Cost Complexity
This setup $6 Low
DigitalOcean App Platform $12+ Medium
AWS ECS $30+ High
Kubernetes $50+ Very High
Fly.io $0-10 Medium
Railway $5-20 Low

For most side projects and MVPs, the $6 droplet with this script is all you need.


Quick Reference

First-Time Server Setup

ssh root@YOUR_IP
apt update && apt upgrade -y
apt install -y build-essential pkg-config libssl-dev
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source ~/.cargo/env
mkdir -p /opt/myapp
Enter fullscreen mode Exit fullscreen mode

Create systemd Service

cat > /etc/systemd/system/myapp.service << EOF
[Unit]
Description=My Rust App
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/target/release/myapp
Restart=always
RestartSec=5
EnvironmentFile=/opt/myapp/.env

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable myapp
Enter fullscreen mode Exit fullscreen mode

Deploy

./deploy.sh
Enter fullscreen mode Exit fullscreen mode

View Logs

ssh root@YOUR_IP "journalctl -u myapp -f"
Enter fullscreen mode Exit fullscreen mode

Restart

ssh root@YOUR_IP "systemctl restart myapp"
Enter fullscreen mode Exit fullscreen mode

Conclusion

You don't need Kubernetes. You don't need Docker. You don't need a 200-line GitHub Actions workflow.

For most Rust apps, especially side projects and MVPs, a simple bash script is all you need:

  1. rsync your code to the server
  2. cargo build --release on the server
  3. Restart via systemd

Total deployment time: ~60 seconds.
Monthly cost: $6.

Ship fast. Keep it simple. Optimize later when you actually need to.


I have deployed Tagnovate Rust backend
and [https://menugo.live] and NodeJs backend in a similar way.

Got questions? Drop a comment below. Building something cool with Rust? I'd love to hear about it!


If this helped you, consider:

  • 👍 Giving this post a like
  • 🔖 Bookmarking for later
  • 💬 Sharing your deployment setup in the comments

Top comments (0)