How I Use Next.js, Strapi & Docker to Build My Personal Website
Here's the high-level set of technologies I ended up using:
- Next.js (React)
- Docker Swarm
I use Strapi as a content source for the website. I initially used static markdown files for content but it became too inconvenient to update the files. Managing content through a nice user interface is so much easier than writing content in code!
My data model is pretty simple. I used the blog template to setup my initial content model. I have the following content types defined: Article, Project. Category, User, Writer.
Combined with Next.js, Strapi gives me the power and flexibility of a CMS with the speed of a static website.
Next.js Build Overview
- I use getStaticProps for pre-rendering the pages
- I use an auto-generated HTTP client to interact with the Strapi API from Next.js. The client is generated using the OpenAPI Generator which reads the OpenAPI spec from Strapi.
- I use Static HTML Export to generate static HTML files which are served by Nginx from within the docker container
Docker Build Overview
- The runtime docker image is based on nginx-alpine and simply serves the static HTML files generated by Next.js
- The buildtime docker image is based on node-alpine
- The docker image is built and published to my private docker registry using GitHub Actions
Here's a condensed overview of my
FROM node:16.8.0-alpine as base FROM base AS deps WORKDIR /app ENV CI true COPY package.json package-lock.json ./ RUN npm ci FROM base AS builder WORKDIR /app ARG ASSET_PREFIX ARG STRAPI_ENDPOINT ENV ASSET_PREFIX $ASSET_PREFIX ENV STRAPI_ENDPOINT $STRAPI_ENDPOINT WORKDIR /app COPY . . COPY /app/node_modules ./node_modules RUN NODE_ENV=production npm run build FROM ghcr.io/badsyntax/base-nginx:latest COPY /app/.next /app/.next COPY /app/out /usr/share/nginx/html COPY ./nginx /etc/nginx
Dockerfile above uses
to achieve the following:
- Install npm deps in the
- Build project in the
- Run project in the final stage
Using a multi-stage approach results in low file-size for my runtime docker image as no build artifacts are included.
The magic happens in my GitHub Actions CI/CD pipelines, which I use to:
- Build and publish a new docker image
- Publish static assets to AWS S3
- Restart the Next.js docker app using a Portainer webhook
I trigger the GitHub Actions deploy workflow from Strapi whenever any content change is made. To achieve this I configured a webhook in Strapi to call my strapi webhook proxy, which calls the GitHub Actions webhook.
To prevent conflicting workflows and disable parallel runs I set the concurrency setting in my workflow definition. This allows me to make multiple consecutive changes in Strapi without generating conflicting deployments:
With this setup I don't have to worry about the deployment at all, I just make as many changes as I like and the latest change will always be deployed. It takes around 3 minutes to deploy a new version after making a change in Strapi, which is pretty good!
View my complete GitHub Actions Workflow definition.
I use a self-hosted Plausible instance for tracking basic user activity (like page views). It took some effort getting Plausible to run in docker swarm, I faced some issues with docker secrets and also found & fixed a bug. This is the working docker swarm setup I ended up using.
I host all services on a small VPS with the following specs:
- 2 VCPU
- 4 GB RAM
- 40 GB SSD DISK
I use Hetzner Cloud and the server is located in Germany.
I setup the VPS using a handy shell script I created: https://github.com/badsyntax/docker-box which gives me:
- Docker Swarm to manage running Docker containers
- Portainer to manage my swarm stacks and remotely restart services
- Traefik to care of network routing and TLS
- Docker Registry to host my docker images
Static assets (eg
hosted on AWS S3 (See my blog post on
how I setup S3 & CloudFront.)
View my complete Docker Swarm stack definition.
You can find the source code to my website stack at: