CONCEPT · 75 minutes
Learn safe, practical deployment patterns for solo AI engineers — from build pipelines to rollback strategies, grounded in real Mac Mini + Cloudflare Tunnel self-hosting.
When I deployed the clinical-AI platform, I had one constraint that shaped every decision: I was the only engineer, and I was also the only person on call.
No ops team. No SRE standing by. No runbook with twenty people to page.
That constraint turned out to be clarifying. It forced me to ask: what's the simplest deployment story that still lets me roll back in under a minute if something breaks?
The answer wasn't Kubernetes. It wasn't Helm charts or GPU node pools. It was three things:
npm run build:production — a deterministic build stepsystemd service restart on the Mac MiniThat's the entire ops playbook. And it runs reliably, day after day, without an ops team.
Real Constraint Drives Good Design: The clinical-AI deployment taught me that deployment simplicity isn't a compromise — it's an engineering decision. Every layer of infrastructure you add is a layer you have to debug at 2am when something breaks.
Think of deploying like shipping cargo on a ship. You don't swap the engine mid-ocean — you prepare everything at the dock, test it, then sail. The key is: when something goes wrong at sea, you need a lifeboat ready. That lifeboat is your rollback story, and you build it before you need it.
By the end of this module, you will:
build:production → restart → smoke-checkThe classic zero-downtime patterns (blue-green, canary) were designed for environments with multiple servers. When you're running a single Mac Mini with a Cloudflare Tunnel, the same concepts still apply — but the implementation is simpler.
Next.js npm run start serves from .next/ — the compiled output. When you run build:production, it writes a new .next/ directory. The running server keeps serving the old .next/ until you restart it.
This gives you a natural "prepare then switch" pattern:
The window where users see downtime is only the restart, which is a few seconds for a Next.js app. Build time, by contrast, takes minutes — and during that entire time the old version is serving traffic normally.
Don't Skip the Build Verification Step: The most common deploy mistake is restarting the service before confirming the build succeeded. If build:production exits with an error, stop — don't restart. The old version is still running correctly.
Even without multiple servers, you can apply canary thinking:
This is lower-fidelity than a true canary, but it catches most classes of production errors before users see them.
Here's the deploy workflow I use for this site. It runs via a GitHub Actions job that SSHes into the Mac Mini and executes a deploy script.
The set -e at the top is load-bearing: if build:production fails, the script exits immediately. The service is never restarted with a broken build.
Why SSH Over a Deploy Service: For a single self-hosted machine, SSH is the simplest reliable path. No webhook server to maintain, no additional service to secure — just a deploy key with restricted permissions on the server.
The first time I deployed the clinical-AI platform to the Mac Mini, the health check passed — but a specific RAG query path was returning empty results. The error was in a code path the health check didn't exercise.
I needed to roll back quickly, while the system was actively in use.
The pattern I use: tag every production deploy, and keep the previous tag as the rollback target.
This gives you a one-command rollback to any previous production state. No guessing which commit was running in production.
A rollback is only useful if you can execute it under stress — at 11pm, when something is broken, with users waiting.
That means:
The Rollback Drill: Once a quarter, pick a random recent deployment and practice rolling back to the one before it. You'll find friction (missing tags, wrong permissions) in a safe context rather than during an incident.
Secrets management is where solo engineers make the most dangerous shortcuts. The clinical-AI platform handled sensitive data, so I had to get this right from day one.
For the systemd service, use EnvironmentFile to inject secrets without putting them in the service unit (which might be world-readable):
The Most Common Secrets Mistake: Using a single .env file that "lives on the server." If the server is compromised or the file is accidentally committed, all your API keys are exposed at once. Use per-service environment files with strict permissions (600, root-owned) to limit the blast radius.
Keep your staging and production configurations structurally identical — same environment variable names, same service file layout. Only the values differ (staging API keys, staging domain, etc.).
This means you can test your deploy process on staging with confidence that it will behave the same on production.
A health check answers one question: "Is the service alive and capable of handling requests?"
This endpoint is called by the deploy script after restart. If it returns anything other than 200, the deploy is considered failed and the rollback runs.
Different checks answer different questions:
| Check | What It Tests | When to Run |
|---|---|---|
| Health check | Service is alive and configured | After every restart |
| Smoke test | Critical user paths work | After every deploy |
| Full test suite | All features work | In CI, before deploy |
A smoke test exercises the most critical user-facing paths. For this site, that means the home page loads, the learning path index returns modules, and the health endpoint returns 200.
Automate the Smoke Test: Add the smoke test to your deploy script. If any check fails, trigger the rollback automatically. The 30 seconds you spend running checks is worth far less than the incident you prevent.
1. You run 'npm run build:production' and it exits with a non-zero status code. What should you do next?
Correct Answer: B
With 'set -e' in your deploy script, the service is never restarted when a build fails. The old version continues serving traffic normally. Investigate the build error before proceeding — there's no urgency because nothing is broken yet.
2. What is the main advantage of tagging each production deploy with a git tag?
Correct Answer: B
Git tags give you a named, persistent reference to the exact code that was running in production at any point. Rollback becomes a 'checkout tag, build, restart' operation rather than a memory test about which commit was deployed.
3. Why does the deploy script use 'set -e' at the top?
Correct Answer: B
'set -e' is a critical safety net. Without it, a failed build step is silently ignored and the next command — restarting the service — still runs, which would replace a working service with a broken one. With 'set -e', the script stops at the first failure.
4. The clinical-AI platform's health check passed but a specific query path was broken. What does this reveal?
Correct Answer: B
A health check answers 'is the service running?' A smoke test answers 'are the critical paths working?' Both are needed. The health check passing correctly indicated the service started — a smoke test exercising the RAG query path would have caught the broken behavior before users saw it.
5. What is the risk of storing all production secrets in a single shared .env file on the server?
Correct Answer: B
A single secrets file is a single point of compromise. If an attacker gains read access to that file, all your API keys, database credentials, and other secrets are exposed simultaneously. Per-service environment files with strict permissions (600, root-owned) limit the blast radius of any single compromise.
6. You need to test a new API integration in staging before deploying to production. Which approach best fits the 'environment parity' principle?
Correct Answer: A
Environment parity means staging mirrors production structurally. Same variable names, same service file structure, same deploy script. Only the values differ. If staging uses 'STAGING_ANTHROPIC_KEY' but production uses 'ANTHROPIC_API_KEY', your staging tests don't validate what production will actually do.
7. Thinking about your own projects: if your deployed AI service suddenly stopped responding to users, what would be your first diagnostic step?
Correct Answer: A
The first step is to narrow down where the failure is: Is the service process running? Is the Cloudflare Tunnel active? Is the health endpoint reachable locally? Work from the outside in — network → service process → application logs. Rolling back before diagnosing the cause might not fix the problem and obscures what went wrong.
Think about this: If your deploy script ran tonight and the smoke test caught a regression, what would your rollback sequence look like? Walk through each step — from receiving the alert to confirming the old version is serving traffic again. Write down the sequence before you need it. The best rollback plan is the one that exists before the incident.