Next.js gives you two deployment paths: managed (Vercel) and self-hosted (Docker). The managed option works well if vendor lock-in and dollar-denominated billing are not a concern. Self-hosting gives you full control over the runtime, the infrastructure, and the final bill.
The problem is that most Next.js Docker tutorials skip the standalone output. They copy the entire .next folder into the image and produce a 1.2GB container that takes forever to start. It does not have to be that way. With standalone mode and a multi-stage Dockerfile, the image stays under 150MB and the app runs without the full node_modules.
This guide walks through the full path from next.config.js to a live service with HTTPS on Guara Cloud, running on infrastructure in Brazil.
Quick answer
To deploy a Next.js app with Docker in Brazil, enable output: 'standalone' in next.config.js, use a multi-stage Dockerfile that copies only the standalone directory and static assets, expose port 3000, and publish the container on Guara Cloud. The platform handles HTTPS, public domain, logs, and bills in BRL.
Key takeaways
- Use
output: 'standalone'in next.config.js. This generates a minimal server that does not need the fullnode_modules. - The multi-stage Dockerfile separates install, build, and production stages. The final image weighs around 130MB.
- The
server.jsgenerated by standalone listens on port 3000 by default. SetHOSTNAME=0.0.0.0to accept external connections. - Static assets (
public/and.next/static) must be manually copied into the standalone directory in the final image. - Set environment variables from the Guara Cloud dashboard, not in code.
When this tutorial applies
Use this flow for Next.js applications using App Router or Pages Router that need to run self-hosted. It works with API routes, Server Actions, SSR, ISR, and static routes. If the project uses next/image with local optimization, the container supports it through sharp (included in the standalone bundle).
When not to use this flow
If the project is 100% static (output: 'export'), you do not need a Node.js container. Any CDN serves the HTML files. If the app requires Edge Runtime (middleware with Edge, geolocation at the edge), self-hosting loses that capability since Edge Runtime is specific to serverless platforms. For those cases, consider static export to a CDN plus a separate container for routes that need SSR.
Before you start
- A Next.js 14+ project created with create-next-app or an existing one
- Node.js 20+ installed locally for testing
- Docker installed to validate the image
- A Guara Cloud account (app.guaracloud.com)
1. Enable standalone output
Open next.config.js (or next.config.mjs) and add the option:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
};
export default nextConfig; When you run next build, Next.js generates the .next/standalone directory with a minimal server.js that loads only the required dependencies. No 500MB node_modules.
If the project uses @next/font or next/image with sharp, they are included automatically in the standalone bundle.
2. Multi-stage Dockerfile for Next.js
Here is the Dockerfile I actually use for my Next.js projects on Guara Cloud. Four stages, each with a clear job.
FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /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"
CMD ["node", "server.js"] A few things about this Dockerfile:
The npm ci in the deps stage is separated from build. When you change code but not dependencies, Docker reuses the deps layer and the build is faster.
The non-root nextjs user is created in the final stage. I know plenty of people skip this, but running containers as root is asking for trouble, even when the platform sandboxes the process.
The HOSTNAME=0.0.0.0 makes the server accept connections from outside the container. Without it, Next.js listens only on 127.0.0.1 and the platform health check fails. This is an error I see a lot of people hit on their first deploy.
3. Add the .dockerignore
Without this, Docker copies node_modules and .next into the build context, which makes every build 30 seconds slower.
node_modules
.next
.git
.gitignore
docker-compose*.yml
Dockerfile
*.md
.env*.local 4. Configure environment variables
In the Guara Cloud dashboard, add the variables the Next.js app consumes:
Common environment variables for Next.js
| Name | Value |
|---|---|
NODE_ENV | production |
DATABASE_URL | postgres://user:***@host:5432/mydb |
NEXT_PUBLIC_APP_URL | https://my-app.guaracloud.com |
JWT_SECRET | long-random-string |
Variables prefixed with NEXT_PUBLIC_ are embedded in the JavaScript bundle at build time. If you change these variables, you need a new deploy. Variables without the prefix (like DATABASE_URL) are read at runtime on the server and can be changed without a rebuild.
5. Create a health check route
Guara Cloud uses HTTP probes to verify the container is responding. Create a simple route:
import { NextResponse } from 'next/server';
export const dynamic = 'force-dynamic';
export async function GET() {
return NextResponse.json({ status: 'ok' });
} If you use Pages Router, create pages/api/health.ts:
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.status(200).json({ status: 'ok' });
} The force-dynamic export ensures the route is not cached by ISR. The probe needs a fresh response.
6. Deploy on Guara Cloud
With the repository on GitHub and the Dockerfile at the root, the process is:
Dashboard walkthrough
- Create a new service and connect your GitHub repository
- The platform detects the Dockerfile automatically
- Confirm the HTTP port (3000, the Next.js default)
- Add environment variables through the dashboard
- Start the deploy and watch the build logs in real time
The first build takes 3 to 5 minutes, including npm ci, next build, and image generation. Subsequent deploys are faster thanks to Docker layer caching. The deps layer only rebuilds when package-lock.json changes.
7. Validate the service in production
After the deploy completes, check:
- The public URL responds with 200.
- The
/healthroute returns{ status: 'ok' }. - SSR pages render correctly (not empty HTML).
- API routes work if the project has endpoints in
app/api/. - Logs show the Next.js server listening on port 3000.
If something fails, build and runtime logs are available in the dashboard. Most problems surface in the first few seconds.
Final image size and performance
With the Dockerfile above, the final image is around 130MB. That includes Alpine, Node.js 20, the standalone server.js, and static assets. For reference, a naive Dockerfile that copies the entire node_modules goes past 900MB.
Container cold start time is about 1 to 2 seconds on Guara Cloud. The Next.js standalone output comes with routes pre-compiled, so the first request has no additional compilation overhead.
Common issues
- Problem The container starts but the health check fails
- Solution Verify that HOSTNAME is set to "0.0.0.0". Without it, Next.js listens only on loopback. Also confirm the port in the dashboard is set to 3000.
- Problem Images are broken or CSS does not load
- Solution The public/ and .next/static files were not copied to the correct directory. In the Dockerfile, confirm the two COPY lines that move these directories into the standalone path.
- Problem API routes return 500 with "Module not found"
- Solution A dependency used in API routes was not included in the standalone bundle. Check that it is properly imported in the code. The standalone mode tracks imports automatically, but re-exports from packages with dynamic resolution can fail.
- Problem The Next.js build fails with an out-of-memory error
- Solution The next build needs more RAM during compilation. Add NODE_OPTIONS="--max-old-space-size=4096" as a build environment variable on Guara Cloud or in the builder stage of the Dockerfile.
- Problem NEXT_PUBLIC_ variables do not appear on the client
- Solution These variables are embedded in the JavaScript bundle during the build. If you changed the value after deploy, you need a new build. They are not read at runtime.
- Problem next/image returns 500 in the container
- Solution Sharp may not have been included. Confirm you are using the default next/image (not a custom loader) and that package-lock.json includes sharp in the dependencies.
Differences between self-hosted and Vercel
A few things change when you run Next.js in a container instead of Vercel:
Edge Runtime does not work. Middleware defined with runtime: 'edge' needs a platform that supports the Edge Runtime (Vercel, Cloudflare Workers). In a container, use the Node.js runtime for middleware.
ISR works, but invalidation is local. On Vercel, revalidation is global and distributed across the edge network. Self-hosted, each container keeps its own cache. If you have multiple replicas, revalidation does not propagate automatically between them.
Preview deploys need manual configuration. Vercel creates a deploy per PR automatically. On Guara Cloud, you can create separate staging environments, but the preview deploy per branch flow is different.
Cost. This is where self-hosting really wins. A Next.js app on Guara Cloud with 512MB of RAM costs a fraction of what Vercel charges on the Pro plan in dollars, with a 6.38% IOF tax on top. And billing comes in BRL, with a proper invoice.
Can I use App Router and Pages Router together?
Yes. Next.js supports both in the same project, and the standalone output works with both. The Dockerfile stays the same.
Does standalone mode support Server Actions?
Yes. Server Actions work normally in the Node.js runtime. They just don't work in Edge Runtime, and that runtime does not exist in a container anyway.
How do I scale horizontally?
On Guara Cloud, increase the number of service replicas. Each replica runs an independent instance of server.js. To share ISR cache between replicas, consider using Redis as a cache backend.
Is billing in BRL?
Yes. Guara Cloud bills in BRL through Stripe. No dollar conversion, no IOF on your credit card.
Do I need Nginx in front of Next.js?
No. Guara Cloud provides HTTPS, a public domain, and routing without you configuring Nginx or TLS certificates manually.
Ship your Next.js on Guara Cloud
Docker deploys, managed HTTPS, real-time logs, and BRL billing. Infrastructure in São Paulo.