DevOps writeups assume a team. Most of mine has been solo work — engines, hostel sites, real-estate listings. Here is the minimum DevOps stack that earns its weight when you are the entire team.
Most DevOps writing is for teams of twenty. The advice does not transfer cleanly to a solo developer who is also the designer, on-call engineer, and product manager. You do not need a service mesh. You do not need GitOps with ArgoCD. You need a small set of habits that prevent the project from slowly becoming hostile to its own author.
This is the stack I have settled on across game engines, web work, and side experiments. It is opinionated. It is also short.
Strip away the jargon and DevOps is solving four practical problems:
Every tool below is justified by which of those four it solves. If a tool does not map cleanly to one of them, it is probably premature.
For a solo project, one container is usually enough. A web app, an engine build, a Discord bot — each gets a single Dockerfile that compiles the world from a clean base image. The discipline is not micro-services. The discipline is: the image you push is the image you run.
A reasonable shape for a Next.js app:
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS run
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/.next ./.next
COPY --from=build /app/public ./public
COPY --from=build /app/node_modules ./node_modules
COPY package.json ./
EXPOSE 3000
CMD ["npm", "start"]
Three stages, no surprises, layer cache is friendly to incremental dev. If your project is C++ or Rust, the same shape works — builder stage compiles, runtime stage gets only the binary and the dynamic libs it needs.
For solo work, your CI file should fit on one screen. Push to main runs:
name: ci
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name == 'push' }}
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
That is the whole file. Tests run as a build stage if they are fast, or as a separate job if they are not. Resist matrix builds across five OSes until you have a reason.
The traditional pattern is the CI job SSH-ing into the server and running docker compose up. That works. It also means CI has production credentials, and any leak is bad.
A safer pattern for one developer:
docker compose pull && docker compose up -d).Now CI cannot accidentally take production down by being clever, and rolling back is docker compose up -d --force-recreate after pinning to the previous SHA tag. Rollback in 30 seconds, no kube-anything required.
.env files in production are fine for one box. The rule is:
.env, owned by root, mode 600, never committed.compose.override.yml injects them.For a Rust binary or a Python service, the same shape applies. Do not invent a config-loading framework.
When you outgrow this — usually around the time you have more than one server or a real customer — move to SOPS + age for encrypted secrets in git, or a real secret manager. Not before.
The single biggest leverage for a solo dev is structured logs you can grep. Not Prometheus, not OpenTelemetry. Logs.
Add metrics when you have an actual question that logs cannot answer ("what is my P99 latency this week?"). Until then, metrics are infrastructure you will eventually have to migrate.
For uptime monitoring, UptimeRobot or Healthchecks.io is a 2-minute setup that catches the embarrassing failure modes. A heartbeat from your cron jobs to Healthchecks is the single most underrated DevOps habit.
The DevOps practice that has saved me the most time, by a wide margin:
# /etc/cron.daily/backup
#!/bin/bash
set -euo pipefail
restic backup /var/lib/myapp /etc/myapp
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
curl -fsS https://hc-ping.com/uuid > /dev/null
Restic to Backblaze B2 is cents per month. The Healthchecks ping at the end means you find out within an hour if the backup stops running. Once a quarter, restore one file from a backup and confirm it opens. Do not skip this step. A backup you have never restored is a hope.
.staging in its hostname. Two environments, not five.If a new project of mine has all of the following, I consider its DevOps story done:
ci.yml that builds and pushes images on every push to main.That is roughly 200 lines of config across the whole stack. It scales to "one developer, real users" and stays out of your way. Beyond that, you are paying for a team's worth of complexity, so you should have a team's worth of users first.