Next 13 Docker Help
Unanswered
Sander posted this in #help-forum
SanderOP
Is there anyone who can help me with docker? I am way out of my depth, it worked then not, then a while, now not and now new errors:
So maybe volumes is messed up or something..?
I Used Next with-docker/Dockerfile as a template and changed next to nothing.
docker-compose.prod.yaml
Cannot find module '/app/server.js'So maybe volumes is messed up or something..?
I Used Next with-docker/Dockerfile as a template and changed next to nothing.
docker-compose.prod.yaml
version: "3"
services:
innosend-retour-frontend:
build:
context: .
dockerfile: Dockerfile
container_name: innosend-retour-frontend
restart: on-failure
volumes:
- ./:/app
- /app/node_modules
- /app/.next
ports:
- 3000:300089 Replies
SanderOP
Dockerfile
# syntax=docker/dockerfile:1.4
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY --link package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed \
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --link . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
#COPY .env.production .env.production
#COPY next.config.js next.config.js
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1
RUN \
addgroup --system --gid 1001 nodejs; \
adduser --system --uid 1001 nextjs
COPY --from=builder --link /app/public ./public
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --link --chown=1001:1001 /app/.next/standalone ./
COPY --from=builder --link --chown=1001:1001 /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME localhost
CMD ["node", "server.js"]European sprat
You're building the nextjs app in the image and then in your compose file trying to map your host volume into the container. You shouldn't be mapping any host directories into the container. The point of the dockerfile is to build the nextjs app as a standalone image which can be run anywhere
SanderOP
ohww
so something like this?
except it will not be called next i guess
services:
innosend-retour-frontend:
+ image: next
build:
context: .
dockerfile: Dockerfile
container_name: innosend-retour-frontend
restart: on-failure
volumes:
- ./:/app
- /app/node_modules
- /app/.next
ports:
- 3000:3000except it will not be called next i guess
how is this meant to work?
European sprat
You shouldn't have any of those volumes in the compose file
SanderOP
ok got it
European sprat
What exactly are you trying to achieve with using Docker?
SanderOP
At the company I work for they use docker and terraform as a deployment strategy on azure.
I need to get this to work on a production enviroment but we were getting tons of ECONNREFUSED and err_name_not_resolved. ECONNREFUSED due to CORS errors and having to use
So TL:DR
Get a production build of the app working together with the API
I need to get this to work on a production enviroment but we were getting tons of ECONNREFUSED and err_name_not_resolved. ECONNREFUSED due to CORS errors and having to use
host.docker.internal instead of localhost in environment files and this story goes on and on.So TL:DR
Get a production build of the app working together with the API
the DEV version does work however
But maybe not after some changes (not confident anymore)
But maybe not after some changes (not confident anymore)
European sprat
Ok and you're just trying to test the production build locally?
SanderOP
yes because we were having CORS errors in production enviroment so I was told to try get it working locally first
European sprat
And this dockerfile is what is currently being used in production where you are getting the issues or is there a different dockerfile?
SanderOP
yes I have Dockerfile and Dockerfile.dev
Dockerfile | Prod / Local & Azure
Dockerfile.dev | Dev / Local
Dockerfile.dev | Dev / Local
European sprat
When I get to my desk I can share some more about what I'm using for docker and production
SanderOP
that would be AWESOME
I also gotta go to gym right now and go bowling with the girlfriend so yes i'll be waiting
European sprat
the first thing is make sure you your next config is set to output standalone (which it should be if you were following the "with-docker" example)
i don't know exactly what your build process or environment is like but for me, I have to pass a number of environment variables as docker build ARGs into the dockerfile so when the image is built (and nextjs gets built) it can use those variables.
in each of the dockerfile stages i'm adding ARGs and ENVs like this:
i don't know exactly what your build process or environment is like but for me, I have to pass a number of environment variables as docker build ARGs into the dockerfile so when the image is built (and nextjs gets built) it can use those variables.
in each of the dockerfile stages i'm adding ARGs and ENVs like this:
ARG ARG_PHP_URL
ARG ARG_BASE_URL
ARG ARG_BASE_PATH
ARG ARG_NEXT_PUBLIC_BASE_PATH
ARG ARG_CDN_URL
ARG ARG_BUILD_VERSION
ARG ARG_AWS_ACCESS_KEY_ID
ARG ARG_AWS_SECRET_ACCESS_KEY
ENV BASE_URL=$ARG_BASE_URL
ENV PHP_URL=$ARG_PHP_URL
ENV BASE_PATH=$ARG_BASE_PATH
ENV NEXT_PUBLIC_BASE_PATH=$ARG_NEXT_PUBLIC_BASE_PATH
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
ENV CDN_URL=$ARG_CDN_URL
ENV BUILD_VERSION=$ARG_BUILD_VERSION
ENV AWS_ACCESS_KEY_ID=$ARG_AWS_ACCESS_KEY_ID
ENV AWS_SECRET_ACCESS_KEY=$ARG_AWS_SECRET_ACCESS_KEYand then when building the image you pass in the args in the docker build command or in the compose file. in production, my CI does all that with docker build but when i want to test the image locally i'm using compose:
services:
nextjs:
container_name: nextjs
image: nextjs:latest
ports:
- "3000:3000"
build:
context: ./../../.
dockerfile: ./docker/production/Dockerfile.dev
args:
ARG_PHP_URL: ${PHP_URL}
ARG_BASE_URL: ${BASE_URL}
ARG_BASE_PATH: ${BASE_PATH}
ARG_NEXT_PUBLIC_BASE_PATH: ${NEXT_PUBLIC_BASE_PATH}
ARG_AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
ARG_AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
ARG_BUILD_VERSION: ${BUILD_VERSION}
ARG_CDN_URL: ${CDN_URL}
environment:
- PRISMA_DB_URL=${PRISMA_DB_URL}
- NEXT_JWT_SECRET=${NEXT_JWT_SECRET}
- BASE_URL=${BASE_URL}
- PHP_URL=${PHP_URL}
- BASE_PATH=${BASE_PATH}
- NEXT_PUBLIC_BASE_PATH=${NEXT_PUBLIC_BASE_PATH}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- DB_QUERY_LOGGING=${DB_QUERY_LOGGING}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- DYNAMODB_SESSION_TABLE_NAME=${DYNAMODB_SESSION_TABLE_NAME}
- CDN_URL=${CDN_URL}
- BUILD_VERSION=${BUILD_VERSION}SanderOP
I uses .env.production by default
It is loaded as we can see in logs
European sprat
and those variables are loaded during the Dockerfile build time when RUN yarn build happens?
maybe in your scenario they aren't needed but i found in mine they were
SanderOP
Those are loaded when the container starts
Ohw wait you make a good point... server.js might not load env or will it mhm
SanderOP
Anyone online right now who is available to help?
SanderOP
Progress
For server component fetch requests OR server side requests the requests must be to http://host.docker.internal:5000 but on the client it HAS to go to http://localhost:5000
Any ways to make this more consistent?
For server component fetch requests OR server side requests the requests must be to http://host.docker.internal:5000 but on the client it HAS to go to http://localhost:5000
Any ways to make this more consistent?
SanderOP
I know why requests failed. My backend devs didnt add a Allow Access Control Origin: *
They have been using proxy hacks since the beginning
European sprat
Were you able to fix it then after changing that?
SanderOP
They are pretty stubborn and think it introduces security risks while being industry standard
Glad i know laravel so I could test the scenario with a properly configured cors config. Instead if their python flask backend 😅
European sprat
lol
Broad-snouted Caiman
@Sander how did you end up resolving the issue with localhost inside the container? I’m running into that currently and it is so confusing to me.
Broad-snouted Caiman
@European sprat, do you mind explaining how you were able to get env vars injected into your Docker container? I've been hacking at this problem for quite some time today and just can't seem to crack it. Like most, this works fine when developing locally, but for some reason, it doesn't seem that my standalone Next.js app is reading in the vars once in a Docker container
@Broad-snouted Caiman <@135324139648057344>, do you mind explaining how you were able to get env vars injected into your Docker container? I've been hacking at this problem for quite some time today and just can't seem to crack it. Like most, this works fine when developing locally, but for some reason, it doesn't seem that my standalone Next.js app is reading in the vars once in a Docker container
European sprat
are you on next 13.4.15? if so there's a bug when trying to pass vars into the container
Broad-snouted Caiman
damn right I am lmao!
European sprat
i ran into this issue today
Broad-snouted Caiman
dude great freaking find. You just saved me several more hours of troubleshooting this.
European sprat
i'm glad you pinged me since i spent most of the day on this plus some other bug they introduced in 13.4.13 lol
Broad-snouted Caiman
I posted earlier to @Sander , but I'll ask you too– how exactly do you handle SSR/server-side calls in Docker containers? I saw Sander mention using host.docker.internal but that doesn't seem to work for me.
European sprat
i just have the port mapped to 3000 so i access it on localhost
@European sprat i'm glad you pinged me since i spent most of the day on this plus some other bug they introduced in 13.4.13 lol
Broad-snouted Caiman
Yeah I've personally ran into a lot of issues as of late. I guess that's what happens when you stay on the bleeding edge haha
@European sprat i just have the port mapped to 3000 so i access it on localhost
Broad-snouted Caiman
so if the app is running on port 3000 in the container, a SSR page/RSC can fetch from localhost:3000 and it work? I tried that in production a few times lately and that didn't seem to work.
European sprat
like you mean from your browser at http://localhost:3000 ?
it could be differences in OS, i'm on linux
i know docker host internal whatever was always a thing with macs
Broad-snouted Caiman
nah, I get how to do that, but recently I've found that any sort of redirect (middleware, api route, etc) redirects fine locally but once you put it in a Docker container it doesn't know how to resolve the baseUrl. I thought it was also happening in RSCs but I think downgrading to
13.4.12 fixed that.For example:
will work fine locally, but once you throw it in a Docker container, I think the host/hostname of the
import { NextRequest, NextResponse } from "next/server";
export function GET(req: NextRequest) {
// redirect user to another route
const uri = new URL("/api/health", req.url);
console.log({ uri })
return NextResponse.redirect(uri, { status: 302 });
}will work fine locally, but once you throw it in a Docker container, I think the host/hostname of the
req.url resolves to localhost:3000 and therefore sort of breaks any redirect. From what I've read, it sounds like it's because localhost actually refers to the container itself, not the host. That sort of breaks things once you use something like NGINX to reverse proxy. I was wondering if you ever came across this or are knowledgeable of a fix.European sprat
Ahh yea you're right and I do have to use a base URL env var
Which I have set to localhost if I'm testing the build locally and then my production URL otherwise
Broad-snouted Caiman
My peanut brain thinks
localhost should always map to the host machine because that's the way it usually is until Docker is involved.European sprat
Actually I'm not sure if a base URL var is necessary for me, I'm seeing a redirect I do in middleware and it like this
@European sprat Which I have set to localhost if I'm testing the build locally and then my production URL otherwise
Broad-snouted Caiman
Do you mind sharing how you do that? I'm assuming you just add an additional env var? It kind of sucks that all the Next examples use the
req.url but that doesn't actually translate 1:1 with Docker deployments. It's kind of makes you wonder how services like railway supposedly Dockerize applications under the hood when you deploy.European sprat
return new NextResponse(null, {
status: 307,
headers: {
...requestHeaders,
Location: new URL(
},
})
status: 307,
headers: {
...requestHeaders,
Location: new URL(
/next/writer/articles/${articleId}/sections, req.url).toString(), },
})
@European sprat return new NextResponse(null, {
status: 307,
headers: {
...requestHeaders,
Location: new URL(`/next/writer/articles/${articleId}/sections`, req.url).toString(),
},
})
European sprat
But this may have not been tested in production 

