DEV Community

Ugo Palatucci
Ugo Palatucci

Posted on • Edited on

Secure your Dockerfile for SSG with NextJS and Prisma

Hi, everyone!!
This is my first blog post and I'm so happy!!

The Project

In this post, I would like to talk about an issue I encountered while writing a dockerfile for a Next.js/Prisma application and the solution that I found.

I'm developing a project as a volunteer for a non-profit organization (Istituto Buddista Italiano Soka Gakkai) and we'll hit production soon.
The repository is accessible at this link

The project is a website for consulting antique Buddhist scriptures of the Nichiren Daishonin school and conducting advanced research on them.

Because the organization already has a well-structured infrastructure with databases, ElasticSearch nodes, and other WordPress websites, we've decided to use docker instead of going for the classic Vercel deployment and have everything pretty much in the same location or close to each other. If something changes and we have to migrate our production system, we'll be safe doing so with containers.

The Issue

Thanks to the create-t3-app documentation I was able to set up the Dockerfile pretty quickly, but I've encountered one issue with this setup (the documentation might change after I post this).

In our case, the Next.js application is fetching data from the database at build time (Server Side Generation) to generate pages and cache them. To do that, we need the database URL available during the docker build.

This database URL contains password credentials and cannot be passed to the docker build as an ARG. ARGs can be easily inspected by everyone if the docker image is publicly available, and an attacker could find the database URL, username, and password using just docker history (See documentation about this issue here).

The Solution

Sensitive information has to be handled through docker secrets.

So, the best solution that i could find is this:

RUN --mount=type=secret,id=ENV_WITH_SECRETS,required \
  source /run/secrets/ENV_WITH_SECRETS && \
  DATABASE_URL=$DATABASE_URL yarn build
Enter fullscreen mode Exit fullscreen mode

With this command, docker mounts the environment file with secrets just for this line. The variable is loaded from that file and can be accessed in the code using process.env by Next.js and Prisma. Prisma by default use the .env file to load the database url, but in our case we have to modify this behavior with the follow line:

const prisma = new PrismaClient({
    datasources: {
      db: {
        url: process.env.DATABASE_URL,
      },
    },
  })
Enter fullscreen mode Exit fullscreen mode

With my current version of Prisma, the DATABASE_URL variable even if it's in the secret file, gets removed just at the start of yarn build from the process.env, so a workaround is to run DATABASE_URL=$DATABASE_URL yarn build to enforce the variable in prisma.

The build command looks something like this:

docker build . --secret id=ENV_WITH_SECRETS,src=./.env -t nextjs-prisma-application
Enter fullscreen mode Exit fullscreen mode

The id (ENV_WITH_SECRETS in this example) is absolutely arbitrary.

The secret file contain the database url with credentials that we need at build time.

It is even easier to use secrets with Docker Compose (documentation).

The syntax depends on your docker compose version. This is the syntax that i used for the 2.18.1 version:

services:
  app:
    platform: "linux/amd64"
    build:
      context: .
      dockerfile: Dockerfile
      secrets:
        - ENV_WITH_SECRETS
    working_dir: /app
    ports:
      - "3000:3000"
    image: nextjs-app
    env_file:
      - .env
secrets:
  ENV_WITH_SECRETS:
    file: .env
Enter fullscreen mode Exit fullscreen mode

I bet you don't build every docker image manually yourself, though, do you?

Here is how docker/build-push-action handles secrets:

- name: Build and push
  id: docker_build
  uses: docker/build-push-action@v4
  with:
    push: true
    tags: tag
    secrets: |
      "ENV_WITH_SECRETS=${{ secrets.ENV_WITH_SECRETS }}"
Enter fullscreen mode Exit fullscreen mode

The use of " is essential here to assign a multi-line secret file in this way (documentation).

In this case, ENV_WITH_SECRETS is the docker secret id used previously.

At this point, on Github repo settings you can add a secret with the same ENV_WITH_SECRETS name and copy-paste the .env file containing multiple secrets.

For example

ELASTIC_SEARCH_PASSWORD=*****
DATABASE_URL=""mysql://username:password@host:post/db""
Enter fullscreen mode Exit fullscreen mode

Note: on github you have to escape double quotes character (") with another double quotes character.

To be able to access the database at runtime, (for revalidating the pages on demand or automatically), we can add an environment variable called DATABASE_URL just before the CMD.

ENV DATABASE_URL ''
CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

Environment variables are only available at runtime and not in the build step, which makes it safe to use them for sensitive information.

Conclusion

It was so fun to dig into this issue, learn more about docker secrets, and find a solution.
I also posted a pull request against create-t3-app repo and they helped me find a suitable solution for many more environments.
It was a great opportunity to write a post!!
Thanks!
I'm looking forward to your suggestions and comments!!

Top comments (0)