Deploying Next.js on a Single VM: Docker, PostgreSQL, nginx and GitHub Actions
Mohammed Moutawakkil
5 min read
Next.jsDockernginxDevOps
You don't need a platform to host a Next.js app. A single Linux VM with Docker, PostgreSQL and nginx is enough, and you stay in control of every part. This is the setup I use for this portfolio, file by file.
Secrets are not baked in..env is listed in .dockerignore. Database passwords and tokens are passed when the container starts, never at build time.
Migrations run on start.prisma migrate deploy applies pending migrations and does nothing when there are none, so it's safe on every restart.
Step 2: Docker Compose on the Host Network
A container normally can't reach localhost on the VM. Since PostgreSQL runs on the VM, I use the host network and bind the app to the loopback address myself:
Migrations create empty tables. To load the initial projects and articles, I run the seed once in a throwaway Node container. The VM needs nothing but Docker:
On the VM, it pulls, rebuilds and then waits for the app to answer. A deploy only counts as successful when the site actually responds:
Shell
set -euo pipefail
cd "$1"
git pull --ff-only origin master
docker compose up -d --build
docker image prune -f
for i in $(seq 1 30); do
if curl -fsS -o /dev/null http://127.0.0.1:3001/; then echo "Site is up"; exit 0; fi
sleep 5
done
echo "Site did not come up within 150s" >&2
docker compose logs --tail 50 portfolio >&2
exit 1
A few choices here are deliberate:
--ff-only makes the deploy fail loudly if someone edited files directly on the VM, instead of creating a merge there.
cancel-in-progress: false lets a running deploy finish. A newer push waits its turn rather than stopping a rebuild halfway.
A dedicated SSH key is used only for deploys, and the VM's host key is stored as a secret (ssh-keyscan), so Actions verifies it is talking to the right machine.
The last 50 log lines are printed on failure, so the reason is visible in the Actions log without logging in to the VM.
Why Not a Platform?
Platforms are great, and for many projects they are the right choice. A VM gave me a few things I wanted for this site:
One fixed monthly cost, whatever the traffic.
The database next to the app, with no network hop or extra service to pay for.
Full control: logs, nginx rules and upgrades are mine to manage.
The trade-off is that I also own the maintenance: security updates, backups and certificates. For a portfolio and side projects, that has been a good deal.
Conclusion
Five files cover the whole deployment: a Dockerfile, a Compose file, an nginx site, a seed command and a workflow. Each one does one job, and each one can be read in a minute.
If you are setting up something similar and get stuck, feel free to contact me.