// DevOps
Modern Next.js deployment: GitHub Actions, Docker, and Zero-Downtime
Published on 2026-09-22
If you’re still running next build directly on the production server — your server is really suffering. CPU pegged, OOM-kill, 502 errors and long downtime — that’s a classic that needs to end.
In 2026 the industry standard is separate build:
- Build a minimal standalone image in the cloud with GitHub Actions.
- Push it to GHCR (GitHub Container Registry).
- On the server do only pull + atomic restart.
Chapter 1. Dockerfile: multi-stage and standalone
You get a small and fast image in standalone mode. Next.js itself figures out which files and parts of node_modules are actually needed for the server to run, and copies only them.
# syntax=docker/dockerfile:1
# STAGE 1 — Dependencies
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json yarn.lock* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else npm ci; \
fi
# STAGE 2 — Build
FROM node:24-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# STAGE 3 — Production image (Runner)
FROM node:24-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Security first: run as non-root
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
# Copy only standalone build artifacts
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Container health check
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]The
node:24-alpineimage — on the current Node.js LTS branch. Check support for the chosen version at endoflife.date/nodejs: as of September 2026 branches 24, 22 and 20 are supported.
What this provides
- Size: The image is ~200 MB versus ~1.5 GB for a typical one.
- Security: Using a
non-rootuser (nextjs) protects the host system if the container is compromised. - Healthcheck: Docker will detect if the app hung on startup and won’t send traffic to it.
Chapter 2. Breakdown of the GitHub Actions Workflow
Your pipeline is split into two stages (jobs): build in the cloud and deploy to your own hardware.
1. Preparation and build
- name: Docker meta & tags
id: prepare
run: |
IMAGE_REPO="ghcr.io/${GITHUB_REPOSITORY,,}"
SHORT_SHA="${GITHUB_SHA::8}"
echo "image_repo=$IMAGE_REPO" >> $GITHUB_OUTPUT
echo "image_tag=sha-$SHORT_SHA" >> $GITHUB_OUTPUTImportant note: The ${GITHUB_REPOSITORY,,} syntax lowercases the repository name. Docker registries don’t like uppercase letters, and they are common on GitHub.
2. Build caching
cache-from: type=gha
cache-to: type=gha,mode=maxWe use native GitHub Actions caching. If you didn’t change package.json, the dependency installation stage will be skipped, and the build will take 1–2 minutes instead of 10.
3. Deploy with healthcheck verification (healthcheck loop)
The most important part — we don’t just tell the server “update”, we check whether the application survived.
for i in {1..60}; do
STATUS=$(docker inspect --format='{{json .State.Health.Status}}' nextjs 2>/dev/null || echo '"not-found"')
if [[ $STATUS == '"healthy"' || $STATUS == '"no-healthcheck"' ]]; then
HEALTHY=1
break
fi
sleep 2
doneIf Next.js crashes due to an error in environment variables, the script will detect it, not update the proxy server (Caddy/Nginx), and end the action with an error. Your old site will keep running, and you’ll receive a notification about the problem.
Chapter 3. Runtime vs Build-time variables
These are the pitfalls that almost everyone hits.
NEXT_PUBLIC_(Build-time): These variables are baked into the JS bundle duringnext build. If you change them on the server in.env, nothing will change. They need to be passed to GitHub Actions asbuild-args.Secrets (Runtime):
DATABASE_URL,JWT_SECRET. They must not be placed into the Docker image. They are pulled in at container start viadocker-compose.
Tip: If possible make the API URL a runtime variable as well via proxying or special config scripts, so the same image can be rolled out to both staging and production without rebuilding.
Related topics are covered on the blog: proper Dockerfile, CI/CD from manual deployment to automated and deploying containers via Kamal.
// Reviews
Related reviews
I came with an expensive request to configure a VPS server, but during the consultation Mikhail suggested a much simpler, more affordable solution. In the end I saved time and money. Mikhail — a true expert who works for the client's result, not for the fee. I recommend him!
I came with an expensive request to configure a VPS server, but during the consultation Mikhail suggested a much simpler and more cost-effective solution. In the end I saved budget and time. Mikhail — a true expert who …
VPS setup, server setup
2026-05-12 · ★ 5/5
Excellent work! Set up the server very quickly, installed the control panel, and configured the IP. Definitely recommend!
Excellent work! Very quickly set up the server, installed the panel, configured the IP I can definitely recommend it!
Everything was excellent; helped promptly and professionally. Thank you — I recommend them to the community.
Everything's great, helped promptly and professionally, thank you, I recommend it to the community
VPS setup, server setup
2026-04-16 · ★ 5/5
There were several issues concerning both the technical side and overall understanding. Mikhail responded quickly, resolved the technical problems, and helped me understand them — many thanks. I'm satisfied with the result.
There were several issues concerning both the technical side and overall understanding. Mikhail responded quickly to the request, helped sort things out and resolved the technical problems and helped clarify …
VPS setup, server setup
2026-02-18 · ★ 5/5
Everything was done quickly and efficiently. I recommend.
Everything was done quickly and efficiently. I recommend.
VPS setup, server setup
2026-01-17 · ★ 5/5
Everything went well; the contractor responded quickly to questions and helped resolve the issue. Thanks!
Everything went well, the contractor responded quickly to questions and helped resolve the issue. Thank you!
VPS setup, server setup
2025-12-16 · ★ 5/5
// Contact
Need help?
Get in touch with me and I'll help solve the problem
I reply within one business day (03:00-13:00 GMT)
Или оставьте заявку здесь:
// Related