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

CategoryItem
SecurityEnvironment variables secured in secrets manager
SecurityHTTPS enabled with valid certificates
SecurityRate limiting configured
DatabaseConnection pooling configured
DatabaseBackups automated
MonitoringHealth checks implemented
MonitoringMetrics exposed for Prometheus
MonitoringAlerts configured for critical issues
RPCMultiple RPC endpoints configured
RPCFallback and retry logic implemented
ScalingHorizontal pod autoscaler configured
ScalingDatabase read replicas for heavy loads

Next: Governance Overview - Program upgrades and governance patterns.