Step-by-Step nextpalestine Setup: Complete Overview

Step-by-Step nextpalestine Setup: Complete Overview

·

8 min read

Looking to build a stunning blogging platform with ease? Discover nextpalestine, our open-source web application! It offers an effortless blogging experience with a powerful editor, user management, and more. Available on GitHub! This guide will walk you through the setup of nextpalestine.

The root application folder:

nextpalestine is build on a mono-repo structure:

.
├── frontend
│   └── package.json
├── backend
│   └── package.json
└── package.json

The root package.json:

{
  "name": "nextpalestine-monorepo",
  "version": "0.1.0",
  "private": true,
  "license": "GPL",
  "description": "Blogging platform",
  "repository": {
    "type": "git",
    "url": "<https://github.com/adelpro/nextpalestine.git>"
  },
  "author": "Adel Benyahia <adelpro@gmail.com>",
  "authors": ["Adel Benyahia <adelpro@gmail.com>"],
  "engines": {
    "node": ">=18"
  },
  "devDependencies": {
    "husky": "^8.0.0",
    "npm-run-all": "^4.1.5"
  },
  "scripts": {
    "backend": "npm run start:dev -w backend",
    "frontend": "npm run dev -w frontend",
    "frontend:prod": "npm run build -w frontend && npm run start:prod -w frontend",
    "backend:prod": "npm run build -w backend && npm run start:prod -w backend",
    "dev": "npm-run-all --parallel backend frontend",
    "start": "npm-run-all --parallel backend:prod frontend:prod",
    "docker:build": "docker compose down && docker compose up -d --build",
    "prepare": "husky install"
  },
  "workspaces": ["backend", "frontend"],
  "lint-staged": {
    "**/*.{js,jsx,ts,tsx}": ["npx prettier --write", "npx eslint --fix"]
  }
}

In this package.json, we are using npm-run-all package to run multiple commands in concurrency, for example the command start will start two command in parallel backend and frontend .

Husky package is used to run pre-commit hooks, take a look at the .husky folder in the root folder.

.
├── .husky
│   └── _
│   └── pre-commit
├── frontend
│   └── package.json
├── backend
│   └── package.json
└── package.json

The ./husky folder:

We are using husky to lint fix our code before committing it, and to make this process faster we are using a second package: lint-staged to only lint the staged code (newly added or modified code).


#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# Exit immediately if any command exits with a non-zero status.
set -e

echo 'Linting project before committing'
npx lint-staged

The ./github folder:

In this folder we have added a custom action called ./workflows/scanning_git_secrets.yml

name: gitleaks
on:
  pull_request:
  push:
  workflow_dispatch:
  schedule:
    - cron: "0 4 * * *" # run once a day at 4 AM
jobs:
  scan:
    name: gitleaks
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

This GitHub action will scan our commits for shared secrets (.env files for example), if any secret is detected, the action will fail and an alert will be sent to you from GitHub.

The compose.yaml

This file is used to build a self-hosted dockerized application that we can deploy to any system that run docker.

To properly run deploy our application to docker we have to:

