Usability - Productivity - Business - The web - Singapore & Twins

Deploy private npm packages into private containers using github actions

GitHub Actions are rapidly becoming my favorite CI environment. Their marketplace has an action for everything. Sometimes it takes a little trial and error before things work smoothly. This is one of that stories.

Authentication is everything

Imagine the following scenario: you have developed a set of private TypeScript (or JavaScript) packages and have successfully deployed them to the private GitHub npm registry under the name @myfamousorg/coolpackage - where myfamousorg must match the repository owner (org or individual).

Now you want to use them in your application. That application shall be packed in a Container and made available in GitHub's private registry. All that automated using GitHub Actions.

You will need a PAT (or two)

In GitHub, head to the Personal access tokens / Tokens (classic) section of your developer settings in profile. You need to create tokens that allow you to handle packages.

GitHub Tokens

There are two places where you want to enter that token:

  • In https://github.com/[your-org]/[your-repo]/settings/secrets/actions create a key GIT_NPM_PACKAGES and copy your PAT there. You can pick any name, you will need it in the GitHub action later
  • In ~/.npmrc, your global settings for npm in your home directory. Don't put the info in the .npmrc in your git project.
prefix=/home/[your username]/.npm-packages
//npm.pkg.github.com/:_authToken=[here goes the token]

The prefix property allows you to run `npm install -g [package] without admin access.

Next stop:


The Dockerfile uses a two container approach: one to build the application and one to deliver the runtime with no ts files or development dependencies. Note that we copy a github.npmrc file into .npmrc

# build container
FROM node:18-alpine

# Create app directory
COPY package*.json ./
COPY tsconfig.json ./
# GitHub Action specific npmrc
COPY github.npmrc ./.npmrc
COPY src ./src
COPY test ./test
# The PAT you created
RUN npm run build

# Actual runtime container
FROM node:18-alpine

# Create app directory
WORKDIR /usr/src/app
COPY package*.json ./
COPY github.npmrc ./.npmrc

# Only source from build container
COPY  --from=0 /usr/dist .

# Labels
LABEL org.opencontainers.image.source="https://github.com/myfamousorg/shinyapp"

# Run it
CMD ["node","server.js"]

project github.npmrc


GitHub Action

Create .gihub/workflows/create-container.yml (or whatever name you deem fit) with content like this:

name: Build docker container

      - main

    name: Build and store backend docker
    runs-on: ubuntu-latest
      - name: Checkout Backend
        uses: actions/checkout@v3

      # Setup hardware emulator using QEMU
      # to get image for both Intel and ARM
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2

      # Build environment thst caches layers
      - name: Setup Docker Buildx
        id: buildx
        uses: docker/setup-buildx-action@v2

      - name: Cache Docker layers
        uses: actions/cache@v3
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-

      # Login to docker hub to get node:18-alpine
      - name: Docker Hub login
        uses: docker/login-action@v2
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      # Login to github to deposit the result GITHUB_TOKEN works here
      - name: GitHub container Registry login
        uses: docker/login-action@v2
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # The main action
      - name: Build and Push
        id: docker_build
        uses: docker/build-push-action@v4
          context: ./
          file: ./Dockerfile
          builder: ${{ steps.buildx.outputs.name }}
          push: true
          # this triggers platform builds, requires the earlier QEMU setuo
          platforms: linux/amd64, linux/arm64
          tags: |
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache
          # This is where you need the PAT since GITHUB_TOKEN won't do
          build-args: GITHUB_TOKEN=${{ secrets.GIT_NPM_PACKAGES }}


  • The default GITHUB_TOKEN doesn't work, you need a pat
  • The dual container approach makes your runtime substantially smaller (depending on how much dev dependencies can be omitted)
  • Alpine Linux seems to be the most efficient balance between ease-of-use and size
  • If you also want to run on Arm (macOS M1/M2), use the QEMU emulator

As usual YMMV

Posted by on 16 July 2023 | Comments (1) | categories: GitHub JavaScript WebDevelopment


  1. posted by Matt H on Wednesday 30 August 2023 AD:

    Hi, I'm currently doing something similar. I'm wondering why you say the GITHUB_TOKEN won't suffice? In my case we have some npm packages in npm.pkg.github.com scoped to the Org. We've added the repo where the workflow is running in the package settings, so I think the GITHUB_TOKEN should work? I'll find out soon enough I guess.