How this site ships to my own server with Coolify
A managed platform would have taken ten minutes. Here is what the Coolify pipeline I built instead does, and where it tripped me up.
- devops
- coolify
- docker
Why my own server
Hosting this site on a managed platform would have taken ten minutes. Running it on my own server turns the site itself into evidence: a portfolio that claims DevOps work should also be willing to show the infrastructure it stands on.
How the pipeline works
The application is built by a multi stage Dockerfile with separate deps, builder and runner stages. The running container drops to a non root user and carries only the standalone output, the static assets and the public folder. Coolify, a self hosted, open source deployment platform, watches the repository through its GitHub App integration: every commit on the main branch triggers a deploy, and every pull request gets its own preview URL. Traefik sits in front and handles TLS termination and routing.
Where it tripped me up
The first snag was the health check. Coolify's built in HTTP health check needs curl or wget inside the container (Coolify docs (opens in a new tab)); the node:24-alpine image I use ships without curl, and its wget is the limited busybox applet. So instead of leaning on an external binary, the check in this repository uses Node's own fetch call and is verified on staging before it is wired to production. The container's own health check came down to this line:
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \
CMD ["node", "-e", "fetch(`http://127.0.0.1:${process.env.PORT || 3000}/api/health`).then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
The second was the environment variable layer. Values prefixed with NEXT_PUBLIC_ are inlined at build time, so they have to arrive as build variables; in this repository the build fails on purpose when the site url is missing, so a wrong address is never silently baked into the bundle. The reverse is worse: marking an API key as a build variable can leak it into image layers and build logs.
What I got out of it
The whole release step collapsed into one command, and every piece of the infrastructure lives in the repository. The price is maintenance: the server, the certificates and the updates are now my job. For a personal site that trade was worth it, because operating it is exactly the thing I wanted to learn. I set up the same pipeline for a client site too; I keep that kind of work on the services page.
Share
The card this page shows when its link is shared.
- Share on X (opens in a new tab)
- Share on LinkedIn (opens in a new tab)
- Share on WhatsApp (opens in a new tab)
- Send by email
Something you need built?
A few lines are enough: the scope, and the date you need it by.