Search code examples
node.jsdockernestjspuppeteer

Puppeteer / docker (Target.setAutoAttach): Target is closed


There is a project in nestjs

build is produced in the docker

Dockerfile

###################
# BUILD FOR LOCAL DEVELOPMENT
###################

FROM --platform=linux/amd64 node:18-alpine As development

# Create app directory
WORKDIR /usr/src/app

# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY --chown=node:node package*.json ./

# Install app dependencies using the `npm ci` command instead of `npm install`
ENV PUPPETEER_SKIP_DOWNLOAD=true
RUN npm ci

# Bundle app source
COPY --chown=node:node . .

# Use the node user from the image (instead of the root user)
USER node

###################
# BUILD FOR PRODUCTION
###################

FROM --platform=linux/amd64 node:18-alpine As build

WORKDIR /usr/src/app

COPY --chown=node:node package*.json ./

# In order to run `npm run build` we need access to the Nest CLI which is a dev dependency. In the previous development stage we ran `npm ci` which installed all dependencies, so we can copy over the node_modules directory from the development image
COPY --chown=node:node --from=development /usr/src/app/node_modules ./node_modules

COPY --chown=node:node . .

# Run the build command which creates the production bundle
RUN npm run build

# Set NODE_ENV environment variable
ENV NODE_ENV production

# Running `npm ci` removes the existing node_modules directory and passing in --only=production ensures that only the production dependencies are installed. This ensures that the node_modules directory is as optimized as possible
ENV PUPPETEER_SKIP_DOWNLOAD=true
RUN npm ci --only=production && npm cache clean --force

USER node

###################
# PRODUCTION
###################

FROM --platform=linux/amd64 node:18-alpine As production

# Copy the bundled code from the build stage to the production image
COPY --chown=node:node --from=build /usr/src/app/node_modules ./node_modules
COPY --chown=node:node --from=build /usr/src/app/dist ./dist

RUN apk add chromium

EXPOSE 3010
ENV PORT 3010

ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

# Start the server using the production build
CMD [ "node", "dist/main.js" ]

An error appears in the console when starting the container

[Nest]  ERROR [ExceptionHandler] Protocol error (Target.setAutoAttach): Target closed.
 ProtocolError: Protocol error (Target.setAutoAttach): Target closed.
    at /node_modules/puppeteer-core/lib/cjs/puppeteer/common/Connection.js:104:24
    at new Promise (<anonymous>)
    at Connection.send (/node_modules/puppeteer-core/lib/cjs/puppeteer/common/Connection.js:100:16)
    at ChromeTargetManager.initialize (/node_modules/puppeteer-core/lib/cjs/puppeteer/common/ChromeTargetManager.js:247:82)
    at CDPBrowser._attach (/node_modules/puppeteer-core/lib/cjs/puppeteer/common/Browser.js:156:76)
    at CDPBrowser._create (/node_modules/puppeteer-core/lib/cjs/puppeteer/common/Browser.js:49:23)
    at ChromeLauncher.launch (/node_modules/puppeteer-core/lib/cjs/puppeteer/node/ChromeLauncher.js:130:53)
    at async InstanceWrapper.useFactory [as metatype] (/node_modules/@noahqte/nest-puppeteer/dist/puppeteer-core.module.js:38:24)
    at async Injector.instantiateClass (/node_modules/@nestjs/core/injector/injector.js:355:37)
    at async callback (/node_modules/@nestjs/core/injector/injector.js:56:34)

I've been thinking about solving this error for quite some time now. Tried different methods to solve it

Puppeteer is connected via the nestjs-puppeteer library

Service

constructor(
    private appService: AppService,
    @InjectBrowser() private readonly browser: Browser,
  ) {}
