Skip to content
AI Full-Stack Wiki

DevOps Basics for AI Apps: Git, Docker, CI/CD, and Deployment

Ship an AI web app safely with Git workflows, a multi-stage Docker image, a CI pipeline that runs tests and evals, environment validation, health checks, and rollbacks.

Difficulty: Beginner15 min readUpdated
On this page· The pipeline at a glance

Why this matters

AI features change behavior in ways unit tests do not catch: a one-word prompt edit can make answers worse while every test stays green. A good delivery pipeline makes changes small, reviewed, reproducible, and reversible, and it adds an AI-specific gate: evals that measure answer quality before a change reaches users. This is the boring infrastructure that lets you move fast without breaking production.

The pipeline at a glance#

From Git push to productionA push triggers CI: lint, typecheck, and tests, then evals for AI behavior; passing changes deploy to a preview environment and, after review and merge, to production. Failures go back to the developer.passpassmergegit pushCI checkslint, types, testsEvalsAI regression suitePreview deployper pull requestProductionafter merge

Hover or tap a component to see what it does.

Deployment pipeline for an AI app: git push, CI tests, evals, preview deploy, and production, with failing checks sending the change back to the developer.
Show text description

The pipeline runs left to right. The extra step compared with a classic web app is the eval suite: prompt and model changes can break behavior without breaking any unit test.

  • git push: Every change starts as a commit on a branch, with a pull request for review.
  • CI checks: Fast, deterministic checks on every push. A red build blocks the merge.
  • Evals: Runs the eval dataset against the changed prompts or models and fails if quality drops below the baseline.
  • Preview deploy: A full copy of the app at its own URL, so reviewers can try the change.
  • Production: Merging to main deploys. Keep the previous version ready for a one-click rollback.

Each stage answers one question. CI: does the code still work? Evals: does the AI still behave well? Preview: does it look and feel right to a reviewer? Production: can we ship it, and undo it in one click if not?

Git: small, reviewed, reversible#

  • Short-lived branches and pull requests. One change per pull request, merged within a day or two. Small diffs get real reviews.
  • Prompts are code. Keep system prompts, tool descriptions, and eval datasets in the repository, not in a dashboard, so every change is diffable, reviewed, and revertible.
  • Never commit secrets. Add .env* to .gitignore before the first commit, and enable your Git host's secret scanning. If a key is ever pushed, revoke it immediately; deleting the commit is not enough.
  • Write commit messages for your future self: what changed and why, especially for prompt changes ("Ask for citations in the system prompt; eval faithfulness 0.81 → 0.90").

Validate configuration at startup#

A missing environment variable should fail the deploy, not the first user request at 3 a.m. Parse process.env with a schema in one module and import env everywhere else.

lib/env.ts
import { z } from "zod";
 
const Env = z.object({
  NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
  DATABASE_URL: z.url(),
  REDIS_URL: z.url().default("redis://localhost:6379"),
  ANTHROPIC_API_KEY: z.string().min(1, "Set ANTHROPIC_API_KEY on the server"),
  SESSION_SECRET: z.string().min(32),
});
 
const parsed = Env.safeParse(process.env);
if (!parsed.success) {
  // Print every problem at once, then stop: a half-configured server is worse than none.
  console.error("Invalid environment:", parsed.error.issues.map((i) => `${i.path.map(String).join(".")}: ${i.message}`));
  throw new Error("Invalid environment variables");
}
 
export const env = parsed.data;

A production Docker image#

A multi-stage Dockerfile keeps the final image small: dependencies and the build happen in throwaway stages, and only the standalone server output is copied into the image that runs. Enable it in Next.js with output: "standalone" in next.config.ts.

Dockerfile
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
 
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
 
FROM node:22-alpine AS run
WORKDIR /app
ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0
RUN addgroup -S app && adduser -S app -G app
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
USER app
EXPOSE 3000
CMD ["node", "server.js"]

The same image can run more than one process. A background worker that indexes documents uses the image with a different command, next to Postgres and Redis:

From Dockerfile to running servicesA Dockerfile builds an image; Docker Compose runs that image as the web app and as a worker, next to Postgres and Redis containers on one network.docker buildDOCKER COMPOSEDockerfileImageimmutable layersweb containernext startworker containernode worker.jspostgresredis

Hover or tap a component to see what it does.

