Context
- I have a Vike/React application that sends request to a backend API via Tanstack React-Query/Axios.
- I recently implemented SSR functionality via a migration from Vite to Vike.
The Problem
When running my application in a local terminal outside Docker, I'm having no problem. However, after creating a Docker implementation, it appears to be failing with SSR. I have a page at endpoint /blog
that makes a call to the backend API and renders the content server side before returning a response.
When I use curl
on the endpoint...
- Without Docker, the content fetched server-side from the backend API appears properly in the HTML.
- With Docker, the content does not appear in the HTML and I get the 500 Internal Server Error fallback.
If I navigate to /blog
via the client side, the content is properly rendered on the page. But, if I reload the page, it immediately fails and I get the fallback page.
What I've Tried
- Using
127.0.0.1
and the name of the container instead oflocalhost
when calling my backend APIs. - Exposing the
24678
port for the websocket connection. - Reinstalling node-modules.
- Logging the
PageContext
and checking for errors via Axios. - Verified that my backend API does not receive any call when I navigate to the
/blog
endpoint directly in my browser.
Code
Here is some of the relevant code.
index.ts (Express server)
import { createDevMiddleware, renderPage } from 'vike/server';
import compression from 'compression';
import express from 'express';
import { root } from './root.ts';
const isProduction = process.env.NODE_ENV === 'production';
startServer();
export async function startServer() {
const app = express();
app.use(compression())
// Vite integration
if (isProduction) {
// In production, we need to serve our static assets ourselves.
// (In dev, Vite's middleware serves our static assets.)
const sirv = (await import('sirv')).default;
app.use(sirv(`${root}/public`))
} else {
const { devMiddleware } = await createDevMiddleware({ root });
app.use(devMiddleware);
}
// Vike middleware. It should always be our last middleware (because it's a
// catch-all middleware superseding any middleware placed after it).
app.get('*', async (req, res) => {
const pageContextInit = {
urlOriginal: `${req.protocol}://${req.get('host')}` + req.originalUrl, // This will provide the full page URL to Vike's PageContext
headersOriginal: req.headers
}
const pageContext = await renderPage(pageContextInit);
if (pageContext.httpResponse.pipe) {
pageContext.httpResponse.pipe(res)
} else {
res.status(pageContext.httpResponse.statusCode).send(pageContext.httpResponse.body)
}
})
const port = process.env.PORT || 5174
app.listen(port)
console.log(`Server running at http://localhost:${port}`)
}
package.json "scripts"
"scripts": {
"dev": "npm run server:dev",
"prod": "npm run build && npm run server:prod",
"build": "vite build && tsc",
"server": "node --import 'data:text/javascript,import { register } from \"node:module\"; import { pathToFileURL } from \"node:url\"; register(\"ts-node/esm\", pathToFileURL(\"./\"));' ./server/index.ts",
"server:dev": "npm run server",
"server:prod": "cross-env NODE_ENV=production npm run server",
"lint": "eslint . --max-warnings 0"
}
Blog Page component
import { useConfig } from "vike-react/useConfig";
import { usePageContext } from "vike-react/usePageContext";
import { useSuspenseQuery } from "@tanstack/react-query";
import { withFallback } from "vike-react-query";
import ms from "ms";
import { Box, Divider, Flex, HStack, Tag, Text } from "@chakra-ui/react";
import ChakraMarkdown from "../../../../../../components/ChakraMarkdown";
import CustomMetaTags from "../../../../../../components/CustomMetaTags";
import GenericPage from "../../../../../../components/GenericPage";
import Title from "../../../../../../components/Title";
import BlogPost from "../../../../../../entities/BlogPost";
import APIClient, {
blogPostApiV1AxiosInstance,
} from "../../../../../../services/apiClient";
import { getMonthName } from "../../../../../../utils/utilities";
const blogPostApiClient = new APIClient<BlogPost>(blogPostApiV1AxiosInstance);
const cacheTtl = import.meta.env.VIKE_CACHE_TTL || "10s"; // 10s default cache time
// Blog Post Page
const Page = withFallback(
// Component if query is successful
() => {
const pageContext = usePageContext();
const { year, month, day, slug } = pageContext.routeParams;
// Query data
const query = useSuspenseQuery({
queryKey: ["blogPost", slug],
queryFn: () =>
blogPostApiClient.get(`/blogpost/${year}/${month}/${day}/${slug}`),
staleTime: ms(cacheTtl as string),
});
const blogPost = query.data?.content;
// Set meta tags
const config = useConfig();
config({
title: `${blogPost?.title} | Fernando Sesma`,
description: blogPost?.subtitle,
image: blogPost?.thumbnailUrl,
Head: <CustomMetaTags pageContext={pageContext} isArticle={true} />,
});
return (
<>
<GenericPage
isReturnable={true}
isReturnableLink="/blog"
pageWidth="700px"
>
<Box>
<Title titleText={blogPost?.title || ""} />
</Box>
<Box>
<Text fontSize="lg">{blogPost?.subtitle || ""}</Text>
</Box>
<Flex justifyContent="space-between" alignItems="center" pt={2}>
<HStack justify="flex-end" flexWrap="wrap">
{blogPost?.tags.length !== 0 &&
blogPost?.tags.map((tag) => <Tag key={tag}>{tag}</Tag>)}
</HStack>
<Text>{getMonthName(Number(month)) + ` ${day}, ${year}`}</Text>
</Flex>
<Box pt={5} pb={5}>
<Divider />
</Box>
<ChakraMarkdown content={blogPost?.postContent} />
</GenericPage>
</>
);
},
// Loading
// TODO: Make a loading component for the blog post page
undefined,
({ error }) => (
<GenericPage isReturnable={true} isReturnableLink="/blog" pageWidth="700px">
<Text fontSize="lg">{`An error occurred. Message: ${error.message}.`}</Text>
</GenericPage>
)
);
export default Page;
Dockerfile
FROM node:alpine
LABEL authors="fernandosesma"
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json ./
RUN npm install --silent
docker-compose.yaml
services:
ferns-web:
image: ferns-web:latest
build: .
command: npm run dev # 'dev' can be overriden with another profile to use
container_name: ferns-web
labels:
service: ferns-web # Labels required for deployment scripts
ports:
- "5174:5173"
- "24678:24678" # Expose these ports for WebSocket
volumes:
- .:/app
#
# For now we need to mount node_modules to the container from the project root
- /app/node_modules
environment:
- PUBLIC_ENV__VIKE_BLOG_POST_API_URL=http://localhost:8081
- PUBLIC_ENV__VIKE_MEDIA_API_URL=http://localhost:8082
- PORT=5173
volumes:
node_modules: {}
Axios error in console for React app (this is what appears when I navigate directly to /blog
2025-02-10 11:09:29 5:09:29 PM [vike][request(2)] Following error was thrown by the onRenderHtml() hook defined at vike-react/__internal/integration/onRenderHtml
2025-02-10 11:09:29 AxiosError [AggregateError]:
2025-02-10 11:09:29 at Function.AxiosError.from (file:///app/node_modules/axios/lib/core/AxiosError.js:92:14)
2025-02-10 11:09:29 at RedirectableRequest.handleRequestError (file:///app/node_modules/axios/lib/adapters/http.js:620:25)
2025-02-10 11:09:29 at RedirectableRequest.emit (node:events:507:28)
2025-02-10 11:09:29 at ClientRequest.eventHandlers.<computed> (/app/node_modules/follow-redirects/index.js:49:24)
2025-02-10 11:09:29 at ClientRequest.emit (node:events:507:28)
2025-02-10 11:09:29 at emitErrorEvent (node:_http_client:104:11)
2025-02-10 11:09:29 at Socket.socketErrorListener (node:_http_client:518:5)
2025-02-10 11:09:29 at Socket.emit (node:events:507:28)
2025-02-10 11:09:29 at emitErrorNT (node:internal/streams/destroy:170:8)
2025-02-10 11:09:29 at emitErrorCloseNT (node:internal/streams/destroy:129:3)
2025-02-10 11:09:29 at Axios.request (file:///app/node_modules/axios/lib/core/Axios.js:45:41)
2025-02-10 11:09:29 at processTicksAndRejections (node:internal/process/task_queues:105:5)
2025-02-10 11:09:29 at APIClient.getAll (/app/src/services/apiClient.ts:36:21) {
2025-02-10 11:09:29 code: 'ECONNREFUSED',
2025-02-10 11:09:29 errors: [
2025-02-10 11:09:29 Error: connect ECONNREFUSED ::1:8081
2025-02-10 11:09:29 at createConnectionError (node:net:1675:14)
2025-02-10 11:09:29 at afterConnectMultiple (node:net:1705:16)
2025-02-10 11:09:29 at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
2025-02-10 11:09:29 errno: -111,
2025-02-10 11:09:29 code: 'ECONNREFUSED',
2025-02-10 11:09:29 syscall: 'connect',
2025-02-10 11:09:29 address: '::1',
2025-02-10 11:09:29 port: 8081
2025-02-10 11:09:29 },
2025-02-10 11:09:29 Error: connect ECONNREFUSED 127.0.0.1:8081
2025-02-10 11:09:29 at createConnectionError (node:net:1675:14)
2025-02-10 11:09:29 at afterConnectMultiple (node:net:1705:16)
2025-02-10 11:09:29 at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
2025-02-10 11:09:29 errno: -111,
2025-02-10 11:09:29 code: 'ECONNREFUSED',
2025-02-10 11:09:29 syscall: 'connect',
2025-02-10 11:09:29 address: '127.0.0.1',
2025-02-10 11:09:29 port: 8081
2025-02-10 11:09:29 }
2025-02-10 11:09:29 ],
2025-02-10 11:09:29 config: {
2025-02-10 11:09:29 transitional: {
2025-02-10 11:09:29 silentJSONParsing: true,
2025-02-10 11:09:29 forcedJSONParsing: true,
2025-02-10 11:09:29 clarifyTimeoutError: false
2025-02-10 11:09:29 },
2025-02-10 11:09:29 adapter: [ 'xhr', 'http', 'fetch' ],
2025-02-10 11:09:29 transformRequest: [ [Function: transformRequest] ],
2025-02-10 11:09:29 transformResponse: [ [Function: transformResponse] ],
2025-02-10 11:09:29 timeout: 0,
2025-02-10 11:09:29 xsrfCookieName: 'XSRF-TOKEN',
2025-02-10 11:09:29 xsrfHeaderName: 'X-XSRF-TOKEN',
2025-02-10 11:09:29 maxContentLength: -1,
2025-02-10 11:09:29 maxBodyLength: -1,
2025-02-10 11:09:29 env: { FormData: [Function], Blob: [class Blob] },
2025-02-10 11:09:29 validateStatus: [Function: validateStatus],
2025-02-10 11:09:29 headers: Object [AxiosHeaders] {
2025-02-10 11:09:29 Accept: 'application/json, text/plain, */*',
2025-02-10 11:09:29 'Content-Type': undefined,
2025-02-10 11:09:29 'User-Agent': 'axios/1.7.9',
2025-02-10 11:09:29 'Accept-Encoding': 'gzip, compress, deflate, br'
2025-02-10 11:09:29 },
2025-02-10 11:09:29 baseURL: 'http://localhost:8081/api/v1',
2025-02-10 11:09:29 params: { page: 0, size: 6, sort: 'desc' },
2025-02-10 11:09:29 method: 'get',
2025-02-10 11:09:29 url: '/blogpost/getall',
2025-02-10 11:09:29 data: undefined
2025-02-10 11:09:29 },
2025-02-10 11:09:29 request: <ref *1> Writable {
2025-02-10 11:09:29 _events: {
2025-02-10 11:09:29 close: undefined,
2025-02-10 11:08:43 page context: [object Object]
2025-02-10 11:09:28 5:09:28 PM [vike][request(2)] HTTP request: /blog
2025-02-10 11:09:29 page context: [object Object]
2025-02-10 11:09:29 error: [Function: handleRequestError],
2025-02-10 11:09:29 prefinish: undefined,
2025-02-10 11:09:29 finish: undefined,
2025-02-10 11:09:29 drain: undefined,
2025-02-10 11:09:29 response: [Function: handleResponse],
2025-02-10 11:09:29 socket: [Function: handleRequestSocket]
2025-02-10 11:09:29 },
2025-02-10 11:09:29 _writableState: WritableState {
2025-02-10 11:09:29 highWaterMark: 65536,
2025-02-10 11:09:29 length: 0,
2025-02-10 11:09:29 corked: 0,
2025-02-10 11:09:29 onwrite: [Function: bound onwrite],
2025-02-10 11:09:29 writelen: 0,
2025-02-10 11:09:29 bufferedIndex: 0,
2025-02-10 11:09:29 pendingcb: 0,
2025-02-10 11:09:29 [Symbol(kState)]: 17580812,
2025-02-10 11:09:29 [Symbol(kBufferedValue)]: null
2025-02-10 11:09:29 },
2025-02-10 11:09:29 _maxListeners: undefined,
2025-02-10 11:09:29 _options: {
2025-02-10 11:09:29 maxRedirects: 21,
2025-02-10 11:09:29 maxBodyLength: Infinity,
2025-02-10 11:09:29 protocol: 'http:',
2025-02-10 11:09:29 path: '/api/v1/blogpost/getall?page=0&size=6&sort=desc',
2025-02-10 11:09:29 method: 'GET',
2025-02-10 11:09:29 headers: [Object: null prototype],
2025-02-10 11:09:29 agents: [Object],
2025-02-10 11:09:29 auth: undefined,
2025-02-10 11:09:29 family: undefined,
2025-02-10 11:09:29 beforeRedirect: [Function: dispatchBeforeRedirect],
2025-02-10 11:09:29 beforeRedirects: [Object],
2025-02-10 11:09:29 hostname: 'localhost',
2025-02-10 11:09:29 port: '8081',
2025-02-10 11:09:29 agent: undefined,
2025-02-10 11:09:29 nativeProtocols: [Object],
2025-02-10 11:09:29 pathname: '/api/v1/blogpost/getall',
2025-02-10 11:09:29 search: '?page=0&size=6&sort=desc'
2025-02-10 11:09:29 },
2025-02-10 11:09:29 _ended: true,
2025-02-10 11:09:29 _ending: true,
2025-02-10 11:09:29 _redirectCount: 0,
2025-02-10 11:09:29 _redirects: [],
2025-02-10 11:09:29 _requestBodyLength: 0,
2025-02-10 11:09:29 _requestBodyBuffers: [],
2025-02-10 11:09:29 _eventsCount: 3,
2025-02-10 11:09:29 _onNativeResponse: [Function (anonymous)],
2025-02-10 11:09:29 _currentRequest: ClientRequest {
2025-02-10 11:09:29 _events: [Object: null prototype],
2025-02-10 11:09:29 _eventsCount: 7,
2025-02-10 11:09:29 _maxListeners: undefined,
2025-02-10 11:09:29 outputData: [],
2025-02-10 11:09:29 outputSize: 0,
2025-02-10 11:09:29 writable: true,
2025-02-10 11:09:29 destroyed: true,
2025-02-10 11:09:29 _last: true,
2025-02-10 11:09:29 chunkedEncoding: false,
2025-02-10 11:09:29 shouldKeepAlive: true,
2025-02-10 11:09:29 maxRequestsOnConnectionReached: false,
2025-02-10 11:09:29 _defaultKeepAlive: true,
2025-02-10 11:09:29 useChunkedEncodingByDefault: false,
2025-02-10 11:09:29 sendDate: false,
2025-02-10 11:09:29 _removedConnection: false,
2025-02-10 11:09:29 _removedContLen: false,
2025-02-10 11:09:29 _removedTE: false,
2025-02-10 11:09:29 strictContentLength: false,
2025-02-10 11:09:29 _contentLength: 0,
2025-02-10 11:09:29 _hasBody: true,
2025-02-10 11:09:29 _trailer: '',
2025-02-10 11:09:29 finished: true,
2025-02-10 11:09:29 _headerSent: true,
2025-02-10 11:09:29 _closed: true,
2025-02-10 11:09:29 _header: 'GET /api/v1/blogpost/getall?page=0&size=6&sort=desc HTTP/1.1\r\n' +
2025-02-10 11:09:29 'Accept: application/json, text/plain, */*\r\n' +
2025-02-10 11:09:29 'User-Agent: axios/1.7.9\r\n' +
2025-02-10 11:09:29 'Accept-Encoding: gzip, compress, deflate, br\r\n' +
2025-02-10 11:09:29 'Host: localhost:8081\r\n' +
2025-02-10 11:09:29 'Connection: keep-alive\r\n' +
2025-02-10 11:09:29 '\r\n',
2025-02-10 11:09:29 _keepAliveTimeout: 0,
2025-02-10 11:09:29 _onPendingData: [Function: nop],
2025-02-10 11:09:29 agent: [Agent],
2025-02-10 11:09:29 socketPath: undefined,
2025-02-10 11:09:29 method: 'GET',
2025-02-10 11:09:29 maxHeaderSize: undefined,
2025-02-10 11:09:29 insecureHTTPParser: undefined,
2025-02-10 11:09:29 joinDuplicateHeaders: undefined,
2025-02-10 11:09:29 path: '/api/v1/blogpost/getall?page=0&size=6&sort=desc',
2025-02-10 11:09:29 _ended: false,
2025-02-10 11:09:29 res: null,
2025-02-10 11:09:29 aborted: false,
2025-02-10 11:09:29 timeoutCb: [Function: emitRequestTimeout],
2025-02-10 11:09:29 upgradeOrConnect: false,
2025-02-10 11:09:29 parser: null,
2025-02-10 11:09:29 maxHeadersCount: null,
2025-02-10 11:09:29 reusedSocket: false,
2025-02-10 11:09:29 host: 'localhost',
2025-02-10 11:09:29 protocol: 'http:',
2025-02-10 11:09:29 _redirectable: [Circular *1],
2025-02-10 11:09:29 [Symbol(shapeMode)]: false,
2025-02-10 11:09:29 [Symbol(kCapture)]: false,
2025-02-10 11:09:29 [Symbol(kBytesWritten)]: 0,
2025-02-10 11:09:29 [Symbol(kNeedDrain)]: false,
2025-02-10 11:09:29 [Symbol(corked)]: 0,
2025-02-10 11:09:29 [Symbol(kChunkedBuffer)]: [],
2025-02-10 11:09:29 [Symbol(kChunkedLength)]: 0,
2025-02-10 11:09:29 [Symbol(kSocket)]: [Socket],
2025-02-10 11:09:29 [Symbol(kOutHeaders)]: [Object: null prototype],
2025-02-10 11:09:29 [Symbol(errored)]: null,
2025-02-10 11:09:29 [Symbol(kHighWaterMark)]: 65536,
2025-02-10 11:09:29 [Symbol(kRejectNonStandardBodyWrites)]: false,
2025-02-10 11:09:29 [Symbol(kUniqueHeaders)]: null
2025-02-10 11:09:29 },
2025-02-10 11:09:29 _currentUrl: 'http://localhost:8081/api/v1/blogpost/getall?page=0&size=6&sort=desc',
2025-02-10 11:09:29 [Symbol(shapeMode)]: true,
2025-02-10 11:09:29 [Symbol(kCapture)]: false
2025-02-10 11:09:29 },
2025-02-10 11:09:29 cause: AggregateError:
2025-02-10 11:09:29 at internalConnectMultiple (node:net:1139:18)
2025-02-10 11:09:29 at afterConnectMultiple (node:net:1712:7)
2025-02-10 11:09:29 at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
2025-02-10 11:09:29 code: 'ECONNREFUSED',
2025-02-10 11:09:29 [errors]: [ [Error], [Error] ]
2025-02-10 11:09:29 }
2025-02-10 11:09:29 }
2025-02-10 11:09:29 5:09:29 PM [vike][request(2)] HTTP response /blog 500
apiClient.ts
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
export interface FetchResponse<T> {
content: T[];
}
export interface FetchResponseSingle<T> {
content: T;
}
const blogPostApiUrl = import.meta.env.PUBLIC_ENV__VIKE_BLOG_POST_API_URL;
const mediaApiUrl = import.meta.env.PUBLIC_ENV__VIKE_MEDIA_API_URL;
// Currently using version 1 of all APIs for both blog post and media services
const blogPostApiV1AxiosInstance = axios.create({
baseURL: blogPostApiUrl + '/api/v1'
});
const mediaApiV1AxiosInstance = axios.create({
baseURL: mediaApiUrl + '/v1'
});
class APIClient<T> {
axiosInstance: AxiosInstance;
constructor(axiosInstance: AxiosInstance) {
this.axiosInstance = axiosInstance;
}
get = async (endpoint: string) => {
const res = await this.axiosInstance.get<FetchResponseSingle<T>>(`${endpoint}`);
return res.data;
}
getAll = async (endpoint: string, config: AxiosRequestConfig) => {
const res = await this.axiosInstance.get<FetchResponse<T>>(`${endpoint}`, config);
return res.data;
}
}
export { blogPostApiV1AxiosInstance, mediaApiV1AxiosInstance };
export default APIClient;
Any help is greatly appreciated, thanks!