Skip to main content

Deploy with Docker

Docker deployment is ideal for:

  • Self-hosted servers (VPS, dedicated server)
  • Kubernetes / container orchestration
  • Environments where you want full control over the infrastructure
  • Teams with existing Docker-based deployment pipelines

Prerequisites


Docker Compose (quickest)

The generated project includes a ready-to-use docker-compose.yml:

cd my-blog
cp .env.example .env # fill in your values
docker compose up -d

Access your blog at http://localhost:3000.

docker-compose.yml

services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NOTION_PAGE_ID=${NOTION_PAGE_ID}
- SITE_NAME=${SITE_NAME}
- SITE_DOMAIN=${SITE_DOMAIN}
- SITE_AUTHOR=${SITE_AUTHOR}
- SITE_DESCRIPTION=${SITE_DESCRIPTION}
- REVALIDATE_SECRET=${REVALIDATE_SECRET}
restart: unless-stopped

Dockerfile explained

The included Dockerfile uses a 3-stage build to minimize the final image size:

# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json bun.lock ./
RUN npm install --frozen-lockfile

# Stage 2: Build Next.js
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Stage 3: Minimal production runtime
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

# Next.js standalone output (includes only required files)
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public

EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]

The standalone output mode (configured in next.config.ts with output: "standalone") produces a server.js that bundles all required Node.js modules, reducing the final image to ~100–150 MB.

Enabling standalone output

If your next.config.ts doesn't already have output: "standalone", add it:

// next.config.ts
const nextConfig: NextConfig = {
output: "standalone",
// ...
};

Manual Docker commands

# Build the image
docker build -t my-blog .

# Run with environment variables
docker run -d \
--name my-blog \
-p 3000:3000 \
-e NOTION_PAGE_ID=abc123def456 \
-e SITE_NAME="My Blog" \
-e SITE_DOMAIN=myblog.com \
-e SITE_AUTHOR="Jane Doe" \
--restart unless-stopped \
my-blog

# Or use an env file
docker run -d \
--name my-blog \
-p 3000:3000 \
--env-file .env \
--restart unless-stopped \
my-blog

Production setup with nginx reverse proxy

For production, you'll typically want a reverse proxy in front of Next.js to:

  • Handle TLS/HTTPS (SSL certificates)
  • Serve static files directly (faster)
  • Handle compression
  • Rate limiting

nginx configuration

# /etc/nginx/sites-available/myblog.com
server {
listen 80;
server_name myblog.com www.myblog.com;
return 301 https://$server_name$request_uri;
}

server {
listen 443 ssl http2;
server_name myblog.com www.myblog.com;

ssl_certificate /etc/letsencrypt/live/myblog.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myblog.com/privkey.pem;

# Security headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header Referrer-Policy "strict-origin-when-cross-origin";

# Serve Next.js static files directly
location /_next/static/ {
alias /app/.next/static/;
expires 365d;
add_header Cache-Control "public, immutable";
}

location /static/ {
alias /app/public/;
expires 365d;
}

# Proxy everything else to Next.js
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}

SSL with Certbot (Let's Encrypt)

apt install certbot python3-certbot-nginx
certbot --nginx -d myblog.com -d www.myblog.com

Docker Compose with nginx

For a complete self-hosted setup:

# docker-compose.yml
services:
web:
build: .
expose:
- "3000"
env_file: .env
restart: unless-stopped

nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- web
restart: unless-stopped

Image builds for different platforms

If you're building on an Apple Silicon Mac but deploying to a Linux AMD64 server:

docker buildx build \
--platform linux/amd64 \
-t my-blog:latest \
--push \
.

Or use Docker Buildx with multi-platform support for images that run on both architectures.


Resource requirements

ResourceMinimumRecommended
RAM256 MB512 MB
CPU1 vCPU1–2 vCPU
Disk2 GB10 GB (if downloading images)

Next.js in standalone mode is quite lean. A $4–6/month VPS (e.g., Hetzner CX11, DigitalOcean Droplet) is sufficient for a personal blog.


Updating

# Pull latest code
git pull

# Rebuild and restart
docker compose down
docker compose build --no-cache
docker compose up -d

Or use a CI/CD pipeline (GitHub Actions → SSH deploy) to automate this on every push to main.