Docker for a Node.js AI app: one image built from a Dockerfile runs as both the web server and the background worker, alongside Postgres and Redis in Docker Compose.
Show text description

Containers make the environment part of the code: the versions of Node.js, Postgres, and Redis are pinned in files you commit.

  • Dockerfile: Recipe: base image, install dependencies, copy code, build, set the start command.
  • Image: The built artifact. The same image runs on your laptop, in CI, and in production.
  • web container: Serves the app. Scale horizontally by running more copies.
  • worker container: Same image, different command: processes background jobs.
  • postgres: Official image with the pgvector extension, data on a named volume.
  • redis: Queue and cache.
compose.yaml
services:
  web:
    build: .
    env_file: .env.production
    ports: ["3000:3000"]
    depends_on: [postgres, redis]
  worker:
    build: .
    command: ["node", "worker.js"]
    env_file: .env.production
    depends_on: [postgres, redis]
  postgres:
    image: pgvector/pgvector:pg17
    environment: { POSTGRES_USER: app, POSTGRES_PASSWORD: app, POSTGRES_DB: app }
    volumes: [pgdata:/var/lib/postgresql/data]
  redis:
    image: redis:7-alpine
volumes:
  pgdata:

Continuous integration with an eval gate#

This GitHub Actions workflow runs fast checks on every push and the eval suite on pull requests. Evals call a real model, so they need an API key from your repository secrets and they cost money: keep the dataset focused and run it where it matters.

.github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:
 
jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v5
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test
      - run: npm run build
 
  evals:
    needs: checks
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v5
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run evals
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

Repository secrets are not passed to workflows triggered by pull requests from forks, so external contributions will skip or fail the eval job until a maintainer runs it. The evals article shows what npm run evals contains.

Deploying#

Pick a managed platform until you have a reason not to: most of them build from Git, give every pull request a preview URL, manage TLS, and support instant rollbacks. Whatever you choose, check these boxes:

  • Secrets in the platform's encrypted settings, separate per environment (preview environments should use a separate, budget-capped API key).
  • Streaming support. Confirm the platform does not buffer responses and allows long enough request durations for model calls.
  • A health check the platform polls to decide whether a new version is ready to receive traffic.
  • One-click rollback to the previous build, and the habit of using it before debugging in production.
  • Database migrations as a separate, explicit step, written to be backward-compatible so the old version keeps working during rollout and after a rollback.
app/api/health/route.ts
// Liveness check for the platform's health probe. Keep it cheap: no model calls here.
export function GET() {
  return Response.json({ status: "ok", version: process.env.GIT_SHA ?? "dev", uptime: Math.round(process.uptime()) });
}

Common mistakes#

  • Prompts edited in production dashboards. Unreviewed, untested, and impossible to roll back. Prompts belong in Git.
  • Sharing one API key across local, preview, and production. A bug in a preview branch can drain the production budget, and you cannot tell traffic apart. Use separate keys with separate limits.
  • Building on the server at deploy time. Build once in CI, then deploy the same artifact everywhere, so what you tested is what runs.
  • Migrations that break the running version. Renaming a column the old code still reads takes the site down during rollout. Add the new column, deploy, backfill, then remove the old one later.
  • Health checks that call the model. A slow provider then marks every instance unhealthy at once. Check your own process, and monitor upstreams separately.

Try it

Wire up the pipeline

  1. Add typecheck (tsc --noEmit) and test scripts to a project's package.json and push the CI workflow.
  2. Open a pull request that breaks a type on purpose and confirm the check fails and blocks the merge.
  3. Build the Docker image locally and run it with docker run -p 3000:3000 --env-file .env.local <image>. Call /api/health.
  4. Remove one required variable from the env file and confirm the container exits with a clear message.

Check your understanding

Why add an eval step to CI for an AI app?
What is the main benefit of a multi-stage Dockerfile?
A preview deployment should use which API key?

Key takeaways

  • Keep prompts, tool definitions, and eval datasets in Git; review them like code.
  • Validate environment variables at startup and fail fast.
  • Build once into a small, non-root Docker image and deploy that artifact everywhere.
  • CI runs lint, types, tests, and build on every push, plus evals on pull requests.
  • Deploy with previews, health checks, separate keys per environment, backward-compatible migrations, and one-click rollbacks.

Next steps#

Finished this article? Track it to see your progress on the roadmap.