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!