画像をMiniOサーバにアップロードするアプリを作成してビルド後自鯖にデプロイしてみたところ、Server Actionsの1MB制限に引っかかってしまいアップロードできない事態に見舞われました。開発環境だと問題なく上げられたのでちょっと困りました。

Next.js13以下ではnext.config.tsに下記を追加することで回避できたようですが、Next.js15 App Routerでは使用できません。

next.config.ts
// Next.js13以前の設定
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // これを追記
  experimental: {
    serverActions: {
      bodySizeLimit: '500mb',
    },
  },
};

export default nextConfig;

Next.js15ではスタンドアロンモード(最小限の構成でビルド)でデプロイすることでExpressのserver.js側でアップロード上限を設定できるようなのでやってみました。next.config.tsに下記を追記します。

next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // スタンドアロンモードで出力
  output: "standalone",
};

export default nextConfig;

設定系は/myapp/.env.productionに書いています。

ディレクトリ構成が下記になっているので適宜変更してください。

/
├─ myapp/ (Next.jsアプリ)
│  ├─ app/
│  │  └─ page.tsx
│  ├─ .env.production
│  ├─ next.config.ts
│  ├─ package.json
│  ├─ package.lock.json
│  ├─ server.js
│  └─ tsconfig.json
└─ docker-compose.yml

自前でserver.jsを用意します

server.js
/* eslint-disable @typescript-eslint/no-require-imports */

// server.js
const express = require("express");
const next = require("next");
const path = require("path");

const port = parseInt(process.env.PORT || "3000", 10);
const dev = process.env.NODE_ENV !== "production";

const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const server = express();

  // 最大アップロードサイズを設定
  server.use(express.json({ limit: "500mb" }));
  server.use(express.urlencoded({ extended: true, limit: "500mb" }));
  
  server.use(
    "/_next/static",
    express.static(path.join(__dirname, ".next/static"))
  );

  server.all("*", (req, res) => {
    return handle(req, res);
  });

  server.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`);
  });
});
Dockerfile
# Dockerfile

FROM node:20-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=base /app/node_modules ./node_modules
COPY . .
RUN npm run build

# 本番用
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /server.js ./server.js

# Next standalone実行に必要
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
CMD ["node", "server.js"]
docker-compose.yml
services:
  app:
    build:
      context: ./myapp
      dockerfile: Dockerfile
    container_name: myapp-app
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    env_file:
      - ./myapp/.env.production
    depends_on:
      - db

volumes:
  postgres_data:

あとはNginx側でもclient_max_body_sizeを設定してあげます。

Nginx
server {
    listen 32222 ssl;
    ssl_certificate /home/owner/system/pem/server_cert.pem;
    ssl_certificate_key /home/owner/system/pem/server_key.pem;
    client_max_body_size 500m; # ここで上限設定
    
    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Next.jsのApp routerの同じファイル名だらけでタブの視認性が悪い問題解決!

globパターンが分かってなくてちょっと戸惑いました

VScodeのsetteing.jsonに下記

setting.json
  // タブ名カスタム
  "workbench.editor.customLabels.enabled": true,
  "workbench.editor.customLabels.patterns": {
    "**/admin/*/*/page.tsx": "${dirname(1)}${dirname(0)} - Page",
    "**/admin/*/page.tsx": "${dirname} - Page",
    "**/api/*/*/route.ts": "${dirname(1)}${dirname(0)} - API",
    "**/api/*/route.ts": "${dirname} - API",
    "**/layout.tsx": "${dirname} - Layout",
    "**/page.tsx": "${dirname} - Page",
    "**/route.ts": "${dirname} - API"
  }

admin/*/*/page.tsxなどにすることで階層を指定できるようです。

admin/upload.tsxは「upload – Page」、admin/edit/[id]/page.tsxは「edit[id] – Page」と[slug]が分かるようにしたかったので