Back to Blog

January 28, 2026

DIY CI/CD for Rust on Oracle Cloud: The $0 Alternative to Premium Platforms

Introduction

If you're running a Rust microservice on Oracle Cloud and dreading the costs of enterprise CI/CD platforms like GitLab Premium, Jenkins, or hosted solutions—there's good news. You can implement production-grade continuous integration and deployment using three free-tier tools: GitHub Actions, SSH key authentication, and systemd.

This guide shows you exactly how to build a CI/CD pipeline that rivals platforms costing hundreds per month, while paying absolutely nothing.


Why This Matters: The Cost Reality

Platform Monthly Cost Annual Cost
GitLab Premium $228/month $2,736
Jenkins (managed) $150-500/month $1,800-6,000
CircleCI $100-500/month $1,200-6,000
GitHub Actions (our approach) $0 $0
Oracle Cloud Free Tier VM $0 $0

For small teams and indie developers, this cost difference is transformative.


Architecture Overview: How It Works

┌─────────────────────────────────────────────────────────────┐
│                   Developer Workflow                         │
│                                                               │
│  1. git push to master                                       │
│  2. GitHub Actions triggered (free)                          │
│  3. CI: Tests, linting, format checks                        │
│  4. If CI passes → CD: SSH to Oracle server                  │
│  5. On server: git pull + cargo build + systemctl restart    │
│  6. Service live with new code ✅                             │
└─────────────────────────────────────────────────────────────┘

Key insight: We leverage GitHub Actions' free tier (up to 2,000 CI minutes/month for private repos) and SSH key-based authentication to deploy directly to your Oracle VM without intermediary services.


Prerequisites

What you need:

  • A Rust project hosted on GitHub
  • An Oracle Cloud Compute VM (Ubuntu 20.04+, free tier eligible)
  • SSH key access to your Oracle VM
  • Basic familiarity with systemd and shell scripting

Time estimate: 30 minutes to set up, 5 minutes per deployment after


Step 1: Generate SSH Deploy Key (Local Machine)

Your GitHub Actions runner needs a way to authenticate to your Oracle server. We'll use Ed25519 SSH keys—modern, compact, and more secure than RSA.

# Generate deploy key (press Enter for empty passphrase)
ssh-keygen -t ed25519 -f ~/.ssh/rust_deploy -C "github-actions-deploy"

# View the public key (you'll need this next)
cat ~/.ssh/rust_deploy.pub

Output will look like:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... github-actions-deploy

Step 2: Install the Public Key on Oracle Server

SSH into your Oracle VM and authorize the GitHub Actions deploy key:

# SSH to your Oracle server
ssh ubuntu@YOUR_ORACLE_IP

# Create SSH directory if needed
mkdir -p ~/.ssh
chmod 700 ~/.ssh

# Add the public key to authorized_keys
nano ~/.ssh/authorized_keys

Paste the public key from Step 1 (the line starting with ssh-ed25519), save, then:

chmod 600 ~/.ssh/authorized_keys

# Verify it works from your local machine
ssh -i ~/.ssh/rust_deploy ubuntu@YOUR_ORACLE_IP
# Should connect without password

Step 3: Configure systemd Service (Oracle Server)

Your service needs to be managed by systemd so GitHub Actions can restart it cleanly.

# On your Oracle server
sudo nano /etc/systemd/system/newsletter.service

Paste this template (adjust newsletter to your binary name):

[Unit]
Description=Newsletter Rust Service
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/newsletter
Environment=PATH=/home/ubuntu/.cargo/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/home/ubuntu/newsletter/target/release/newsletter
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

Enable and test:

sudo systemctl daemon-reload
sudo systemctl enable newsletter
sudo systemctl start newsletter
sudo systemctl status newsletter

If status shows "active (running)", you're good. If not, check logs:

sudo journalctl -u newsletter -n 50  # Last 50 log lines

Step 4: Add GitHub Actions Secrets

