January 28, 2026
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.
| 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.
┌─────────────────────────────────────────────────────────────┐
│ 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.
What you need:
Time estimate: 30 minutes to set up, 5 minutes per deployment after
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
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
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
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:
| 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.
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
✅ Dependency caching – Speeds up subsequent builds (Cargo cache)
✅ Database integration – Postgres container for integration tests
✅ Code quality gates – fmt, 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
# 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:
You should see:
If deployment fails, the error messages are verbose—scroll through the logs to debug.
| 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.
~/.ssh/authorized_keys on Oracle serverssh -i ~/.ssh/rust_deploy ubuntu@YOUR_IPcargo build on Oracle server to set up environmentsudo journalctl -u newsletter -n 50ExecStart in systemd service filels -la target/release/newsletter--incremental or --codegen-units for faster buildsOnce you're comfortable, add these improvements:
- name: Run migrations
run: |
sqlx migrate run --database-url postgres://... || true
- name: Notify deployment
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-d 'Deployed to production: ${{ github.sha }}'
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
# In deploy script
if [ -f /home/ubuntu/newsletter/.env.prod ]; then
source /home/ubuntu/newsletter/.env.prod
fi
| 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.
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
sudo journalctl -u newsletter -f during first deploymentYou don't need expensive CI/CD platforms to ship production code reliably. GitHub Actions + SSH + systemd gives you:
Start small, deploy confidently, and save money doing it.
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! 🚀