
Next.js and Strapi blog / portfolio
elliott_lvnb • October 27, 2026
Docker Environment
This chapter goes over how to setup a good docker environment with docker compose.
Requirements
First you need to make sure docker is properly installed. It's the only requirement aside from make if you want to use the makefile.
Â
The compose files
Two Docker Compose files sit side by side in this repository, and they start the same three services: Strapi, Postgres, and a Next.js frontend. What changes is the job each stack is meant to do.
Â
The development compose file
Development is a workspace. The frontend and Strapi run in watch mode, with the source tree mounted from the host, so a save is enough to see the change. The database is reachable from the machine, and uploaded files stay in the project folder.
In my development setup, I also use a devcontainer which allows me to install all dependencies in a docker container so that my IDE can use its packages for syntax highlighting, commands etc... It is not part of the same compose file, but is in the same network, which is why its an external network.
services:
# Strapi CMS Backend
strapi:
container_name: ${STRAPI_CONTAINER_NAME:-strapi-dev}
build:
context: ./strapi
dockerfile: docker/development/Dockerfile
image: ${STRAPI_IMAGE:-strapi:dev}
restart: unless-stopped
env_file:
- ./strapi/.env.dev
# HOST_WORKSPACE_FOLDER is the path on the Docker host. The devcontainer
# talks to that daemon, so bind mounts cannot use the in-container /workspace path.
volumes:
- ${HOST_WORKSPACE_FOLDER:-.}/strapi/config:/opt/app/config
- ${HOST_WORKSPACE_FOLDER:-.}/strapi/src:/opt/app/src
- ${HOST_WORKSPACE_FOLDER:-.}/strapi/package.json:/opt/package.json
- ${HOST_WORKSPACE_FOLDER:-.}/strapi/package-lock.json:/opt/package-lock.json
- ${HOST_WORKSPACE_FOLDER:-.}/strapi/.env.dev:/opt/app/.env
- ${HOST_WORKSPACE_FOLDER:-.}/strapi/public/uploads:/opt/app/public/uploads
ports:
- "${STRAPI_HOST_PORT:-1337}:${STRAPI_PORT:-1337}"
networks:
- app-network
depends_on:
strapiDB:
condition: service_healthy
# PostgreSQL Database
strapiDB:
container_name: ${DATABASE_CONTAINER_NAME:-strapiDB-dev}
platform: ${DATABASE_PLATFORM:-linux/amd64}
restart: unless-stopped
image: ${DATABASE_IMAGE:-postgres:16.0-alpine}
environment:
POSTGRES_USER: ${DATABASE_USERNAME:-portfolio_user}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-portfolio_password}
POSTGRES_DB: ${DATABASE_NAME:-portfolio_dev}
volumes:
- strapi-data:/var/lib/postgresql/data/
ports:
- "${DATABASE_HOST_PORT:-5432}:${DATABASE_PORT:-5432}"
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USERNAME:-portfolio_user} -d ${DATABASE_NAME:-portfolio_dev}"]
interval: 5s
timeout: 5s
retries: 5
# Next.js frontend
frontend:
container_name: ${FRONTEND_CONTAINER_NAME:-frontend-dev}
build:
context: ./frontend
dockerfile: docker/development/Dockerfile
image: ${FRONTEND_IMAGE:-frontend:dev}
restart: unless-stopped
env_file:
- ./frontend/.env.dev
environment:
# The optimizer runs inside this container, where localhost is not Strapi.
NEXT_PUBLIC_STRAPI_IMAGE_URL: http://strapi:1337
volumes:
- ${HOST_WORKSPACE_FOLDER:-.}/frontend/app:/app/app:z
- ${HOST_WORKSPACE_FOLDER:-.}/frontend/src:/app/src:z
- ${HOST_WORKSPACE_FOLDER:-.}/frontend/public:/app/public:z
- ${HOST_WORKSPACE_FOLDER:-.}/frontend/package.json:/app/package.json:z
- ${HOST_WORKSPACE_FOLDER:-.}/frontend/next.config.ts:/app/next.config.ts:z
- ${HOST_WORKSPACE_FOLDER:-.}/frontend/tsconfig.json:/app/tsconfig.json:z
- ${HOST_WORKSPACE_FOLDER:-.}/frontend/proxy.ts:/app/proxy.ts:z
ports:
- "${FRONTEND_HOST_PORT:-3000}:${FRONTEND_PORT:-3000}"
networks:
- app-network
depends_on:
- strapi
security_opt:
- ${FRONTEND_SECURITY_OPT:-seccomp:unconfined}
volumes:
strapi-data:
name: ${DATABASE_VOLUME_NAME:-portfolio-dev-strapi-data}
networks:
# Shared with the devcontainer. `make start-dev` creates it if needed.
app-network:
name: ${NETWORK_NAME:-portfolio-dev}
external: true
The production compose file
Production is the site you ship. The frontend is built into an image and served as a finished app, with its public URLs fixed at build time. Strapi runs in production mode, uploads live in a volume of their own, and the database stays on the private network. The frontend still has the ability to be rebuilt with SSG, which will be explained in another chapter.
services:
# Strapi CMS Backend
strapi:
container_name: ${STRAPI_CONTAINER_NAME:-strapi}
build:
context: ./strapi
dockerfile: docker/production/Dockerfile
image: ${STRAPI_IMAGE:-portfolio-strapi:latest}
restart: unless-stopped
env_file: ./strapi/.env.prod
environment:
NODE_ENV: production
volumes:
- ./strapi/config:/opt/app/config
- ./strapi/src:/opt/app/src
- ./strapi/package.json:/opt/app/package.json
- ./strapi/package-lock.json:/opt/app/package-lock.json
- ./strapi/.env.prod:/opt/app/.env
- strapi-uploads:/opt/app/public/uploads
networks:
- app-network
ports:
- "${STRAPI_HOST_PORT:-1337}:${STRAPI_PORT:-1337}"
depends_on:
strapiDB:
condition: service_healthy
# PostgreSQL Database
strapiDB:
container_name: ${DATABASE_CONTAINER_NAME:-strapiDB}
platform: ${DATABASE_PLATFORM:-linux/amd64}
restart: unless-stopped
image: ${DATABASE_IMAGE:-postgres:16.0-alpine}
environment:
POSTGRES_USER: ${DATABASE_USERNAME}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
POSTGRES_DB: ${DATABASE_NAME}
volumes:
- strapi-data:/var/lib/postgresql/data/
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USERNAME} -d ${DATABASE_NAME}"]
interval: 5s
timeout: 5s
retries: 5
# Next.js Frontend
frontend:
container_name: ${FRONTEND_CONTAINER_NAME:-frontend}
build:
context: ./frontend
dockerfile: docker/production/Dockerfile
# BuildKit cannot attach a build container to a custom compose network.
# The Makefile creates a dedicated buildx builder wired to the
# portfolio-prod network so the static export can reach Strapi by
# container name (http://strapi:1337/api).
args:
STRAPI_API_URL: ${STRAPI_API_URL:-http://strapi:1337/api}
NEXT_PUBLIC_STRAPI_URL: ${NEXT_PUBLIC_STRAPI_URL}
SITE_URL: ${SITE_URL}
image: ${FRONTEND_IMAGE:-portfolio-frontend:latest}
restart: unless-stopped
env_file: ./frontend/.env.prod
ports:
- "${FRONTEND_HOST_PORT:-3000}:${FRONTEND_PORT:-3000}"
networks:
- app-network
depends_on:
strapi:
condition: service_started
volumes:
strapi-data:
name: ${DATABASE_VOLUME_NAME:-portfolio-prod-strapi-data}
strapi-uploads:
networks:
app-network:
name: ${NETWORK_NAME:-portfolio-prod}
driver: ${NETWORK_DRIVER:-bridge}