GitHub Actions workflows can't run if they don't have credentials. Store your deploy key and server info as repository secrets.

In your GitHub repository:

  1. Go to Settings → Secrets and variables → Actions
  2. Click "New repository secret"
  3. Add three secrets:
Secret Name Value
SSH_HOST 80.225.191.45 (or your Oracle IP)
SSH_USER ubuntu
SSH_PRIVATE_KEY (Paste entire content of ~/.ssh/rust_deploy, including -----BEGIN... and -----END... lines)

⚠️ Security note: GitHub encrypts these and never displays them in logs. The *** masking you see in CI logs is intentional.


Step 5: Create the GitHub Actions Workflow

Create this file in your repository:

.github/workflows/rust.yml

name: Rust CI/CD

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

env:
  CARGO_TERM_COLOR: always
  SQLX_OFFLINE: true

jobs:
  # === CONTINUOUS INTEGRATION ===
  build:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:13
        env:
          POSTGRES_PASSWORD: postgres
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
    - uses: actions/checkout@v4

    - name: Cache Cargo dependencies
      uses: actions/cache@v3
      with:
        path: |
          ~/.cargo/registry
          ~/.cargo/git
          target
        key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
        restore-keys: |
          ${{ runner.os }}-cargo-

    - name: Check code formatting
      run: cargo fmt --check --all

    - name: Cargo check
      run: cargo check
      env:
        DATABASE_URL: postgres://postgres:postgres@127.0.0.1:5432/postgres
        SQLX_OFFLINE: false

    - name: Run Clippy linter
      run: cargo clippy --all-targets --all-features -- -D warnings
      env:
        DATABASE_URL: postgres://postgres:postgres@127.0.0.1:5432/postgres
        SQLX_OFFLINE: false

    - name: Run tests
      run: cargo test
      env:
        DATABASE_URL: postgres://postgres:postgres@127.0.0.1:5432/postgres
        SQLX_OFFLINE: false

  # === CONTINUOUS DEPLOYMENT ===
  deploy:
    needs: build  # Only deploy if CI passes
    if: github.ref == 'refs/heads/master'  # Only on master branch
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Deploy to Oracle Cloud
      uses: appleboy/ssh-action@v1.0.3
      with:
        host: ${{ secrets.SSH_HOST }}
        username: ${{ secrets.SSH_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd /home/ubuntu/newsletter
          
          # Ensure Rust toolchain is available
          curl -sSf https://sh.rustup.rs | sh -s -- -y
          source ~/.cargo/env
          
          # Pull latest code
          git pull origin master
          
          # Build release binary
          cargo build --release --bin newsletter
          
          # Restart service with new binary
          sudo systemctl stop newsletter || true
          sleep 2
          sudo systemctl daemon-reload
          sudo systemctl restart newsletter
          sleep 3
          
          # Verify service is running
          sudo systemctl status newsletter --no-pager -l

Key Features of This Workflow:

Dependency caching – Speeds up subsequent builds (Cargo cache) ✅ Database integration – Postgres container for integration tests ✅ Code quality gatesfmt, check, clippy run before deployment ✅ Conditional deployment – Only deploys on master push if CI passes ✅ Automatic binary building – Builds directly on Oracle VM ✅ Service management – Graceful systemd restarts


Step 6: Test the Pipeline

# Make a small change to your code
echo "// CI/CD test" >> src/main.rs

# Commit and push to master
git add .
git commit -m "Test CI/CD pipeline"
git push origin master

Watch in real-time:

  • Go to your GitHub repo → Actions tab
  • Click the running workflow
  • Expand each step to see logs

You should see:

  1. ✅ Build job: tests pass
  2. ✅ Deploy job: SSH connects, pulls code, builds, restarts service

If deployment fails, the error messages are verbose—scroll through the logs to debug.


How Much Does This Actually Cost?

Component Monthly Cost Notes
GitHub Actions $0 Free tier: 2,000 min/month for private repos
Oracle Cloud VM $0 Free tier: 1x AMD VM (2 vCPU, 12GB RAM)
Custom domain (optional) ~$10 Not required for CI/CD
Storage/backup $0 Free tier includes 10GB
Total ~$0-10 Wildly cheaper than platforms like GitLab

If you exceed free tier limits (building 2,000+ minutes/month), GitHub Actions billing is $0.35/minute—still vastly cheaper than enterprise platforms.


Troubleshooting Common Issues

"Permission denied (publickey)" during deploy

  • Verify SSH key is in ~/.ssh/authorized_keys on Oracle server
  • Test manually: ssh -i ~/.ssh/rust_deploy ubuntu@YOUR_IP

"cargo: command not found"

  • The workflow installs Rust, but first build might fail if system Rust isn't present
  • Solution: Run one manual cargo build on Oracle server to set up environment

Service fails to start after deployment

  • Check logs: sudo journalctl -u newsletter -n 50
  • Verify binary path matches ExecStart in systemd service file
  • Ensure binary has execute permissions: ls -la target/release/newsletter

Deployment takes too long

  • First build compiles everything (~5-10 min)
  • Subsequent builds use Cargo cache (~1-2 min)
  • Consider adding --incremental or --codegen-units for faster builds

Production Hardening

Once you're comfortable, add these improvements:

1. Database Migrations on Deploy

- name: Run migrations
  run: |
    sqlx migrate run --database-url postgres://... || true

2. Slack/Discord Notifications

- name: Notify deployment
  run: |
    curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
      -d 'Deployed to production: ${{ github.sha }}'

3. Rollback Strategy

Keep the previous binary as backup:

sudo mv target/release/newsletter target/release/newsletter.new
sudo mv target/release/newsletter.bak target/release/newsletter || true
sudo mv target/release/newsletter.new target/release/newsletter.bak

4. Environment-Specific Configuration

# In deploy script
if [ -f /home/ubuntu/newsletter/.env.prod ]; then
  source /home/ubuntu/newsletter/.env.prod
fi

Why This Approach Wins

Aspect Jenkins/GitLab Our Approach
Cost $100-500/month $0
Setup time Days 30 minutes
Maintenance burden High (server updates, security patches) Minimal (GitHub handles it)
Deployment time 2-5 minutes 1-3 minutes
Learning curve Steep (pipelines, agents, runners) Shallow (bash + systemd)
Scalability Manual scaling Auto-scales free tier

For solo developers and small teams, this is unbeatable. For enterprises with 50+ deployments/day, you might need more sophisticated tooling—but even then, this architecture serves as a cost-effective foundation.


Real-World Example: Timeline

15:30 → Developer pushes commit to master
15:31 → GitHub Actions workflow triggered
15:32 → CI: cargo test completes (2,000+ tests pass)
15:33 → CI: clippy linter runs (0 warnings)
15:34 → Deploy: SSH to Oracle, git pull, cargo build starts
15:38 → Deploy: cargo build --release completes
15:39 → Deploy: systemctl restart newsletter
15:40 → Service live with new code ✅

Total time: 10 minutes, $0 cost, zero manual intervention

Next Steps

  1. Try this today – Set up the pipeline with your existing Rust project
  2. Monitor in production – Watch sudo journalctl -u newsletter -f during first deployment
  3. Iterate – Add Slack notifications, automated rollbacks, or database migrations as needed
  4. Share your setup – Tell your team about this approach—it works for any Rust service

Conclusion

You don't need expensive CI/CD platforms to ship production code reliably. GitHub Actions + SSH + systemd gives you:

  • ✅ Automated testing on every push
  • ✅ Zero-downtime deployments
  • ✅ Auditable deployment history
  • ✅ Cost savings of $1,200-6,000/year
  • ✅ Zero infrastructure maintenance

Start small, deploy confidently, and save money doing it.


Resources


Questions? This setup works for Node.js, Python, Go, and any language with a CLI toolchain. Adapt the cargo build step to your language's build command.

Happy deploying! 🚀