DEV Community

Cover image for Building a Local Development Environment: Running a Next.js Full-Stack App with PostgreSQL and Minio S3 Using Docker
Alex
Alex

Posted on • Edited on • Originally published at blog.alexefimenko.com

Building a Local Development Environment: Running a Next.js Full-Stack App with PostgreSQL and Minio S3 Using Docker

Table of Contents

Introduction

As a developer working on a full-stack application, you need to have a local development environment that is as close as possible to the production environment. It will allow you to test and debug your application locally before deploying it to production.

Almost every full-stack application needs a database and a file storage so let's build a basic full-stack application that can save and retrieve data from a database and upload and download files from a file storage.

You can run your own PostgreSQL and Minio S3 server locally, or even use a cloud service like AWS RDS and S3. But it will take some time to set up and configure. Using docker-compose will make it super easy to set up a local development environment for your full-stack application. After you test it locally, all you need to switch to the production environment is to change the environment variables.

Additionally, you can test your application end-to-end (for example, using Cypress) in a local environment that is as close as possible to the production environment. Having pre-configured docker-compose file will make it easy to set up a CI/CD pipeline like GitHub Actions or GitLab CI.

Once you have a docker-compose file, you and your team can use it to set up the same local development environment on any machine in a few minutes with just one command. Overall, it will save you a lot of time and make your life easier.

In this article, we will look at how to build a local development environment for a full-stack Next.js application with Prisma ORM connected to PostgreSQL as a database and Minio S3 as a file storage using Docker-Compose.

You can find the full source code for this tutorial on GitHub

When you are ready to deploy your application to production, you can use any type of database:

  • Supabase (I love this one)
  • Vercel Postgres
  • AWS RDS
  • Google Cloud SQL
  • Azure Database for PostgreSQL
  • Heroku Postgres
  • DigitalOcean Managed Databases
  • ScaleGrid
  • ...

The same goes for the file storage, you need one that is compatible with the S3 protocol:

  • AWS S3
  • Google Cloud Storage (I tested it, it works)
  • Wasabi (One of the cheapest options)
  • Backblaze B2
  • DigitalOcean Spaces
  • ...

Prerequisites

To follow this tutorial, you need to have Docker and Docker-Compose installed on your machine. You can find the instructions on how to install Docker and Docker-Compose on the official Docker website.

When I firstly faced the task of setting up a local development environment for Next.js, Prisma and PostgreSQL, I tried to use T3 Docker tutorial but it didn't work for me. However, I use it as a starting point for this tutorial.

Building a local development environment

1. Create a Next.js application

Let's start by creating a Next.js application. We will use the T3 stack (TypeScript, TailwindCSS, and Prisma ORM) for this tutorial to skip installing and configuring all the dependencies which is out of the scope of this article. You can find more information about the T3 stack.

Run the following command to create a new Next.js:



npm create t3-app@latest


Enter fullscreen mode Exit fullscreen mode

