Happy Hashes: Know What’s Actually Running in Production
“It works on my machine.” “I thought we deployed that fix.” “Which commit is in prod?” “Is staging up to date?”
Version tags like v1.2.3 can point to multiple commits. Tags move. Tags get retagged. Git hashes don’t. Same hash equals identical code, guaranteed. Cryptographic proof.
The solution: Every service exposes a /version endpoint returning its git hash. Instantly verify what’s deployed.
Backend: Capture Hash at Build Time#
Docker images don’t contain .git directories. Capture the hash during build and bake it into the image:
FROM python:3.12-slim
ARG GIT_HASH=unknown
ENV GIT_HASH=$GIT_HASH
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]
Build command:
docker build \
--build-arg GIT_HASH=$(git rev-parse HEAD) \
-t myapp:latest .
Expose via endpoint:
# routes.py
import os
from fastapi import APIRouter
router = APIRouter()
@router.get("/version")
async def get_version():
return {
"git_hash": os.environ.get("GIT_HASH", "unknown"),
}
$ curl https://api.myapp.com/version
{"git_hash":"a1b2c3d4e5f6g7h8i9j0"}
Frontend: The Challenge#
Frontend builds are trickier. Static files served by nginx or CDN don’t have runtime environment variables. Solution: inject at build time into the JavaScript bundle.
Vite configuration:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { execSync } from "child_process";
const gitHash = process.env.GIT_HASH
|| execSync("git rev-parse HEAD").toString().trim();
export default defineConfig({
plugins: [react()],
define: {
__GIT_HASH__: JSON.stringify(gitHash),
},
});
Type declaration:
// src/vite-env.d.ts
/// <reference types="vite/client" />
declare const __GIT_HASH__: string;
Use in code:
// src/version.ts
export function getVersion() {
return {
git_hash: __GIT_HASH__,
};
}
Expose for debugging:
// Expose via window for browser console access
if (typeof window !== "undefined") {
console.log("App Version:", __GIT_HASH__);
(window as any).__VERSION__ = getVersion();
}
Users can type __VERSION__ in the browser console to see what’s running.
Frontend: /version Endpoint Options#
Option 1: React Router
<Route path="/version" element={
<pre>{JSON.stringify(getVersion(), null, 2)}</pre>
} />
Option 2: Static JSON with nginx
Generate at build time:
echo '{"git_hash":"'$GIT_HASH'"}' > dist/version.json
Configure nginx:
location /version {
default_type application/json;
alias /usr/share/nginx/html/version.json;
}
CI/CD Integration#
Jenkins example:
pipeline {
agent any
environment {
GIT_HASH = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
ECR_REPO = '123456789.dkr.ecr.us-east-1.amazonaws.com'
}
stages {
stage('Build Backend') {
steps {
sh """
docker build \
--build-arg GIT_HASH=${GIT_HASH} \
-t ${ECR_REPO}/backend:${GIT_HASH} \
./backend
"""
}
}
stage('Build Frontend') {
steps {
dir('frontend') {
sh "GIT_HASH=${GIT_HASH} npm run build"
}
}
}
stage('Push Images') {
steps {
sh "docker push ${ECR_REPO}/backend:${GIT_HASH}"
}
}
}
}
Terraform Deployment#
Tag Docker images with git hash:
# terraform/variables.tf
variable "git_hash" {
description = "Git commit hash for deployment"
type = string
}
variable "ecr_repo" {
description = "ECR repository URL"
type = string
}
# terraform/ecs.tf
resource "aws_ecs_task_definition" "backend" {
family = "backend"
container_definitions = jsonencode([
{
name = "backend"
image = "${var.ecr_repo}/backend:${var.git_hash}"
environment = [
{
name = "GIT_HASH"
value = var.git_hash
}
]
}
])
}
Deploy:
export GIT_HASH=$(git rev-parse HEAD)
terraform apply \
-var="git_hash=${GIT_HASH}" \
-var="ecr_repo=123456789.dkr.ecr.us-east-1.amazonaws.com"
The image tag, the environment variable, and the version endpoint all match. Always.
Verification#
Quick check:
$ curl https://api.myapp.com/version
{"git_hash":"a1b2c3d4e5f6g7h8i9j0"}
$ curl https://myapp.com/version
{"git_hash":"a1b2c3d4e5f6g7h8i9j0"}
Hashes match? Same commit deployed to both. Hashes differ? Version mismatch.
Debugging Production Issues#
Got a bug report?
# 1. Check deployed version
$ curl https://api.myapp.com/version
{"git_hash":"a1b2c3d4"}
# 2. Find exactly what code is running
$ git show a1b2c3d4
# 3. Check if fix is deployed
$ git branch --contains a1b2c3d4
# 4. See what's different from main
$ git log a1b2c3d4..main --oneline
No more guessing.
Extended Version Information#
Add build time and environment:
@router.get("/version")
async def get_version():
return {
"git_hash": os.environ.get("GIT_HASH", "unknown"),
"build_time": os.environ.get("BUILD_TIME", "unknown"),
"environment": os.environ.get("ENVIRONMENT", "unknown"),
}
ARG GIT_HASH=unknown
ARG BUILD_TIME=unknown
ENV GIT_HASH=$GIT_HASH
ENV BUILD_TIME=$BUILD_TIME
Build with:
docker build \
--build-arg GIT_HASH=$(git rev-parse HEAD) \
--build-arg BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
-t myapp:latest .
Security Note#
Git hashes are not secrets. They’re safe to expose publicly. You can’t reverse a hash to get source code. GitHub shows them on every page.
Don’t expose: full file paths, internal hostnames, or dependency versions if you’re concerned about CVE disclosure.
File Structure#
backend/
├── Dockerfile # ARG/ENV GIT_HASH
└── routes.py # GET /version
frontend/
├── vite.config.ts # define __GIT_HASH__
├── src/
│ ├── vite-env.d.ts # declare __GIT_HASH__
│ └── version.ts # getVersion()
└── nginx.conf # /version route (optional)
Jenkinsfile # Pass hash to both builds
terraform/
└── ecs.tf # Deploy with image tag = hash