@European sprat return new NextResponse(null, {
status: 307,
headers: {
...requestHeaders,
Location: new URL(`/next/writer/articles/${articleId}/sections`, req.url).toString(),
},
})
Broad-snouted Caiman
Interesting that you're able to use req.url. I'm 99% sure that's causing a problem on my end.
European sprat
I'll have to check it in the prod build later to confirm
Broad-snouted Caiman
If you're able, you should try adding a redirect into a route handler like I did and see where it redirects you to. I ran into this after creating a route named
/api/docs that just redirects to /api/v1/docs under the hood. Didn't know until production that Docker basically hijacks localhost and therefore NGINX doesn't pick up the redirect url for proxying.So for a hot second we actually had an https site that would send you to localhost:3000 via some of our redirects lmao
European sprat
Oh you're using nginx? I wonder how that changes things up since I'm just using node
Broad-snouted Caiman
Yeah I'd be curious. I tried Caddy too and got the same result. I'm honestly not too well-versed in NGINX, but my understanding is that you pass a
Host header when you reverse proxy and that's traditionally how redirects just "work". BUT, once you redirect internally in the Docker container it basically never makes it to NGINX and therefore those headers never get set.In other words I think you can redirect once and have your headers preserved. A redirect after another redirect will break. I'm honestly so confused how people have successfully deployed using Docker in the past because there are so many bugs and gotchas like this that don't seem to have any solutions from what I've seen.
@European sprat i'm glad you pinged me since i spent most of the day on this plus some other bug they introduced in 13.4.13 lol
Polar bear
Had any issues with the <Image> tag?
European sprat
Well it deploys just fine for me but I'm not using reverse proxy or anything. AWS ECS behind a load balancer
Broad-snouted Caiman
that's wild. probably an issue on my part then.
Polar bear
I wonder if that's also what's getting me tbh
@Polar bear I wonder if that's also what's getting me tbh
Broad-snouted Caiman
wym
Polar bear
I've got an open question here and it may also be because of docker stuff
But it's frustrating because it's worked (local and deployed) for 8 months... and today I deploy a new version and magically it's all bork'd
Broad-snouted Caiman
can you link to your question? I'll hop over and take a look
@Broad-snouted Caiman Yeah I'd be curious. I tried Caddy too and got the same result. I'm honestly not too well-versed in NGINX, but my understanding is that you pass a `Host` header when you reverse proxy and that's traditionally how redirects just "work". BUT, once you redirect internally in the Docker container it basically never makes it to NGINX and therefore those headers never get set.
European sprat
this redirects fine:
return new NextResponse(null, {
status: 307,
headers: {
...requestHeaders,
Location: new URL(`/next/writer/articles/${articleId}/sections`, req.url).toString(),
},
})when i console.log(req.url), i can see it shows the container internal host name:
http://7c51bdfb1add:3000/next/writer/articles/7/sections
http://7c51bdfb1add:3000/next/writer/articles/7/sections
@European sprat this redirects fine:
return new NextResponse(null, {
status: 307,
headers: {
...requestHeaders,
Location: new URL(`/next/writer/articles/${articleId}/sections`, req.url).toString(),
},
})
Broad-snouted Caiman
Interesting. What happens if that redirect sends you to another route that redirects also? I think that is the problem I'm facing.
Also, where are you getting the request headers from? Are you doing something like this?
export function GET(req: NextRequest) {
const requestHeaders = req.headers;
// ...rest
};@Broad-snouted Caiman Also, where are you getting the request headers from? Are you doing something like this?
typescript
export function GET(req: NextRequest) {
const requestHeaders = req.headers;
// ...rest
};
European sprat
I'm adding headers some headers at the beginning of the middleware
Broad-snouted Caiman
did you use the same redirect config in the second redirect?
European sprat
yeah i copy/pasted and changed the location
Broad-snouted Caiman
can you try redirecting like so?
notice I'm not spreading the headers and I'm also not setting the Location response header.
// src/app/foo/route.ts
export function GET(req: NextRequest) {
const url = new URL("/bar", req.url);
return NextResponse.redirect(url, { status: 302 });
};
// src/app/bar/route.ts
export function GET(req: NextRequest) {
const url = new URL("/hello", req.url);
return NextResponse.redirect(url, { status: 302 });
};
// src/app/hello/route.ts
export function GET(req: NextRequest) {
console.log("Made it!");
return NextResponse.json({ message: "Hello!" });
};notice I'm not spreading the headers and I'm also not setting the Location response header.
European sprat
oh i haven't been redirecting in route handlers at all
it's all middleware
Broad-snouted Caiman
dude, I wonder if that's my problem
Broad-snouted Caiman
@European sprat, I think you actually pointed out something really valuable...I just tried doing the redirect in the
middleware.ts file and the redirect behaves perfectly. The issue is 100% redirects happening in API routes/route handlers. I even tried mapping my local port to 8080 instead of the default 3000 and still got the same results. So that explains why redirects are working for you. All that to say, looks like redirects in middleware.ts is the way to go if using Docker. I really appreciate the help and your patience as I worked though this!