Overview
Modrinth uses different deployment strategies for its various components:
Frontend (Web) : Cloudflare Pages with SSR
Backend (Labrinth) : Docker containers on Kubernetes
Desktop App : GitHub Releases with auto-updater
Documentation : Cloudflare Pages (static)
Frontend Deployment (Cloudflare Pages)
Build Process
The web frontend is deployed to Cloudflare Pages with Nuxt’s Cloudflare preset.
Build command :
NITRO_PRESET = cloudflare-pages pnpm --filter frontend run build
Output : .output/ directory with:
Static assets in .output/public/
Server functions in .output/server/
CI/CD Workflow
.github/workflows/frontend-deploy.yml
name : Deploy Frontend
on :
push :
branches :
- main
paths :
- 'apps/frontend/**'
- 'packages/ui/**'
- 'packages/api-client/**'
jobs :
deploy :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- uses : pnpm/action-setup@v2
with :
version : 9.15.0
- uses : actions/setup-node@v4
with :
node-version : 20
cache : 'pnpm'
- name : Install dependencies
run : pnpm install
- name : Build
run : pnpm pages:build
env :
NUXT_PUBLIC_LABRINTH_URL : https://api.modrinth.com
NUXT_PUBLIC_ARCHON_URL : https://archon.modrinth.com
- name : Deploy to Cloudflare Pages
uses : cloudflare/pages-action@v1
with :
apiToken : ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId : ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName : modrinth-web
directory : apps/frontend/.output/public
gitHubToken : ${{ secrets.GITHUB_TOKEN }}
Preview Deployments
Every pull request gets an automatic preview deployment:
.github/workflows/frontend-preview.yml
name : Preview Deployment
on :
pull_request :
paths :
- 'apps/frontend/**'
- 'packages/ui/**'
jobs :
preview :
runs-on : ubuntu-latest
steps :
# ... same build steps ...
- name : Deploy Preview
uses : cloudflare/pages-action@v1
with :
apiToken : ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId : ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName : modrinth-web
directory : apps/frontend/.output/public
branch : pr-${{ github.event.pull_request.number }}
Preview URL: https://pr-{number}.modrinth-web.pages.dev
Environment Variables
Production environment variables are configured in Cloudflare Pages:
NUXT_PUBLIC_LABRINTH_URL = https://api.modrinth.com
NUXT_PUBLIC_ARCHON_URL = https://archon.modrinth.com
NUXT_PUBLIC_KYROS_URL = https:// { node}.nodes.modrinth.com
GITHUB_CLIENT_ID = ...
GITHUB_CLIENT_SECRET = ...
RATE_LIMIT_KEY = ...
SSR Configuration
Nuxt is configured for server-side rendering:
export default defineNuxtConfig ({
nitro: {
preset: 'cloudflare-pages' ,
compressPublicAssets: true ,
} ,
routingRules: {
'/api/**' : { cache: { maxAge: 0 } },
'/_nuxt/**' : { cache: { maxAge: 31536000 } },
} ,
})
Rollback
Rollback to previous deployment:
# Via Cloudflare dashboard
# Pages > modrinth-web > Deployments > Rollback
# Or redeploy previous commit
git revert HEAD
git push origin main
Backend Deployment (Docker + Kubernetes)
Docker Build
Labrinth is deployed as a Docker container.
Dockerfile :
FROM rust:1.90 as builder
WORKDIR /app
COPY . .
# Build with release profile
RUN cargo build -p labrinth --profile release-labrinth
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
ca-certificates \
libssl3 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release-labrinth/labrinth /usr/local/bin/labrinth
EXPOSE 8000
CMD [ "labrinth" ]
CI/CD Workflow
.github/workflows/labrinth-docker.yml
name : Build Labrinth Docker Image
on :
push :
branches :
- main
paths :
- 'apps/labrinth/**'
- 'packages/ariadne/**'
- 'packages/daedalus/**'
- 'Cargo.toml'
- 'Cargo.lock'
jobs :
docker :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- name : Set up Docker Buildx
uses : docker/setup-buildx-action@v2
- name : Login to GitHub Container Registry
uses : docker/login-action@v2
with :
registry : ghcr.io
username : ${{ github.actor }}
password : ${{ secrets.GITHUB_TOKEN }}
- name : Extract metadata
id : meta
uses : docker/metadata-action@v5
with :
images : ghcr.io/modrinth/labrinth
tags : |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name : Build and push
uses : docker/build-push-action@v5
with :
context : .
file : ./apps/labrinth/Dockerfile
push : true
tags : ${{ steps.meta.outputs.tags }}
labels : ${{ steps.meta.outputs.labels }}
cache-from : type=gha
cache-to : type=gha,mode=max
Release Profile
Production builds use optimized settings:
[ profile . release-labrinth ]
inherits = "release"
opt-level = "s" # Optimize for size
strip = false # Keep debug symbols for Sentry
lto = true # Link-time optimization
panic = "unwind" # Don't exit on panic (allow recovery)
codegen-units = 1 # Better optimization
Kubernetes Deployment
Deployment manifest :
k8s/labrinth-deployment.yaml
apiVersion : apps/v1
kind : Deployment
metadata :
name : labrinth
namespace : modrinth
spec :
replicas : 3
selector :
matchLabels :
app : labrinth
template :
metadata :
labels :
app : labrinth
spec :
containers :
- name : labrinth
image : ghcr.io/modrinth/labrinth:latest
ports :
- containerPort : 8000
env :
- name : DATABASE_URL
valueFrom :
secretKeyRef :
name : labrinth-secrets
key : database-url
- name : REDIS_URL
valueFrom :
secretKeyRef :
name : labrinth-secrets
key : redis-url
resources :
requests :
memory : "512Mi"
cpu : "500m"
limits :
memory : "2Gi"
cpu : "2000m"
livenessProbe :
httpGet :
path : /health
port : 8000
initialDelaySeconds : 30
periodSeconds : 10
readinessProbe :
httpGet :
path : /health
port : 8000
initialDelaySeconds : 5
periodSeconds : 5
Service :
k8s/labrinth-service.yaml
apiVersion : v1
kind : Service
metadata :
name : labrinth
namespace : modrinth
spec :
selector :
app : labrinth
ports :
- port : 80
targetPort : 8000
type : LoadBalancer
Database Migrations
Migrations are run before deploying new version:
# Connect to production database (read-only first!)
psql $DATABASE_URL
# Test migration in transaction
BEGIN ;
\i migrations/XXXXXX_new_migration.sql
ROLLBACK ; -- or COMMIT if looks good
# Run migrations via SQLx
cargo sqlx migrate run --database-url $DATABASE_URL
Always test migrations on staging first! Never run untested migrations on production.
Rolling Updates
# Update image
kubectl set image deployment/labrinth \
labrinth=ghcr.io/modrinth/labrinth:new-version \
-n modrinth
# Monitor rollout
kubectl rollout status deployment/labrinth -n modrinth
# Rollback if needed
kubectl rollout undo deployment/labrinth -n modrinth
Health Checks
Labrinth exposes health endpoints:
use actix_web :: {web, HttpResponse };
pub async fn health () -> HttpResponse {
HttpResponse :: Ok () . json ( json! ({
"status" : "ok" ,
"version" : env! ( "CARGO_PKG_VERSION" ),
}))
}
pub async fn ready (
pool : web :: Data < PgPool >,
) -> HttpResponse {
// Check database connection
if sqlx :: query ( "SELECT 1" ) . execute ( pool . get_ref ()) . await . is_ok () {
HttpResponse :: Ok () . json ( json! ({ "status" : "ready" }))
} else {
HttpResponse :: ServiceUnavailable () . json ( json! ({ "status" : "not ready" }))
}
}
Desktop App Deployment
Build Process
The desktop app is built for all platforms using GitHub Actions.
.github/workflows/theseus-build.yml
name : Build Desktop App
on :
push :
tags :
- 'v*.*.*'
jobs :
build :
strategy :
matrix :
platform :
- macos-latest
- ubuntu-latest
- windows-latest
runs-on : ${{ matrix.platform }}
steps :
- uses : actions/checkout@v4
- uses : dtolnay/rust-toolchain@stable
- uses : pnpm/action-setup@v2
with :
version : 9.15.0
- uses : actions/setup-node@v4
with :
node-version : 20
cache : 'pnpm'
- name : Install dependencies
run : pnpm install
- name : Install platform dependencies (Linux)
if : matrix.platform == 'ubuntu-latest'
run : |
sudo apt update
sudo apt install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev
- name : Build app
run : pnpm app:build
- name : Upload artifacts
uses : actions/upload-artifact@v4
with :
name : app-${{ matrix.platform }}
path : |
apps/app/src-tauri/target/release/bundle/
Release Process
Create Tag
git tag v1.2.3
git push origin v1.2.3
This triggers the build workflow.
Build Artifacts
GitHub Actions builds for:
macOS : Universal binary (x64 + ARM64) .dmg
Windows : .msi installer and .exe portable
Linux : .deb, .AppImage
Create GitHub Release
gh release create v1.2.3 \
--title "Modrinth App v1.2.3" \
--notes "Release notes here" \
apps/app/target/release/bundle/ ** / *
Update Auto-Updater
Tauri’s auto-updater automatically detects new releases from GitHub.
Version Numbering
The app version is set in packages/app-lib/Cargo.toml:
[ package ]
name = "theseus"
version = "1.2.3"
This is automatically updated by the build workflow using the git tag.
Code Signing
macOS :
# Sign with Apple Developer certificate
codesign --sign "Developer ID Application: Modrinth" \
--options runtime \
--entitlements entitlements.plist \
Modrinth.app
# Notarize
xcrun notarytool submit Modrinth.dmg \
--apple-id $APPLE_ID \
--password $APP_PASSWORD \
--team-id $TEAM_ID
Windows :
# Sign with certificate
signtool sign /f certificate.pfx \
/p $PASSWORD \
/tr http://timestamp.digicert.com \
/td SHA256 \
Modrinth-Setup.exe
Auto-Updates
The app checks for updates on launch:
use tauri_plugin_updater :: UpdaterExt ;
fn main () {
tauri :: Builder :: default ()
. plugin ( tauri_plugin_updater :: Builder :: new () . build ())
. setup ( | app | {
let handle = app . handle ();
tauri :: async_runtime :: spawn ( async move {
if let Ok ( Some ( update )) = handle . updater () . check () . await {
// Notify user
// Download and install
update . download_and_install () . await . unwrap ();
}
});
Ok (())
})
. run ( tauri :: generate_context! ())
. expect ( "error while running tauri application" );
}
Documentation Deployment
Documentation (this site) is deployed to Cloudflare Pages:
# Build docs
cd apps/docs
pnpm build
# Deploy
wrangler pages deploy dist --project-name modrinth-docs
Automatically deployed on push to main.
Infrastructure
Services
Modrinth production infrastructure:
Cloudflare Pages : Frontend hosting + CDN
Kubernetes : Backend API orchestration
PostgreSQL : Primary database (managed)
Redis : Cache and sessions (managed)
ClickHouse : Analytics database (managed)
Meilisearch : Search engine (managed)
S3 : Object storage (files, images)
Sentry : Error tracking
Datadog : Monitoring and logging
Monitoring
Health checks :
# API health
curl https://api.modrinth.com/health
# Search health
curl https://api.modrinth.com/_internal/search/health
Metrics (Prometheus):
Request rate
Response time
Error rate
Database query time
Cache hit rate
Logging (Datadog):
Application logs
Access logs
Error logs
Audit logs
Scaling
Horizontal scaling :
# Scale API pods
kubectl scale deployment/labrinth --replicas=5 -n modrinth
# Auto-scaling
kubectl autoscale deployment/labrinth \
--min=3 --max=10 \
--cpu-percent=70 \
-n modrinth
Database scaling :
Read replicas for read-heavy queries
Connection pooling (PgBouncer)
Query optimization
Deployment Checklist
Pre-Deployment
Run Tests
pnpm test
cargo test --workspace
Check Linting
pnpm lint
cargo clippy --workspace --all-targets -- -D warnings
Test Migrations
Test database migrations on staging environment first.
Update Changelog
Document changes in CHANGELOG.md or release notes.
Review Dependencies
Check for security vulnerabilities:
Deployment
Deploy to Staging
Test on staging environment first:
Run Smoke Tests
Verify critical functionality on staging.
Monitor Deployment
Watch logs and metrics for errors:
Check Sentry for new errors
Monitor Datadog dashboards
Watch Kubernetes pod status
Verify Functionality
Test critical user flows:
Login/authentication
Project search
File downloads
API endpoints
Post-Deployment
Announce release (if user-facing changes)
Update documentation if needed
Monitor for issues over next 24 hours
Be ready to rollback if critical issues arise
Rollback Procedures
Frontend Rollback
# Via Cloudflare Pages dashboard
# Or redeploy previous commit
git revert HEAD
git push origin main
Backend Rollback
# Kubernetes rollback
kubectl rollout undo deployment/labrinth -n modrinth
# Or deploy specific version
kubectl set image deployment/labrinth \
labrinth=ghcr.io/modrinth/labrinth:previous-version \
-n modrinth
Database Rollback
If migration needs to be reverted:
# Run down migration (if available)
cargo sqlx migrate revert --database-url $DATABASE_URL
# Or restore from backup
psql $DATABASE_URL < backup.sql
Database rollbacks are risky! Always test migrations thoroughly before deploying.
Security
Secrets Management
GitHub Secrets for CI/CD
Kubernetes Secrets for production
Never commit secrets to git
Rotate secrets regularly
Vulnerability Scanning
# npm audit
pnpm audit
# cargo audit
cargo install cargo-audit
cargo audit
# Container scanning (Docker)
docker scan ghcr.io/modrinth/labrinth:latest
SSL/TLS
Cloudflare provides SSL for frontend
Let’s Encrypt certificates for backend
Enforce HTTPS everywhere
Next Steps
Local Setup Set up local development environment
Testing Run tests before deployment
Backend (Labrinth) Learn about backend architecture
Contributing Contribute to Modrinth