Route Handlers in Docker container
Answered
gryt posted this in #help-forum
grytOP
Hi I have a nextjs application that I built using antd components. I have built a backend using FastAPI and made use of the route handlers shown in this documentation:
https://nextjs.org/docs/app/building-your-application/routing/route-handlers
Everything works fine in dev environment
In @/api/upload/route.js:
In client component @/components/Table.js:
https://nextjs.org/docs/app/building-your-application/routing/route-handlers
Everything works fine in dev environment
npm run dev however when I containerise the application using Docker. I get the error that the localhost:3000/api/upload route is not working. I have checked that the endpoint localhost:8000/upload works fine. For the upload button I am using a component from antdesign and it made use of the props element to trigger the actions. Anyone who knows. Can provide some advice. Thanks!In @/api/upload/route.js:
export async function POST(req) {
const formData = await req.formData();
const res = await fetch('http://localhost:8000/upload', {
method: 'POST',
body: formData,
});
const data = await res.json();
console.log("this is server res data", data);
return NextResponse.json(data);
}In client component @/components/Table.js:
const props = {
name: 'file',
action: 'http://localhost:3000/api/upload',
headers: {
authorization: 'authorization-text',
},
async onChange(info) {
if (info.file.status !== 'uploading') {
console.log(info.file, info.fileList);
console.log("hello world");
}
if (info.file.status === 'done') {
message.success(`file uploaded successfully`);
// console.log('##info', info.file.response.data);
setDataSource(info.file.response.data);
} else if (info.file.status === 'error') {
message.error(`file upload failed.`);
}
},
};
<Upload {...props} showUploadList={false}>
<Button icon={<UploadOutlined />}>Click to Upload</Button>
</Upload>Answered by Ray
ok try changing
http://localhost:8000/upload to http://backend:8000/upload when process.env.NODE_ENV === 'production'40 Replies
grytOP
bump
@Ray what is the error you got from localhost:3000/api/upload?
grytOP
from the client component, the error is this:
on the network side:
⨯ TypeError: fetch failed
at node:internal/deps/undici/undici:12344:11
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
cause: Error: connect ECONNREFUSED 127.0.0.1:8000
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1595:16)
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
errno: -111,
code: 'ECONNREFUSED',
syscall: 'connect',
address: '127.0.0.1',
port: 8000
}
}on the network side:
url: http://localhost:3000/api/upload
Status: 500 Internal Server Error
Source: NetworkAddress: ::1:3000@Ray look like its unable to connect :8000?
grytOP
Yup i am running it in docker. port 8000 is the port to my backend service
@Ray look like its unable to connect :8000?
grytOP
since i am using fastapi, i was able to verified that port 8000 connection works fine as i am able to acccess the swagger UI to test the endpoints
@gryt since i am using fastapi, i was able to verified that port 8000 connection works fine as i am able to acccess the swagger UI to test the endpoints
could you connect to the container and try to curl the endpoint and see if it can connect it?
@Ray could you connect to the container and try to curl the endpoint and see if it can connect it?
grytOP
as i am currently running 2 containers, one for the nextjs frontend and the other for my backend. should i access the frontend container or the backend to do this testing?
@gryt as i am currently running 2 containers, one for the nextjs frontend and the other for my backend. should i access the frontend container or the backend to do this testing?
frontend since you get the error on frontend
@gryt as i am currently running 2 containers, one for the nextjs frontend and the other for my backend. should i access the frontend container or the backend to do this testing?
did you deploy them with docker-compose? usually we can connect other container by their name
grytOP
yup i deploy it with docker-compose: this is my current file:
docker-compose.yml
docker-compose.yml
version: '3.8'
services:
backend:
container_name: backend
build:
context: ./backend
command: uvicorn app:app --host 0.0.0.0 --port 8000 --reload
ports:
- 8000:8000
environment:
API_KEY: ${API_KEY}
URI: ${URI}
MONGO_DB: ${MONGO_DB}
restart: unless-stopped
networks:
- internal-network
- default
#depends_on:
#caddy:
#condition: service_started
frontend:
container_name: frontend
build:
context: ./frontend/my-app
dockerfile: Dockerfile
restart: always
ports:
- 3000:3000
networks:
- internal-network
- default@gryt yup i deploy it with docker-compose: this is my current file:
docker-compose.yml
docker
version: '3.8'
services:
backend:
container_name: backend
build:
context: ./backend
command: uvicorn app:app --host 0.0.0.0 --port 8000 --reload
ports:
- 8000:8000
environment:
API_KEY: ${API_KEY}
URI: ${URI}
MONGO_DB: ${MONGO_DB}
restart: unless-stopped
networks:
- internal-network
- default
#depends_on:
#caddy:
#condition: service_started
frontend:
container_name: frontend
build:
context: ./frontend/my-app
dockerfile: Dockerfile
restart: always
ports:
- 3000:3000
networks:
- internal-network
- default
ok try changing
http://localhost:8000/upload to http://backend:8000/upload when process.env.NODE_ENV === 'production'Answer
@Ray ok try changing `http://localhost:8000/upload` to `http://backend:8000/upload` when `process.env.NODE_ENV === 'production'`
grytOP
alright let me try that out thanks
@Ray ok try changing `http://localhost:8000/upload` to `http://backend:8000/upload` when `process.env.NODE_ENV === 'production'`
grytOP
where should i include
process.env.NODE_ENV === 'production' ?@gryt where should i include `process.env.NODE_ENV === 'production'` ?
like this
export async function POST(req) {
const formData = await req.formData();
const apiUrl = process.env.NODE_ENV === 'production' ? 'backend': 'localhost'
const res = await fetch(`http://${apiUrl}:8000/upload`, {
method: 'POST',
body: formData,
});
const data = await res.json();
console.log("this is server res data", data);
return NextResponse.json(data);
}@Ray like this
ts
export async function POST(req) {
const formData = await req.formData();
const apiUrl = process.env.NODE_ENV === 'production' ? 'backend': 'localhost'
const res = await fetch(`http://${apiUrl}:8000/upload`, {
method: 'POST',
body: formData,
});
const data = await res.json();
console.log("this is server res data", data);
return NextResponse.json(data);
}
grytOP
am i suppose to declare the env variable within my Dockerfile? it is currently like this:
FROM node:20-alpine AS base
# Step 1. Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
# Omit --production flag for TypeScript devDependencies
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; \
# Allow install without lockfile, so example works even without Node.js installed locally
else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \
fi
# COPY . .
COPY app ./app
COPY components ./components
COPY public ./public
COPY next.config.js .
COPY jsconfig.json .
# Environment variables must be present at build time
# https://github.com/vercel/next.js/discussions/14030
# ARG ENV_VARIABLE
# ENV ENV_VARIABLE=${ENV_VARIABLE}
# ARG NEXT_PUBLIC_ENV_VARIABLE
# ENV NEXT_PUBLIC_ENV_VARIABLE=${NEXT_PUBLIC_ENV_VARIABLE}
# Build Next.js based on the preferred package manager
RUN \
if [ -f yarn.lock ]; then yarn build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then pnpm build; \
else yarn build; \
fi
# Step 2. Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
# Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER 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
# this HOSTNAME environment variable in used in server.js
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]@Ray no need, next will do it
grytOP
i see, i am trying to rebuild the containers now, will try it out
let me know if it works
@Ray let me know if it works
grytOP
Nope it doesnt.
â–² Next.js 14.0.4
- Local: http://localhost:3000
- Network: http://0.0.0.0:3000
✓ Ready in 51ms
⨯ TypeError: fetch failed
at node:internal/deps/undici/undici:12344:11
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
cause: Error: connect ECONNREFUSED 172.22.0.3:8000
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1595:16)
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
errno: -111,
code: 'ECONNREFUSED',
syscall: 'connect',
address: '172.22.0.3',
port: 8000
}
}this are the errors when i tried to curl
/app $ curl localhost:8000/
curl: (7) Failed to connect to localhost port 8000 after 1 ms: Couldn't connect to server
/app $ curl backend:8000/
curl: (7) Failed to connect to backend port 8000 after 7 ms: Couldn't connect to server@Ray try curl backend:8000
grytOP
yup i tried both, localhost:8000 and backend:8000
which mean the containers cannot communicate each other
@Ray which mean the containers cannot communicate each other
grytOP
i see, i have tried to add an internal network for both of them
@Ray is this full code on docker-compose?
grytOP
this is the full code:
version: '3.8'
services:
backend:
container_name: backend
build:
context: ./backend
command: uvicorn app:app --host 0.0.0.0 --port 8000 --reload
ports:
- 8000:8000
environment:
API_KEY: ${API_KEY}
URI: ${URI}
MONGO_DB: ${MONGO_DB}
restart: unless-stopped
networks:
- internal-network
- default
#depends_on:
#caddy:
#condition: service_started
frontend:
container_name: frontend
build:
context: ./frontend/my-app
dockerfile: Dockerfile
restart: always
ports:
- 3000:3000
networks:
- internal-network
- default
networks:
internal-network:
name: internal-network
driver: bridgeversion: '3.8'
services:
backend:
container_name: backend
build:
context: ./backend
command: uvicorn app:app --host 0.0.0.0 --port 8000 --reload
ports:
- 8000:8000
environment:
API_KEY: ${API_KEY}
URI: ${URI}
MONGO_DB: ${MONGO_DB}
restart: unless-stopped
networks:
- internal-network
- app-network
#depends_on:
#caddy:
#condition: service_started
frontend:
container_name: frontend
build:
context: ./frontend/my-app
dockerfile: Dockerfile
restart: always
ports:
- 3000:3000
networks:
- internal-network
- app-network
networks:
internal-network:
name: internal-network
driver: bridge
app-network:
external: true
try this
grytOP
Hey roy, apologizes. Earlier i realized my backend wasnt running properly due to a small bug within the scripts. I managed to fixed it and i have tried curl backend:8000 and it is responding with a
The solution was to change it to my backend container service and it worked. Thank you so much for that
health_check message i set. After that i tried to test the application fully and it worked. The solution was to change it to my backend container service and it worked. Thank you so much for that
By the way, would you recommend using route handlers for production? It works fine if i just make a direct http request from the client component. Instead of routing it through a /api route through the server side in nextjs
@gryt By the way, would you recommend using route handlers for production? It works fine if i just make a direct http request from the client component. Instead of routing it through a /api route through the server side in nextjs
yes it is fine but I would use server action over route handler these day
auto typesafe
grytOP
i see. does server action has the same concept as route handlers? for context, i am very new to nextjs.
@gryt i see. does server action has the same concept as route handlers? for context, i am very new to nextjs.
basically, you defined an async function which return the result and next will create the route handler for you
and you just execute the function in the client component
so you get typesafe for the input and output
if you use typescript
grytOP
I see, alright i will look into it some other day. Also, since i am new to frontend dev, i am struggling with grasping the concept of css. I have an idea of how i want to design my app on figma, but i could not really execute in code. are there any templates you would recommend me using? or i could just hop on using tailwind now