async onSendForm1(data, @Ctx() ctx?: Context) {
    const page = await this.browser.newPage()
    await page.setRequestInterception(true)
    await page.once('request', (interceptedRequest) => {
      interceptedRequest.continue({ method: 'POST', postData: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } })
    })
    await page.goto('http://localhost:3010', { waitUntil: 'networkidle2' })
    const fileName = Guid.newGuid().toString()
    await page.pdf({
      path: `${this.appService.pathFile}${fileName}.pdf`,
      scale: 0.9,
      format: 'A4',
      landscape: false,
      pageRanges: '1,3',
    })
    await page.close()
    await this.browser.close()
}

app.module

@Module({
  imports: [
    PuppeteerModule.forRoot({ pipe: true, isGlobal: true }),
  ],
  controllers: [AppController],
  providers: [AppService],
  exports: [AppService],
})

Chromium in a container works if you specify env ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

Can you help with this


Solution

  • I found a solution to my problem

    Step 1: Delete nestjs-puppeteer

    npm uninstall nestjs-puppeteer

    Step 2: I had to change the dockerfile

    Dockerfile

    FROM node:18-alpine As development
    
    # Create app directory
    WORKDIR /usr/src/app
    
    ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
    
    # Copy application dependency manifests to the container image.
    # A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
    # Copying this first prevents re-running npm install on every code change.
    COPY --chown=node:node package*.json ./
    
    # Install app dependencies using the `npm ci` command instead of `npm install`
    RUN npm install
    
    # Bundle app source
    COPY --chown=node:node . .
    
    # Use the node user from the image (instead of the root user)
    USER node
    
    ###################
    # BUILD FOR PRODUCTION
    ###################
    
    FROM node:18-alpine As build
    
    WORKDIR /usr/src/app
    
    ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
    
    COPY --chown=node:node package*.json ./
    
    # In order to run `npm run build` we need access to the Nest CLI which is a dev dependency. In the previous development stage we ran `npm ci` which installed all dependencies, so we can copy over the node_modules directory from the development image
    COPY --chown=node:node --from=development /usr/src/app/node_modules ./node_modules
    
    COPY --chown=node:node . .
    
    # Run the build command which creates the production bundle
    RUN npm run build
    
    # Set NODE_ENV environment variable
    ENV NODE_ENV production
    
    # Running `npm ci` removes the existing node_modules directory and passing in --only=production ensures that only the production dependencies are installed. This ensures that the node_modules directory is as optimized as possible
    RUN npm install && npm cache clean --force
    
    USER node
    
    ###################
    # PRODUCTION
    ###################
    
    FROM node:18-alpine As production
    
    ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
    
    EXPOSE 3010
    
    RUN apk add --no-cache \
          chromium \
          nss \
          freetype \
          harfbuzz \
          ca-certificates \
          ttf-freefont \
          nano \
          sudo \
          bash
    
    # Copy the bundled code from the build stage to the production image
    COPY --chown=node:node --from=build /usr/src/app/dist ./dist
    COPY --chown=node:node --from=build /usr/src/app/node_modules ./dist/node_modules
    
    # Start the server using the production build
    USER node
    CMD [ "node", "dist/main.js" ]
    

    Step 3: change init browser

    service.ts

    import puppeteer from 'puppeteer'
    
    
    const browser = await puppeteer.launch({
          headless: true,
          args: ['--disable-gpu', '--disable-dev-shm-usage', '--disable-setuid-sandbox', '--no-sandbox'],
        })
        const page = await browser.newPage()
        await page.setRequestInterception(true)
        page.once('request', (interceptedRequest) => {
          interceptedRequest.continue({
            method: 'POST',
            postData: JSON.stringify(data),
            headers: { 'Content-Type': 'application/json' },
          })
        })
        await page.goto('http://localhost:3010', { waitUntil: 'networkidle2' })
        await page.waitForSelector('body')
        const fileName = Guid.newGuid().toString()
        await page.pdf({
          path: `${this.appService.pathFile}${fileName}.pdf`,
          scale: 0.9,
          format: 'A4',
          landscape: false,
          pageRanges: '1,2',
        })
        await browser.close()
    

    The error occurs if the container is run as a root user