Deployment is the part of the job most tutorials skip, and it's the part that actually determines whether a 3 a.m. bug is a five-minute fix or a two-hour fire drill. I've deployed on managed platforms, on raw DigitalOcean droplets with hand-rolled Nginx configs, and now on Docker containers orchestrated through Coolify— a self-hosted, open-source alternative to Heroku/Vercel that runs on infrastructure I own. This is the setup I've settled on for the AI product I currently work on, and it's the one I recommend to anyone who wants Vercel-like convenience without the platform lock-in.
Why containerize in the first place
Before Docker, "deploy" meant SSH-ing into a box, pulling the latest code, hoping the Node or PHP version on the server still matched what the app expected, and restarting a process manager. It worked, until it didn't — a system package update on the server would occasionally take an app down with it, for reasons that had nothing to do with the actual code change being deployed.
A container packages the app and its runtime together. If it runs in the image, it runs the same way on my machine, in CI, and in production. The environment stops being a variable.
# Dockerfile — Next.js frontend, multi-stage build
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]The multi-stage build matters: the final image only contains the compiled output and production dependencies, not the full node_modulesdev toolchain used to build it. That's the difference between a 200MB image and a 1.2GB one — and image size directly affects how fast a rollback or a new deploy actually completes.
Where Coolify fits in
Coolify sits on top of Docker and gives me the parts I don't want to hand-roll: it watches a Git repo, builds the image on push, handles zero-downtime rolling deploys, provisions Let's Encrypt TLS certificates automatically, and gives me one-click rollback to any previous build. It's the Heroku/Vercel developer experience, self-hosted on a DigitalOcean droplet I control, for a fraction of the monthly cost of an equivalent managed platform at any real scale.
The setup I run for most projects:
- One Coolify instance on a small DigitalOcean droplet, managing every project's deployments.
- Each app gets its own Docker container, defined by its own Dockerfile in the repo.
- A push to
maintriggers a build; Coolify builds the new image, health-checks it, and only swaps traffic over once it's confirmed healthy. - Environment variables and secrets live in Coolify's dashboard, never committed to the repo.
The health check is the part that actually saves you
Zero-downtime deploys only work if the platform can tell a new container is actually ready before it cuts traffic over. Every service I deploy exposes a lightweight health endpoint:
# FastAPI example
@app.get("/health")
def health():
return {"status": "ok"}Coolify pings that endpoint after starting the new container. If it doesn't come back healthy within the configured timeout, the deploy is aborted and traffic stays on the previous version — automatically, with no manual intervention. That single check has saved me from shipping a broken build to production more than once.
Rollbacks in thirty seconds, not thirty minutes
Because every deploy is a tagged image, rolling back is just re-deploying the previous tag — not a git revert, not a re-run of a build pipeline, not a support ticket to a platform vendor. Coolify keeps a history of builds and lets me redeploy any of them with one click. The first time I actually needed this — a bad migration paired with a frontend release — the rollback took less time than it took me to finish reading the error in the logs.
What I'd tell someone setting this up for the first time
- Start with the health check endpoint before anything else — it's what makes every other safety net work.
- Keep secrets out of the Dockerfile and repo entirely; inject them as environment variables at deploy time.
- Use multi-stage builds religiously. A bloated image is slow to build, slow to deploy, and slow to roll back.
- Test the rollback before you need it. The first time you click it should not be during an incident.
None of this is about chasing the newest tool. It's about removing as many manual, error-prone steps from deployment as possible, so that shipping a fix at 3 a.m. is a calm, boring, five-minute task — which is exactly how deployment should feel.