After you run the command, you will be asked several questions:



   ___ ___ ___   __ _____ ___   _____ ____    __   ___ ___
  / __| _ \ __| /  \_   _| __| |_   _|__ /   /  \ | _ \ _ \
 | (__|   / _| / /\ \| | | _|    | |  |_ \  / /\ \|  _/  _/
  \___|_|_\___|_/‾‾\_\_| |___|   |_| |___/ /_/‾‾\_\_| |_|

◇  What will your project be called?
│  local-nextjs-postgres-s3
│
◇  Will you be using TypeScript or JavaScript?
│  TypeScript
│
◇  Will you be using Tailwind CSS for styling?
│  Yes
│
◇  Would you like to use tRPC?
│  No
│
◇  What authentication provider would you like to use?
│  None
│
◇  What database ORM would you like to use?
│  Prisma
│
◇   EXPERIMENTAL  Would you like to use Next.js App Router?
│  No
│
◇  Should we initialize a Git repository and stage the changes?
│  Yes
│
◇  Should we run 'npm install' for you?
│  Yes
│
◇  What import alias would you like to use?
│  ~/


Enter fullscreen mode Exit fullscreen mode

2. Configure Next.js to work with Docker

According to the Next.js documentation

Next.js can automatically create a standalone folder that copies only the necessary files for a production deployment including select files in node_modules.

To reduce image size we need to add output: "standalone" in the next.config.js file.
The next.config.js file should look like this:



const config = {
  reactStrictMode: true,
  output: "standalone",
  // ...
};


Enter fullscreen mode Exit fullscreen mode

3. Add .dockerignore file

I prefer having a separate folder for the docker files, so I created a folder called compose in the root of the project. We need to add a .dockerignore file to this folder to exclude unnecessary files from the Docker image. The .dockerignore file should look like this:



.env
Dockerfile
.dockerignore
.next
.git
.gitignore
node_modules
npm-debug.log
README.md


Enter fullscreen mode Exit fullscreen mode

4. Configure Prisma to work with Docker

In the prisma/schema.prisma file, we need to

  • Change the provider from sqlite to postgresql:
  • Add binaryTargets to the generator block. It will allow us to use the Prisma CLI inside the Docker container. You need to your binaryTargets specific to your OS and architecture. For example, for M1 Mac, I use "linux-musl-arm64-openssl-3.0.x". To support other OS and architectures, you need to add them to the binaryTargets array. More about binaryTargets in the Prisma documentation.

I want to use the same schema.prisma file for local development on machines with different OS and architectures and also for CI/CD pipelines on GitHub Actions. So in my case, the prisma/schema.prisma file looks like this:



generator client {
    provider      = "prisma-client-js"
    binaryTargets = ["native", "linux-musl-arm64-openssl-3.0.x", "linux-musl-openssl-3.0.x", "rhel-openssl-1.0.x"]
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}


Enter fullscreen mode Exit fullscreen mode

5. Create a Dockerfile for the Next.js application

Inside the compose folder, create a file called web.Dockerfile with the following content:



FROM node:18-alpine

RUN mkdir app
COPY ../prisma  ./app
COPY ../package.json ../package-lock.json ./app
WORKDIR /app

RUN npm ci

CMD ["npm", "run", "dev"]


Enter fullscreen mode Exit fullscreen mode

We will use the node:18-alpine image as a base image. It is a lightweight image that contains Node.js 18 and npm. We will copy the /prisma folder, package.json and package-lock.json files to the /app folder and run npm ci to install the dependencies.

Using 'clean install' (npm ci) instead of 'install' (npm i) is a good practice for Docker images. It will ensure that the dependencies are installed from the package-lock.json file and not from the node_modules cache. This is faster than 'install', which is especially important for CI/CD pipelines where you want to keep the build time as short as possible.

6. Create a docker compose file

Docker compose file is used to define and run multi-container Docker applications with a single command docker-compose up.

Here we will not go into details about docker-compose files. In general our docker-compose file creates 3 services: web (Next.js application built with our Dockerfile), db (PostgreSQL database), and minio (Minio S3 file storage). Remember to add volumes for the database and file storage services. Otherwise, the data will be lost when you stop the containers.

It is generally not recommended to store environment variables in the docker-compose file. However, in this particular scenario, for educational purposes and given that we are exclusively using it for local development and testing, it looks acceptable.
If you do not want to store secrets in the docker-compose file, you should use a .env file and use ${VARIABLE_NAME} syntax to reference the variables. More about environment variables in docker-compose files Docker compose file reference.

Inside the compose folder, create a file called docker-compose.yml with the following content:



version: "3.9"
name: nextjs-postgres-s3minio
services:
  web:
    container_name: nextjs
    build:
      context: ../
      dockerfile: compose/web.Dockerfile
      args:
        NEXT_PUBLIC_CLIENTVAR: "clientvar"
    ports:
      - 3000:3000
    volumes:
      - ../:/app
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp-db?schema=public
      - S3_ENDPOINT=minio
      - S3_PORT=9000
      - S3_ACCESS_KEY=minio
      - S3_SECRET_KEY=miniosecret
      - S3_BUCKET_NAME=s3bucket
    depends_on:
      - db
      - minio
  db:
    image: postgres:15.3
    container_name: postgres
    ports:
      - 5432:5432
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: myapp-db
    volumes:
      - postgres-data:/var/lib/postgresql/data
    restart: unless-stopped
  minio:
    container_name: s3minio
    image: bitnami/minio:latest
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - minio_storage:/data
volumes:
  postgres-data:
  minio_storage:


Enter fullscreen mode Exit fullscreen mode

Run the application

Depending on where you have your docker-compose file located and what options you want to use,

you need to run the following command:



# If you have docker files in the root of the project
docker-compose up

# In our case, we have dockerfile and docker-compose file in the `compose` folder, so we need to run:
docker-compose -f compose/docker-compose.yml up

# --- Optional ---
# For running the application with secrets ${VARIABLE_NAME} stored in the .env file, we would need to run:
docker-compose -f compose/docker-compose.yml --env-file .env up

# If you want to run the application in the background, you can use the -d flag:
docker-compose -f compose/docker-compose.yml up -d


Enter fullscreen mode Exit fullscreen mode

It will run build the Next.js application and run it on port 3000. It will also download the PostgreSQL and Minio S3 docker images and run them on ports 5432 and 9000 respectively.

After the application is built and images are downloaded, you will see the following output:

Screenshot of docker desktop application running nextjs, postgres and minio s3 services

You can access the application at http://localhost:3000, PostgreSQL database at http://localhost:5432 (login: postgres, password: postgres), and Minio S3 at http://localhost:9000 (login: minio, password: miniosecret).
Credentials for the database and file storage are stored in the docker-compose file. You can change them if you want.

Conclusion

In this article, we looked at how to build a local development environment for a full-stack Next.js application with Prisma ORM connected to PostgreSQL as a database and Minio S3 as a file storage using Docker-Compose.

References and further reading:


I hope you found this article useful. If you have any questions or comments, please let me know in the comments below.

In the the next article, we add a file upload functionality and database integration to our application.


I'm always open to making new connections! Feel free to connect with me on LinkedIn

Top comments (16)

Collapse
 
proteusiq profile image
Prayson Wilfred Daniel • Edited

Nice 👌🏾. We could also pass secrets environments variables not plainly with something like …?


version: "3.9"
services:
  web:
    build:
      context: ../
      dockerfile: compose/web.Dockerfile
      args:
        NEXT_PUBLIC_CLIENTVAR: "clientvar"
    ports:
      - 3000:3000
    volumes:
      - ../:/app
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp-db?schema=public
      - S3_ENDPOINT=minio
      - S3_PORT=9000
      - S3_ACCESS_KEY=minio
      - S3_SECRET_KEY_FILE=/run/secrets/s3_secret_key
      - S3_BUCKET_NAME=s3bucket
    secrets:
      - s3_secret_key
    depends_on:
      - db
      - minio
  db:
    image: postgres:15.3
    container_name: postgres
    ports:
      - 5432:5432
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: myapp-db
    volumes:
      - postgres-data:/var/lib/postgresql/data
    secrets:
      - db_password
    restart: unless-stopped
  minio:
    image: bitnami/minio:latest
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - minio_storage:/data
    secrets:
      - s3_secret_key

secrets:
  db_password:
    file: ./db_password.txt  # Path to the file containing the DB password
  s3_secret_key:
    file: ./s3_secret_key.txt  # Path to the file containing the S3 secret key

volumes:
  postgres-data:
  minio_storage:
Enter fullscreen mode Exit fullscreen mode

Nice 😊 work!

Collapse
 
alexefimenko profile image
Alex • Edited

Thanks!

Right, you should never ever add real API keys or secrets in config files that are stored on GitHub. If you want to use other secrets, like GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET, you should store them in .env file and add it to .gitignore. Then you can use them in docker-compose.yml file like this:

    environment:
      - ...
      - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
      - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
Enter fullscreen mode Exit fullscreen mode

And run docker-compose like this:

docker-compose -f compose/docker-compose.yml --env-file .env up
Enter fullscreen mode Exit fullscreen mode
Collapse
 
proteusiq profile image
Prayson Wilfred Daniel • Edited

Totally. These files are not to be committed 🤭. By docker compose secrets, the sensitive variables are not stored as plain texts.

Collapse
 
dpark profile image
Dan

Thanks for this post!

Whoa t3.app is neat, makes the set up super simple. Curious for thoughts on something I'm working on which is probably overbuilt for this type of tutorial: github.com/kurtosis-tech/kurtosis

Collapse
 
alexefimenko profile image
Alex

Wow, Kurtosis is a great project!
I definitely need some time to learn more about it, thanks for sharing!

Collapse
 
dpark profile image
Dan

For sure! Please feel free to hmu if you have any feedback or questions :)

Collapse
 
nyangweso profile image
Rodgers Nyangweso

good good work

Collapse
 
twdor profile image
Adrian Ghitescu

Niceeee 😊 !!!

Collapse
 
makershihab profile image
Maker Shihab

Nice 🌺 work!

Collapse
 
random_ti profile image
Random

Handy Tutorial Thanks for sharing this 🤝

Collapse
 
saruf_ratul profile image
Saruf Ratul

Good one

Collapse
 
dev_sd profile image
Soumajit Das

Really awesome post.

Collapse
 
dipayansukul profile image
Dipayan Sukul

Very nicely written and described

Collapse
 
john90 profile image
John bobby

Awesome 😎

Some comments have been hidden by the post's author - find out more