Visual regression testing as a service
6.9K
Visual regression testing as a service. CI pipelines POST screenshots to Inspectre; Inspectre diffs them against approved baselines and reports pass/fail. A web UI lets your team review failures, inspect diff images, and promote new baselines with one click.
Built as a modern Python/Angular rewrite of the original Spectre Rails app.
CI job
└─ POST /runs → creates a run
└─ POST /tests → uploads screenshot, returns immediately (~50 ms)
└─ GET /tests/:id/status → poll until status == "done"
Celery worker (async)
└─ ImageMagick compare vs stored baseline
└─ Stores diff image + thumbnails to S3
└─ Marks test passed / failed
Human
└─ Opens http://localhost:4200
└─ Reviews diff images, clicks "Set as baseline" to accept regressions
The first submission for any (project, suite, name, browser, size) combination has nothing to compare against, so it's stored with no comparison images, marked is_new_baseline: true, and marked not passed. A human must review it and click "Set as baseline" before it becomes the reference image subsequent submissions compare against. Later submissions that match an established baseline pass automatically, with no human involved.
| Layer | Technology |
|---|---|
| API server | Python, Django, Django REST Framework |
| Task queue | Celery + Valkey (Redis-protocol broker) |
| Web UI | Angular, standalone/zoneless components, Angular Material |
| Database | PostgreSQL |
| Object storage | S3-compatible — MinIO in dev, AWS S3 in prod |
| Serving | gunicorn (API) + nginx (SPA + reverse proxy) |
| Orchestration | Docker Compose |
Project ──▶ Suite ──▶ Run ──▶ Test
└─▶ Baseline (one per test "key", lives on the Suite)
POST /tests returns in ~50 ms. The heavy work runs in a Celery worker:
imagemagick compare -metric AE → diff pixel count → diff percentagestatus = "done"Two URL namespaces:
/api/* — SPA endpoints; can evolve freelyPOST /runs, POST /tests, GET /tests/:id/status, PATCH /tests/:id, GET /baselines/:key.png, GET /baselines/:key.json| Image | Tags |
|---|---|
wvankuipers/inspectre | api-latest, api-<version> |
wvankuipers/inspectre | spa-latest, spa-<version> |
makecp deploy/.env.example .env
# Edit .env — at minimum set ADMIN_PASSWORD
make build
make up
make migrate
Services:
| Service | URL |
|---|---|
| SPA | http://localhost:4200 |
| API | http://localhost:8000 |
| Django admin | http://localhost:8000/admin |
| MinIO console | http://localhost:9001 (login: minio / miniominio) |
The SPA's nginx reverse-proxies /api, /runs, /tests, /baselines, and /admin to the API container — everything is same-origin from the browser.
make seed
Populates three projects with runs, pass/fail tests, and baselines so you can explore the UI without a real CI pipeline.
| Command | What it does |
|---|---|
make up | Start the stack |
make down | Stop the stack (volumes preserved) |
make test | All backend + frontend tests |
make test-fast | Backend only, no ImageMagick (~1 s) |
make test-slow | Backend including image diff tests |
make lint / make lint-fix | Ruff + Angular ESLint |
make migrate | Run Django migrations inside api container |
make shell-api | Django shell inside api container |
make reset | Nuke and rebuild everything from scratch |
make perf | Performance stress test (needs make up) |
Backend / worker:
docker compose build api worker && docker compose up -d api worker
Frontend:
docker compose build spa && docker compose up -d spa
| Variable | Required | Notes |
|---|---|---|
DJANGO_SECRET_KEY | yes | Generate with python -c 'import secrets; print(secrets.token_urlsafe(50))' |
DEBUG | no | 0 in prod |
ALLOWED_HOSTS | yes | Comma-separated hostnames |
CORS_ALLOWED_ORIGINS | no | Comma-separated origins; empty means same-origin only |
DATABASE_URL | yes | Full postgres DSN |
S3_BUCKET_NAME | yes | Must exist before first run |
S3_REGION | yes | AWS region or any value for MinIO |
S3_ENDPOINT_URL | no | Set for MinIO; omit for AWS S3 |
S3_ACCESS_KEY_ID | yes | AWS key or MinIO root user |
S3_SECRET_ACCESS_KEY | yes | AWS secret or MinIO root password |
S3_PUBLIC_BASE_URL | no | CDN prefix for browser-reachable image URLs |
ADMIN_USERNAME | no | Default: admin |
ADMIN_PASSWORD | no | If unset, ensure_admin_user skips bootstrap (no admin account created) |
REDIS_URL | no | Valkey/Redis broker; default redis://localhost:6379/0 |
CELERY_WORKER_CONCURRENCY | no | Parallel image pipelines per worker; default 2 |
GUNICORN_WORKERS | no | API server processes; default 3 |
IMAGE_DIFF_THRESHOLD | no | Diff % to count as failure; default 0.1 |
RUN_RETENTION_PER_SUITE | no | Max runs kept per suite; default 5 |
# First deploy
cp deploy/.env.example .env # fill in production values
docker compose -f deploy/docker-compose.yml up -d
docker compose -f deploy/docker-compose.yml exec api python manage.py migrate
# Subsequent deploys
docker compose -f deploy/docker-compose.yml build api spa worker
docker compose -f deploy/docker-compose.yml up -d api spa worker
docker compose -f deploy/docker-compose.yml exec api python manage.py migrate
DEBUG=0DJANGO_SECRET_KEY is unique and randomALLOWED_HOSTS includes the production hostnames3:PutObject, s3:GetObject, s3:DeleteObjectS3_PUBLIC_BASE_URL set if serving images through a CDN# 1. Create a run
RUN=$(curl -s -X POST http://inspectre.example.com/runs \
-d "project=My App&suite=Desktop" | jq -r .id)
# 2. Submit a screenshot (returns immediately with status=pending)
TEST_ID=$(curl -s -X POST http://inspectre.example.com/tests \
-F "test[run_id]=$RUN" \
-F "test[name]=Homepage" \
-F "test[browser]=Chrome" \
-F "test[size]=1440" \
-F "test[screenshot][email protected]" | jq -r .id)
# 3. Poll for result
while true; do
STATUS=$(curl -s http://inspectre.example.com/tests/$TEST_ID/status | jq -r .status)
[ "$STATUS" = "done" ] || [ "$STATUS" = "failed" ] && break
sleep 1
done
The pass field in the final response is true if the diff percentage is below the threshold.
The GitHub Actions workflow (.github/workflows/ci.yml) runs lint and tests on every push and pull request to main, then builds and pushes multi-arch Docker images to Docker Hub on successful merges to main.
| Secret | How to obtain |
|---|---|
DOCKERHUB_USERNAME | Your Docker Hub username |
DOCKERHUB_TOKEN | Docker Hub → Account Settings → Security → New Access Token |
Set these under Settings → Secrets and variables → Actions in the GitHub repository.
| Image | Tag |
|---|---|
<DOCKERHUB_USERNAME>/inspectre | api-latest |
<DOCKERHUB_USERNAME>/inspectre | api-$VERSION |
<DOCKERHUB_USERNAME>/inspectre | spa-latest |
<DOCKERHUB_USERNAME>/inspectre | spa-$VERSION |
See LICENSE.
Content type
Image
Digest
sha256:1e8c42a9a…
Size
145.9 MB
Last updated
11 days ago
docker pull wvankuipers/inspectre:api-1.6.1