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
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
- Go to DigitalOcean and create an account
- Click Create → Droplets
-
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)
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
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
Create the app directory:
mkdir -p /opt/myapp
That's the server ready. Exit the SSH session:
exit
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!"
Make it executable:
chmod +x deploy.sh
Step 4: Deploy!
./deploy.sh
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!
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/
| 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
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:
- Guarantees compatibility — Binary is built for the exact target environment
- Simpler setup — No cross-compilation toolchain needed
- Fast enough — Rust builds are cached; subsequent builds only recompile changes
The strip Command
strip target/release/$APP_NAME
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 &
| 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
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
Enable and start:
systemctl daemon-reload
systemctl enable myapp
systemctl start myapp
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!"
Check status anytime:
ssh root@206.189.125.249 "systemctl status myapp"
View logs:
ssh root@206.189.125.249 "journalctl -u myapp -f"
Improvements: Environment Variables
Don't sync your .env file. Instead, create it once on the server:
ssh root@206.189.125.249
cat > /opt/myapp/.env << EOF
DATABASE_URL=postgres://user:pass@localhost/mydb
API_KEY=your_secret_key
RUST_LOG=info
EOF
Update the systemd service to load it:
[Service]
EnvironmentFile=/opt/myapp/.env
Reload:
systemctl daemon-reload
systemctl restart myapp
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
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
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
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\""
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
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
Deploy
./deploy.sh
View Logs
ssh root@YOUR_IP "journalctl -u myapp -f"
Restart
ssh root@YOUR_IP "systemctl restart myapp"
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:
-
rsyncyour code to the server -
cargo build --releaseon the server - 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)