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! 🚀