Next.js and Strapi blog / portfolio

Next.js and Strapi blog / portfolio

elliott_lvnb • October 27, 2026

Page 2

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.

YAML
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.

YAML
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}

Portfolio

Elliott Loevenbruck

A collection of projects built over time.

Privacy Policy

 • 

Terms of Service

Copyright © Elliott Loevenbruck 2026