1- Clone our repo: git clone [https://github.com/adelpro/nextpalestine.git](https://github.com/adelpro/nextpalestine.git)](github.com/adelpro/nextpalestine.git)

2- Create an .env.production file in the frontend folder aligned to the .env.example (in the same folder).

3- Create an .env.production file in the backend folder aligned to the .env.example (in the same folder).

4- Run: docker compose up -d

We will now explain the compose.yaml file

services:
  # Fontend: NextJs
  frontend:
    env_file:
      - ./frontend/.env.production
    container_name: nextpalestine-frontend
    image: nextpalestine-frontend
    build:
      context: ./frontend
      dockerfile: Dockerfile
      args:
        - DOCKER_BUILDKIT=1
    ports:
      - 3540:3540
    restart: unless-stopped
    depends_on:
      backend:
        condition: service_healthy
    volumes:
      - /app/node_modules
      # For live reload if the source or env changes
      - ./frontend/src:/app/src

    networks:
      - app-network
  # Backend: NestJS
  backend:
    container_name: nextpalestine-backend
    image: nextpalestine-backend
    env_file:
      - ./backend/.env.production
    build:
      context: ./backend
      dockerfile: Dockerfile
      args:
        - DOCKER_BUILDKIT=1
    ports:
      - 3500:3500
    restart: unless-stopped
    depends_on:
      mongodb:
        condition: service_healthy
    volumes:
      - backend_v_logs:/app/logs
      - backend_v_public:/app/public
      - /app/node_modules
      # For live reload if the source or env changes
      - ./backend/src:/app/src
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://backend:3500/health || exit 1"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 20s
    networks:
      - app-network

  # Database: Mongodb
  mongodb:
    container_name: mongodb
    image: mongo:latest
    restart: unless-stopped
    ports:
      - 27018:27017
    env_file:
      - ./backend/.env.production
    networks:
      - app-network
    volumes:
      - mongodb_data:/data/db
      - /etc/timezone:/etc/timezone:ro
      #- type: bind
      #    source: ./mongo-entrypoint
      #    target: /docker-entrypoint-initdb.d/
    healthcheck:
      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 20s

  # Database UI: Mongo Express
  mongo-express:
    image: mongo-express:1.0.2-20-alpine3.19
    container_name: mongo-express
    restart: always
    ports:
      - 8081:8081
    env_file:
      - ./backend/.env.production
    depends_on:
      - mongodb
    networks:
      - app-network
volumes:
  backend_v_logs:
    name: nextpalestine_v_backend_logs
  backend_v_public:
    name: nextpalestine_v_backend_public
  mongodb_data:
    name: nextpalestine_v_mongodb_data
    driver: local
networks:
  app-network:
    driver: bridge

As you can see, we have forth images

1- mongo-express:

mongo-express is a web-based MongoDB admin interface

2- mongo:

The official mongo docker image, where we have added a health check, we will need at later in the backend image.

    healthcheck:
      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 20s

We have also loaded the .env file from the backend folder

    env_file:
      - ./backend/.env.production

We will need these .env variables:

MONGO_INITDB_ROOT_USERNAME=root
MONGO_INITDB_ROOT_PASSWORD=password
MONGO_DATABASE_NAME=database

And we are creating a persisted (named) volume to persist data between different builds, the second line is a hack to sync the time zone between the docker image and the host that runs it, it ensures that the timestamps in your database match the host system's timezone.

    volumes:
      - mongodb_data:/data/db
      - /etc/timezone:/etc/timezone:ro

We have also changed the exposed port to(27018), 27018:27017 this will prevent any conflict with any mongodb database installed in the host system with the default port (27017)

3- backend (Nest.js)

  backend:
    container_name: nextpalestine-backend
    image: nextpalestine-backend
    env_file:
      - ./backend/.env.production
    build:
      context: ./backend
      dockerfile: Dockerfile
      args:
        - DOCKER_BUILDKIT=1
    ports:
      - 3500:3500
    restart: unless-stopped
    depends_on:
      mongodb:
        condition: service_healthy
    volumes:
      - backend_v_logs:/app/logs
      - backend_v_public:/app/public
      - /app/node_modules
      # For live reload if the source or env changes
      - ./backend/src:/app/src
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://backend:3500/health || exit 1"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 20s
    networks:
      - app-network

This image is build on ./backend/Dockerfile using DOCKER_BUILDER=1 argument, this will enhance the build speed and caching.

  • Env variable are load from ./backend/.env.production.

  • It depends on mongodb image, the backend image will not start until the mongo image is fully started and healthy.

  • The backend image has it’s own health check that we will use in the frontend (Next.js) image.

  • We are persisting logs and public folders using named volumes.

  • This volume: /app/node_modules is used to persist node_module.

  • This volume: ./backend/src:/app/src to hot reload the image when ever the code is changed.

  • The port 3500:3500 is used for the backend.

We will now explain the Dockerfile of the backend (in the backend folder)

ARG NODE=node:21-alpine3.19

# Stage 1: builder
FROM ${NODE} AS builder

# Combine commands to reduce layers
RUN apk add --no-cache libc6-compat \
    && apk add --no-cache curl \
    && addgroup --system --gid 1001 nodejs \
    && adduser --system --uid 1001 nestjs

WORKDIR /app

COPY --chown=nestjs:nodejs package*.json ./

RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn \
    yarn install --frozen-lockfile

COPY --chown=nestjs:nodejs . .

ENV NODE_ENV production

# Generate the production build. The build script runs "nest build" to compile the application.
RUN yarn build

# Install only the production dependencies and clean cache to optimize image size.
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn \
    yarn install --production --frozen-lockfile && yarn cache clean

USER nestjs

# Stage 2: runner
FROM ${NODE} AS runner

RUN apk add --no-cache libc6-compat \
    && apk add --no-cache curl \
    && addgroup --system --gid 1001 nodejs \
    && adduser --system --uid 1001 nestjs

WORKDIR /app

# Set to production environment
ENV NODE_ENV production

# Copy only the necessary files
COPY --chown=nestjs:nodejs --from=builder /app/dist ./dist
COPY --chown=nestjs:nodejs --from=builder /app/logs ./logs
COPY --chown=nestjs:nodejs --from=builder /app/public ./public
COPY --chown=nestjs:nodejs --from=builder /app/node_modules ./node_modules
COPY --chown=nestjs:nodejs --from=builder /app/package*.json ./

# Set Docker as non-root user
USER nestjs

EXPOSE 3500

ENV HOSTNAME "0.0.0.0"

CMD ["node", "dist/main.js"]

This is a multi-stage docker file, build on Linux alpine, we are first installing an extra package libc6-comat then creating a separate user for our application for an extra security layer.

We are installing curl , we will use it to check if our image is healthy.

Then we copies needed folders with the right permissions

We finally run our application suing node dist/main.js

3- frontend(Nextt.js)

# Args
ARG NODE=node:21-alpine3.19

# Stage 1: builder
FROM ${NODE} AS builder

RUN apk add --no-cache libc6-compat \
    && addgroup --system --gid 1001 nodejs \
    && adduser --system --uid 1001 nextjs

WORKDIR /app

COPY --chown=nextjs:nodejs package*.json ./

RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn \
    yarn install --frozen-lockfile

COPY --chown=nextjs:nodejs . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1

ENV NEXT_PRIVATE_STANDALONE true
ENV NODE_ENV production

# Generate the production build
RUN yarn build

# Install only the production dependencies and clean cache
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn \
    yarn install --frozen-lockfile --production && yarn cache clean

USER nextjs

# Stage 2: runner
FROM ${NODE} AS runner

RUN apk add --no-cache libc6-compat \
    && addgroup --system --gid 1001 nodejs \
    && adduser --system --uid 1001 nextjs

WORKDIR /app

ENV NODE_ENV production

# Uncomment the following line in case you want to disable telemetry during runtime.
 ENV NEXT_TELEMETRY_DISABLED 1

COPY --from=builder /app/public ./public 

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
# Copy next.conf only if it's not the default
COPY --from=builder --chown=nextjs:nodejs /app/next.config.js ./
COPY --from=builder --chown=nextjs:nodejs /app/package*.json ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./        
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

# Set Docker as non-root user
USER nextjs

EXPOSE 3540

ENV PORT 3540

ENV HOSTNAME "0.0.0.0"

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD ["node", "server.js"]

In this multi-stage Dockerfile:

  • we are using a slim Linux image based on alpine

  • We are creating a new user as an extra security layer

  • We are using a special technique to re-use the yarn cache from preview builds (DOCKER_BUILDKIT must be enabled)

--mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn \

  • We can (optionally) disable Next.js telemetry using this line:

ENV NEXT_TELEMETRY_DISABLED 1

  • Then we are building our Next.js as a standalone application and coping the necessary folders

  • We set a custom port: 3540

ENV PORT 3540

  • And starting the app using: node server.js

Conclusion

Setting up nextpalestine is a straightforward process that leverages modern development tools and best practices. By following the steps outlined in this guide, you can quickly deploy a robust blogging platform with a powerful editor, user management, and more. Whether you're looking to self-host or contribute to the project, nextpalestine provides a seamless and efficient experience for developers and users alike. Dive in and start building your stunning blogging platform today!

Did you find this article valuable?

Support Let's code by becoming a sponsor. Any amount is appreciated!