Deploying Applications from Git
RunOS enables you to deploy your own applications directly from GitHub repositories with a Heroku-like experience. Push your code to Git, and RunOS handles the build, deployment, SSL certificates, and scaling automatically.
What You Can Deploy
Deploy any application that can run in a Docker container:
- Web applications - Node.js, Python, Ruby, Go, PHP, Java
- APIs and microservices - REST APIs, GraphQL servers, gRPC services
- Background workers - Job queues, scheduled tasks, data processors
- Static sites - React, Vue, Next.js, or any static site generator
Prerequisites
Before deploying your first application, ensure you have:
Required Services
-
Harbor - Container registry for storing built Docker images
- Deploy from Services → Marketplace → Harbor
- Harbor uses MinIO as its storage backend
-
MinIO - Object storage for Harbor's image storage
- Deploy from Services → Marketplace → MinIO
- Harbor automatically configures MinIO
-
Traefik - Ingress controller (automatically included with RunOS)
- Handles HTTP/HTTPS routing to your applications
- Manages SSL certificate requests
First-Time Setup: When you navigate to Applications from the top navigation for the first time without these services installed, the RunOS console will automatically guide you through installing Harbor and MinIO with recommended configurations. Simply follow the prompts to set up the required infrastructure.
GitHub Repository
Your application repository must include a Dockerfile:
- BuildKit: Requires
Dockerfilein the repository root - GitHub Actions (ARC): Dockerfile location is flexible, defined in your workflow file
Deployment Methods
RunOS offers two build strategies, each with different trade-offs:
BuildKit (In-Cluster Builds)
How it works:
- Builds run inside your Kubernetes cluster using BuildKit
- Simple one-click setup with no external configuration
- Supports multi-architecture builds (AMD64/ARM64)
Advantages:
- ✅ Quick setup - just connect your GitHub account
- ✅ No external infrastructure needed
- ✅ Builds run on your own servers
- ✅ Multi-architecture support out of the box
Requirements:
- GitHub authentication to RunOS
- Manual "Deploy Now" button click to trigger builds
Best for:
- Getting started quickly
- Simple deployment workflows
- Teams that prefer manual deployment control
GitHub Actions Runner Controller (ARC)
How it works:
- Self-hosted GitHub Actions runners run in your cluster
- GitHub workflows trigger builds automatically
- You own the GitHub App and control all credentials
- RunOS acts on behalf of your GitHub App to pull code
Advantages:
- ✅ Automatic deployments on git push
- ✅ Full GitHub Actions workflow customization
- ✅ You own and control the GitHub App
- ✅ Supports complex CI/CD pipelines
Requirements:
- GitHub organization (not personal accounts)
- Create and manage an organization-level GitHub App
- Provide RunOS with GitHub App credentials (App ID, Installation ID, Private Key)
- Add a workflow file to your repository
Best for:
- Production environments
- Teams wanting automated deployments
- Complex CI/CD workflows
- Organizations with existing GitHub Actions experience
Setting Up Integrations
Before deploying applications, you need to add a GitHub integration. RunOS supports two integration types: BuildKit and GitHub Actions Runner Controller (ARC).
Adding an Integration
- Navigate to Applications in the main menu
- Click Add Integration in the top right
- Select your integration type:
- BuildKit - For simple, manual deployments
- GitHub Self-Hosted Runners (ARC) - For automated CI/CD pipelines
Once you've added an integration, you can create applications that use it.
Deploying with BuildKit
This is the fastest way to get started. BuildKit requires authentication to the main RunOS application, which allows RunOS to pull code from your private GitHub repositories.
Step 1: Add BuildKit Integration
- Navigate to Applications in the main menu
- Click Add Integration in the top right
- Select BuildKit
- Click Authorize GitHub to connect your account
- Grant RunOS access to your repositories
- Complete the integration setup
Step 2: Create Application
- Click Add Application (or the + button)
- Select your BuildKit integration from the dropdown
- Configure your application:
Application Details:
- Name - Human-readable name for your application
- Integration - Select your BuildKit integration
- Repository - Select from your GitHub repositories
- Branch - Choose the branch to deploy (e.g.,
main,production)
Build Configuration:
- Platform - Select target architectures:
- AMD64 (default, most common)
- ARM64 (for ARM-based servers)
- Both (multi-architecture images)
Resource Allocation:
- CPU - Millicores (1000 mC = 1 CPU core)
- Requests: Guaranteed CPU (default: 250 mC)
- Limits: Maximum CPU (default: 500 mC)
- Memory - Megabytes
- Requests: Guaranteed memory (default: 500 MB)
- Limits: Maximum memory (default: 1000 MB)
Replicas:
- Number of pods to run (default: 1)
- Increase for high availability and load balancing
Step 3: Configure Ports and Domains
Port Configuration:
Each application can expose multiple ports. For each port, specify:
- Port number (1-65535) - The port your application listens on
- Domains - One or more domains/paths to access this port
Access Methods:
For each port, your application is automatically accessible via:
-
RunOS Cluster Domain (always created automatically):
https://app-{appId}-{port}.{clusterDomain}Example:
https://app-abc123-8080.cluster.runos.io -
Custom Domain with SSL:
- Enter your domain (e.g.,
myapp.com) - Enter path (default:
/) - Check Request SSL Certificate
- Point your domain DNS to any worker node IP
- Ensure port 80 is open for Let's Encrypt verification
- Access via:
https://myapp.com
- Enter your domain (e.g.,
-
Custom Domain without SSL:
- Enter your domain
- Leave Request SSL Certificate unchecked
- Access via:
http://myapp.com:8891
Multiple ports example:
- Port 8080: Web UI on
app.example.com - Port 8081: API on
api.example.com - Port 9090: Metrics on cluster domain only
Step 4: Environment Variables (Optional)
Add environment variables your application needs:
NODE_ENV=production
DATABASE_URL=postgresql://user:pass@host:5432/db
API_KEY=your-secret-key
# Lines starting with # are comments
Format:
- One variable per line:
KEY=VALUE - Everything after first
=becomes the value - Comments start with
# - Empty lines ignored
Security:
- Environment variables are stored as Kubernetes secrets
- Encrypted at rest
- Never logged or exposed in the console
Step 5: Secure Files (Optional)
Upload sensitive files your application needs (certificates, credential files, etc.):
Supported files:
- SSL certificates (
.crt,.pem) - Private keys
- Service account credentials (
.json) - Configuration files
- Maximum size: 32KB per file
Mount Options:
Read-Only (Non-Deletable):
- File permanently mounted from Kubernetes secret
- Cannot be modified or deleted by application
- Best for certificates and static credentials
- Example path:
/app/certs/ssl.crt
Writable (Deletable):
- File copied to writable volume on startup
- Application can modify or delete
- Useful for files loaded into memory then deleted for security
- Example: Load credential, delete file after reading
Upload Process:
- Click Add Secure File
- Drag and drop or select file
- Specify mount path (e.g.,
/app/config/credentials.json) - Choose deletable or non-deletable
- SHA256 hash calculated automatically for verification
Step 6: Deploy
-
Review all configuration
-
Click Create Application
-
RunOS will:
- Create dedicated namespace:
app-{appId} - Set up Harbor credentials
- Prepare routing and ingress
- Save configuration
- Create dedicated namespace:
-
Click Deploy Now to trigger first build
-
Monitor build progress in the application detail page
Build Process:
- BuildKit pulls your code from GitHub
- Builds Docker image using your Dockerfile
- Pushes image to Harbor:
{harbor}/runos-apps/{appId}:{commitHash} - Deploys to Kubernetes
- Creates ingress rules for domains
First deployment: 3-10 minutes depending on image size Subsequent deployments: 1-5 minutes
Step 7: Redeploy When Needed
With BuildKit, deployments are manual:
- Push code changes to GitHub
- Navigate to Applications → Manage → [Your App]
- Click Deploy Now
- RunOS pulls latest code and rebuilds
Why manual? We're gathering user feedback on automatic deployment triggers before implementing them. Most teams prefer manual control for production deployments.
Deploying with GitHub Actions Runner Controller (ARC)
For automatic deployments and full CI/CD control, use self-hosted GitHub Actions runners. This requires more setup but provides complete automation.
Step 1: Add ARC Integration
ARC requires an organization-level GitHub App that you own and control. RunOS will act on behalf of your app to pull code.
- Navigate to Applications in the main menu
- Click Add Integration in the top right
- Select GitHub Self-Hosted Runners (ARC)
- Follow the guided setup wizard in the RunOS console
The console will guide you through:
- Creating a GitHub App in your organization
- Setting the required permissions
- Generating and providing a private key
- Installing the app on your organization
- Providing the Installation ID
What you need:
- A GitHub organization (not a personal account)
- Admin access to the organization
- Ability to create GitHub Apps
Security Note: You own and control the GitHub App. RunOS stores the private key as a base64-encoded Kubernetes secret in your cluster - it never leaves your infrastructure.
Step 2: Create Application
- Click Add Application (or the + button)
- Select your ARC integration from the dropdown
- Configure your application:
Application Details:
- Name - Human-readable name for your application
- Integration - Select your ARC integration
- Repository - Select from your GitHub repositories
- Branch - Choose the branch to deploy (e.g.,
main,production)
Configuration: Similar to BuildKit, configure:
- Resource allocation (CPU, memory, replicas)
- Environment variables
- Ports and domains
- Secure files
Key Difference: No manual "Deploy Now" button. Deployments trigger via GitHub workflows.
Step 3: Add Workflow to Repository
RunOS generates a sample workflow file for you. After creating your application:
- Copy the generated workflow from the success page
- In your repository, create:
.github/workflows/runos-ci-{accountId}-{cid}.yaml - Paste the workflow content
- Commit and push to your repository
Sample Workflow Structure:
name: RunOS CI/CD
on:
push:
branches: [main] # Your deployment branch
workflow_dispatch: # Manual trigger option
jobs:
build-and-deploy:
runs-on: runos-default-arc-runners-{accountId}-{cid}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup kubectl and credentials
run: |
# Sets up Harbor credentials from Kubernetes
- name: Build and push to Harbor
run: |
# Builds image and pushes to Harbor registry
- name: Deploy to Kubernetes
run: |
# Updates Kubernetes deployment with new image
Step 4: Automatic Deployments
With ARC integration and workflow configured:
- Push to your deployment branch (e.g.,
main) - GitHub automatically triggers workflow
- Workflow runs on your self-hosted runner in the cluster
- Application builds, pushes to Harbor, and deploys
- Monitor progress in GitHub Actions tab
Deployment time: 2-8 minutes depending on build complexity
Customization:
- Modify workflow to add tests, linting, security scans
- Add deployment approvals or gates
- Customize build steps for your stack
- Deploy on tags, pull requests, or schedules
SSL Certificates and Let's Encrypt
RunOS uses Let's Encrypt to provide free SSL certificates for your custom domains.
Requirements for SSL
To request an SSL certificate for a custom domain:
-
Domain DNS must point to a worker node IP address
- Use an A record:
myapp.com → 203.0.113.50 - Or CNAME to worker node hostname
- Use an A record:
-
Port 80 must be open on the worker node
- Let's Encrypt validates ownership via HTTP-01 challenge
- Traefik handles the challenge automatically
- Port can be closed after initial certificate issuance
-
Check "Request SSL Certificate" when adding the domain
Certificate Process
- You add domain and check Request SSL Certificate
- You deploy application
- Traefik (via cert-manager) detects new domain
- Let's Encrypt sends HTTP challenge to
http://yourdomain.com/.well-known/acme-challenge/{token} - Traefik responds automatically
- Let's Encrypt issues certificate
- Certificate automatically installed
- Your app becomes accessible via
https://yourdomain.com
Certificate renewal: Automatic every 60 days (Let's Encrypt certs expire after 90 days)
Troubleshooting SSL
Certificate not issuing:
- Verify DNS points to worker node:
dig yourdomain.com - Verify port 80 is open:
curl http://yourdomain.com/.well-known/acme-challenge/test - Check cert-manager logs:
kubectl logs -n cert-manager -l app=cert-manager
Certificate pending forever:
- Let's Encrypt has rate limits (50 certs per domain per week)
- DNS may not have propagated (wait 24-48 hours)
- Firewall may be blocking port 80
Using External Load Balancers
If you're using an external load balancer (AWS ALB, Azure Load Balancer, HAProxy, etc.) in front of your RunOS cluster, you have two SSL termination options:
Option 1: SSL Termination on Load Balancer
Terminate SSL at the load balancer and forward unencrypted traffic to RunOS:
Configuration:
- Configure SSL certificate on your load balancer
- Forward traffic to RunOS nodes on port 8891 (plain HTTP)
- Do NOT check "Request SSL Certificate" when adding the domain in RunOS
- Ensure secure network between load balancer and RunOS nodes (VPC, VPN, or private network)
Traffic Flow:
User → HTTPS (443) → Load Balancer → HTTP (8891) → RunOS Node → Application
Benefits:
- ✅ Centralized certificate management on load balancer
- ✅ No port 80 requirement on RunOS nodes
- ✅ SSL offloading reduces RunOS node workload
Security Note: The network between your load balancer and RunOS nodes must be secure. Use:
- Private VPC/subnet
- VPN tunnels
- Firewall rules restricting 8891 to load balancer IPs only
Option 2: SSL Termination on RunOS (End-to-End Encryption)
Let RunOS handle SSL certificates via Let's Encrypt:
Configuration:
- Point domain DNS to RunOS worker node (not load balancer)
- Configure load balancer to pass through traffic (TCP mode, port 443)
- CHECK "Request SSL Certificate" when adding the domain in RunOS
- Ensure port 80 is accessible for Let's Encrypt validation
Traffic Flow:
User → HTTPS (443) → Load Balancer (passthrough) → HTTPS (443) → RunOS Node → Application
Benefits:
- ✅ End-to-end encryption
- ✅ Free automatic certificates via Let's Encrypt
- ✅ No certificate management on load balancer
Requirements:
- Load balancer must support TCP passthrough mode
- Port 80 must be accessible for Let's Encrypt HTTP-01 challenge
- DNS points directly to RunOS nodes
Understanding Application URLs
Every deployed application gets multiple URLs automatically:
1. RunOS Cluster Domain (Always Created)
For each exposed port:
https://app-{appId}-{port}.{clusterDomain}
Characteristics:
- ✅ Always works immediately
- ✅ Automatic SSL certificate
- ✅ No DNS configuration needed
- ✅ Accessible from anywhere
Example:
- App ID:
abc123 - Port:
8080 - Cluster domain:
cluster.runos.io - URL:
https://app-abc123-8080.cluster.runos.io
2. Custom Domain with SSL
When you add a custom domain with SSL requested:
https://yourdomain.com{path}
Characteristics:
- ✅ Your own domain
- ✅ Free Let's Encrypt SSL
- ✅ Automatic renewal
- ⚠️ Requires DNS configuration
- ⚠️ Requires port 80 open
3. Custom Domain without SSL
When you add a custom domain without SSL:
http://yourdomain.com:8891{path}
Characteristics:
- ✅ Your own domain
- ✅ No port 80 requirement
- ⚠️ No encryption (use behind load balancer or internal services only)
- ⚠️ Requires port 8891 open
Note: This option is commonly used when SSL is terminated on an external load balancer (see Using External Load Balancers section).
4. Internal Service URL
For service-to-service communication within cluster:
http://app-{appId}.app-{appId}.svc.cluster.local:{port}
Characteristics:
- ✅ Fast (no external routing)
- ✅ Always available
- ⚠️ Only accessible from other pods in cluster
Managing Deployed Applications
Application Dashboard
Navigate to Applications → Manage → [Your App] to see:
Overview Tab:
- Application status (Healthy/Degraded/Unhealthy)
- Current image and commit hash
- Replica count and resource usage
- Access URLs
- Recent deployments
Pods Tab:
- Live view of all running pods
- Pod status, restarts, age
- Resource usage per pod
- Quick restart action
Logs Tab:
- Real-time logs from all pods
- Search and filter
- Download logs
- Tail control (last N lines)
Metrics Tab:
- CPU usage over time
- Memory usage over time
- Network traffic
- Request rates (if exposed)
Common Actions
Update Configuration:
- Click Edit in application dashboard
- Modify environment variables, resources, or domains
- Click Save
- Changes apply on next deployment
Redeploy:
- BuildKit: Click Deploy Now
- ARC: Push to Git or trigger workflow manually
Restart Pods:
- Go to Pods tab
- Click Restart on specific pod, OR
- Click Restart All in Danger Zone
Scale Replicas:
- Click Edit
- Change replica count
- Save and redeploy
Delete Application:
- Scroll to Danger Zone
- Click Delete Application
- Confirm deletion
- Namespace
app-{appId}and all resources deleted
Best Practices
Security
Secrets Management:
- ✅ Use environment variables for secrets, not hardcoded
- ✅ Upload sensitive files via Secure Files feature
- ✅ Use deletable files for one-time credentials
- ❌ Never commit secrets to Git
- ❌ Never log secrets in application code
Network Security:
- ✅ Use HTTPS for all public-facing applications
- ✅ Use internal service URLs for service-to-service communication
- ✅ Limit exposed ports to only what's necessary
- ❌ Don't expose debug ports publicly
Resource Allocation
Right-sizing:
- Start small (250 mC CPU, 500 MB memory)
- Monitor actual usage in Metrics tab
- Increase if hitting limits (pod restarts, slow performance)
- Set limits slightly higher than requests
CPU:
- 100-250 mC: Simple APIs, static sites
- 250-500 mC: Web apps, small databases
- 500-1000 mC: Heavy processing, batch jobs
- 1000+ mC: CPU-intensive workloads
Memory:
- 128-256 MB: Minimal applications
- 256-512 MB: Simple web apps
- 512-1024 MB: Standard applications
- 1024-2048 MB: In-memory caching, large apps
- 2048+ MB: Heavy memory usage
High Availability
Multiple Replicas:
- Set replicas to 2+ for production applications
- Kubernetes distributes across nodes automatically
- If one pod fails, others continue serving traffic
- Load balancing handled by Traefik
Resource Limits:
- Set both requests and limits
- Prevents one app from consuming all node resources
- Kubernetes reschedules pods if node fails
Health Checks (Advanced):
- Implement
/healthor/readyendpoints - Configure liveness and readiness probes in Kubernetes
- Automatic pod replacement on health check failures
Deployment Strategy
Development Workflow:
- Use BuildKit for simplicity
- Deploy to development cluster frequently
- Test thoroughly before promoting to production
Production Workflow:
- Use ARC for automatic deployments
- Deploy to staging environment first
- Use Git tags or release branches
- Monitor logs and metrics after deployment
Rollback:
- If deployment fails, previous version keeps running
- Revert Git commit and push
- Or manually specify previous image in deployment
Comparison: BuildKit vs ARC
| Feature | BuildKit | GitHub Actions (ARC) |
|---|---|---|
| Setup Complexity | Simple (5 minutes) | Complex (20 minutes) |
| GitHub Requirement | GitHub account | GitHub organization |
| Deployment Trigger | Manual "Deploy Now" | Automatic on push |
| Customization | Limited | Full workflow control |
| Build Location | In-cluster | In-cluster |
| Multi-arch Support | ✅ Yes | ✅ Yes (via workflow) |
| Workflow Files | Not needed | Required in repo |
| CI/CD Pipeline | Not supported | Fully supported |
| Best For | Development, simple apps | Production, automated pipelines |
| GitHub App Ownership | RunOS manages | You own and control |
| Credentials | RunOS GitHub auth | Your GitHub App credentials |
Troubleshooting
Build Failures
"Failed to pull repository"
- Verify GitHub integration is connected
- Check repository access permissions
- Re-authorize GitHub if token expired
"Dockerfile not found"
- Ensure
Dockerfileexists in repository root - Check Dockerfile capitalization (case-sensitive)
- Verify selected branch has Dockerfile
"Build timeout"
- Increase build resources in cluster
- Optimize Dockerfile (use smaller base images)
- Use Docker layer caching
Deployment Issues
"ImagePullBackOff"
- Harbor credentials may be incorrect
- Check Harbor is running:
kubectl get pods -n {harborOsid} - Verify image was pushed successfully
Pods in "CrashLoopBackOff"
- Application is starting but crashing
- Check logs: Applications → Manage → Logs
- Verify environment variables are correct
- Check resource limits aren't too low
"No healthy pods"
- Check all pods status in Pods tab
- Review events in pod details
- Verify port configuration matches application
Domain and SSL Issues
Domain not accessible
- Verify DNS:
dig yourdomain.com - Check domain is added in port configuration
- Ensure Traefik is running:
kubectl get pods -n traefik
SSL certificate not issued
- Verify port 80 is open on worker node
- Check cert-manager is running:
kubectl get pods -n cert-manager - View certificate status:
kubectl get certificate -n app-{appId} - Let's Encrypt rate limits may apply
"Your connection is not private" SSL error
- Certificate may still be issuing (wait 2-5 minutes)
- Check certificate status in application dashboard
- Verify domain DNS is correct
Next Steps
Now that you know how to deploy applications:
- Deploy a sample app - Try with a simple Node.js or Python app first
- Add monitoring - Deploy Grafana from marketplace to visualize metrics
- Set up alerts - Configure alerts based on application health
- Explore services - Connect your app to PostgreSQL, Redis, or other marketplace services
- Scale up - Add more nodes for high availability
Related Guides:
- Service Marketplace - Deploy databases and services
- Cluster Management - Add nodes and scale
- Observability - Monitor logs and metrics
- Security Best Practices - Secure your applications