Deployment
Deploying Solana applications to production requires careful consideration of infrastructure, monitoring, and reliability.
Deployment Architecture
Text
┌─────────────────────────────────────────────────────────────────┐
│ Production Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Load Balancer │ │
│ │ (CloudFlare / AWS ALB) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ App 1 │ │ App 2 │ │ App 3 │ │
│ │(Next.js) │ │(Next.js) │ │(Next.js) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ API Services │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │Indexer │ │ API │ │Worker │ │Webhook │ │ │
│ │ │Service │ │Service │ │Service │ │Service │ │ │
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │PostgreSQL│ │ Redis │ │ Solana │ │
│ │ (Primary)│ │ Cluster │ │ RPC │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Docker Configuration
dockerfile
# Dockerfile
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json bun.lockb* ./
RUN corepack enable && corepack prepare --activate
RUN bun install --frozen-lockfile
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN corepack enable && corepack prepare --activate
RUN bun run build
# Production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]
YAML
# docker-compose.yml
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/app
- REDIS_URL=redis://redis:6379
- RPC_URL=${RPC_URL}
depends_on:
- db
- redis
deploy:
replicas: 3
resources:
limits:
cpus: "1"
memory: 1G
api:
build:
context: ./api
dockerfile: Dockerfile
ports:
- "4000:4000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/app
- REDIS_URL=redis://redis:6379
- RPC_URL=${RPC_URL}
depends_on:
- db
- redis
deploy:
replicas: 2
indexer:
build:
context: ./indexer
dockerfile: Dockerfile
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/app
- RPC_URL=${RPC_URL}
- GEYSER_URL=${GEYSER_URL}
depends_on:
- db
deploy:
replicas: 1
worker:
build:
context: ./worker
dockerfile: Dockerfile
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/app
- REDIS_URL=redis://redis:6379
- RPC_URL=${RPC_URL}
depends_on:
- db
- redis
deploy:
replicas: 2
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d app"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
command: redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
redis_data:
Kubernetes Deployment
YAML
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: solana-app
labels:
app: solana-app
spec:
replicas: 3
selector:
matchLabels:
app: solana-app
template:
metadata:
labels:
app: solana-app
spec:
containers:
- name: app
image: your-registry/solana-app:latest
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: redis-url
- name: RPC_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: rpc-url
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: solana-app-service
spec:
selector:
app: solana-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: solana-app-ingress
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- app.example.com
secretName: app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: solana-app-service
port:
number: 80
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: solana-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: solana-app
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
RPC Configuration
TypeScript
import { Connection, clusterApiUrl } from "@solana/web3.js";
interface RPCConfig {
url: string;
weight: number;
rateLimit: number;
maxRetries: number;
}
class RPCPool {
private connections: Array<{
connection: Connection;
config: RPCConfig;
requestCount: number;
lastReset: number;
}>;
constructor(configs: RPCConfig[]) {
this.connections = configs.map((config) => ({
connection: new Connection(config.url, {
commitment: "confirmed",
confirmTransactionInitialTimeout: 60000,
}),
config,
requestCount: 0,
lastReset: Date.now(),
}));
}
// Weighted round-robin selection
getConnection(): Connection {
const now = Date.now();
// Reset counters every minute
for (const conn of this.connections) {
if (now - conn.lastReset > 60000) {
conn.requestCount = 0;
conn.lastReset = now;
}
}
// Find available connection
const available = this.connections
.filter((c) => c.requestCount < c.config.rateLimit)
.sort((a, b) => {
const aScore = a.requestCount / a.config.weight;
const bScore = b.requestCount / b.config.weight;
return aScore - bScore;
});
if (available.length === 0) {
throw new Error("All RPC endpoints rate limited");
}
available[0].requestCount++;
return available[0].connection;
}
async executeWithRetry<T>(
fn: (connection: Connection) => Promise<T>,
maxRetries = 3,
): Promise<T> {
let lastError: Error | undefined;
for (let i = 0; i < maxRetries; i++) {
const connection = this.getConnection();
try {
return await fn(connection);
} catch (error: any) {
lastError = error;
// Rate limit or server error - try different RPC
if (error.message?.includes("429") || error.message?.includes("503")) {
continue;
}
throw error;
}
}
throw lastError || new Error("Max retries exceeded");
}
}
// Configuration
const rpcPool = new RPCPool([
{
url: process.env.PRIMARY_RPC_URL!,
weight: 10,
rateLimit: 100,
maxRetries: 3,
},
{
url: process.env.SECONDARY_RPC_URL!,
weight: 5,
rateLimit: 50,
maxRetries: 3,
},
{
url: clusterApiUrl("mainnet-beta"),
weight: 1,
rateLimit: 10,
maxRetries: 2,
},
]);
export { rpcPool };
Health Checks
TypeScript
import express from "express";
import { Pool } from "pg";
import Redis from "ioredis";
import { Connection } from "@solana/web3.js";
const app = express();
interface HealthStatus {
status: "healthy" | "degraded" | "unhealthy";
timestamp: string;
checks: {
database: { status: string; latency?: number };
redis: { status: string; latency?: number };
rpc: { status: string; latency?: number; slot?: number };
};
}
async function checkDatabase(
pool: Pool,
): Promise<{ status: string; latency: number }> {
const start = Date.now();
try {
await pool.query("SELECT 1");
return { status: "healthy", latency: Date.now() - start };
} catch (error) {
return { status: "unhealthy", latency: Date.now() - start };
}
}
async function checkRedis(
redis: Redis,
): Promise<{ status: string; latency: number }> {
const start = Date.now();
try {
await redis.ping();
return { status: "healthy", latency: Date.now() - start };
} catch (error) {
return { status: "unhealthy", latency: Date.now() - start };
}
}
async function checkRPC(connection: Connection): Promise<{
status: string;
latency: number;
slot?: number;
}> {
const start = Date.now();
try {
const slot = await connection.getSlot();
return { status: "healthy", latency: Date.now() - start, slot };
} catch (error) {
return { status: "unhealthy", latency: Date.now() - start };
}
}
app.get("/api/health", async (req, res) => {
const [database, redis, rpc] = await Promise.all([
checkDatabase(pool),
checkRedis(redisClient),
checkRPC(connection),
]);
const checks = { database, redis, rpc };
const unhealthyCount = Object.values(checks).filter(
(c) => c.status === "unhealthy",
).length;
const status: HealthStatus = {
status:
unhealthyCount === 0
? "healthy"
: unhealthyCount < 2
? "degraded"
: "unhealthy",
timestamp: new Date().toISOString(),
checks,
};
const httpStatus =
status.status === "healthy"
? 200
: status.status === "degraded"
? 200
: 503;
res.status(httpStatus).json(status);
});
// Kubernetes liveness probe
app.get("/api/health/live", (req, res) => {
res.status(200).send("OK");
});
// Kubernetes readiness probe
app.get("/api/health/ready", async (req, res) => {
try {
await pool.query("SELECT 1");
res.status(200).send("OK");
} catch (error) {
res.status(503).send("Not Ready");
}
});
Monitoring Setup
TypeScript
import { Counter, Histogram, Gauge, Registry } from "prom-client";
const register = new Registry();
// Metrics
const httpRequestsTotal = new Counter({
name: "http_requests_total",
help: "Total HTTP requests",
labelNames: ["method", "route", "status"],
registers: [register],
});
const httpRequestDuration = new Histogram({
name: "http_request_duration_seconds",
help: "HTTP request duration",
labelNames: ["method", "route"],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
registers: [register],
});
const rpcRequestsTotal = new Counter({
name: "rpc_requests_total",
help: "Total RPC requests",
labelNames: ["method", "status"],
registers: [register],
});
const rpcRequestDuration = new Histogram({
name: "rpc_request_duration_seconds",
help: "RPC request duration",
labelNames: ["method"],
buckets: [0.1, 0.25, 0.5, 1, 2.5, 5, 10],
registers: [register],
});
const transactionsProcessed = new Counter({
name: "transactions_processed_total",
help: "Total transactions processed",
labelNames: ["status"],
registers: [register],
});
const indexerSlotGauge = new Gauge({
name: "indexer_current_slot",
help: "Current slot being indexed",
registers: [register],
});
const indexerLagGauge = new Gauge({
name: "indexer_slot_lag",
help: "Slots behind current",
registers: [register],
});
// Middleware
function metricsMiddleware(req: any, res: any, next: any) {
const start = Date.now();
res.on("finish", () => {
const duration = (Date.now() - start) / 1000;
const route = req.route?.path || req.path;
httpRequestsTotal.inc({
method: req.method,
route,
status: res.statusCode,
});
httpRequestDuration.observe({ method: req.method, route }, duration);
});
next();
}
// Metrics endpoint
app.get("/metrics", async (req, res) => {
res.set("Content-Type", register.contentType);
res.send(await register.metrics());
});
export {
httpRequestsTotal,
httpRequestDuration,
rpcRequestsTotal,
rpcRequestDuration,
transactionsProcessed,
indexerSlotGauge,
indexerLagGauge,
metricsMiddleware,
};
Environment Configuration
TypeScript
// config/index.ts
import { z } from "zod";
const envSchema = z.object({
// App
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
PORT: z.coerce.number().default(3000),
// Database
DATABASE_URL: z.string().url(),
DATABASE_POOL_MIN: z.coerce.number().default(2),
DATABASE_POOL_MAX: z.coerce.number().default(20),
// Redis
REDIS_URL: z.string().url(),
// Solana
RPC_URL: z.string().url(),
RPC_SECONDARY_URL: z.string().url().optional(),
NETWORK: z
.enum(["mainnet-beta", "devnet", "testnet"])
.default("mainnet-beta"),
// Auth
JWT_SECRET: z.string().min(32),
JWT_EXPIRES_IN: z.string().default("7d"),
// External Services
HELIUS_API_KEY: z.string().optional(),
SENTRY_DSN: z.string().url().optional(),
});
function loadConfig() {
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error("❌ Invalid environment variables:");
console.error(result.error.format());
process.exit(1);
}
return result.data;
}
export const config = loadConfig();
CI/CD Pipeline
YAML
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Run tests
run: bun test
- name: Run linter
run: bun lint
build:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure kubectl
uses: azure/k8s-set-context@v3
with:
kubeconfig: ${{ secrets.KUBE_CONFIG }}
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/solana-app \
app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
kubectl rollout status deployment/solana-app
Deployment Checklist
| Category | Item |
|---|---|
| Security | Environment variables secured in secrets manager |
| Security | HTTPS enabled with valid certificates |
| Security | Rate limiting configured |
| Database | Connection pooling configured |
| Database | Backups automated |
| Monitoring | Health checks implemented |
| Monitoring | Metrics exposed for Prometheus |
| Monitoring | Alerts configured for critical issues |
| RPC | Multiple RPC endpoints configured |
| RPC | Fallback and retry logic implemented |
| Scaling | Horizontal pod autoscaler configured |
| Scaling | Database read replicas for heavy loads |
Next: Governance Overview - Program upgrades and governance patterns.