├── .docker ├── project │ ├── .prettierignore │ ├── .npmrc │ ├── .prettierrc.json │ └── .eslintrc.json ├── scripts │ ├── entrypoint.sh │ └── bootstrap.sh └── Dockerfile ├── .env.example ├── .gitignore ├── docker-compose.yml ├── README.md └── dc.sh /.docker/project/.prettierignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.docker/project/.npmrc: -------------------------------------------------------------------------------- 1 | loglevel=verbose 2 | -------------------------------------------------------------------------------- /.docker/project/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true, 4 | "semi": false 5 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MY_UID=1000 2 | MY_GID=1000 3 | DOCKER_COMPOSE_PROJECT=nextjs-docker-compose 4 | TARGET_WORKDIR=/project 5 | NEXT_VERSION=latest 6 | -------------------------------------------------------------------------------- /.docker/project/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "next/typescript", 5 | "plugin:prettier/recommended" 6 | ], 7 | "plugins": ["prettier"], 8 | "rules": { 9 | "prettier/prettier": "warn" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /next/node_modules 3 | /next/.pnp 4 | /next/.pnp.js 5 | 6 | # testing 7 | /next/coverage 8 | 9 | # next.js 10 | /next/.next/ 11 | /next/out/ 12 | 13 | # production 14 | /next/build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | 23 | # local env files 24 | .env*.local 25 | .env 26 | 27 | # vercel 28 | .vercel 29 | 30 | # typescript 31 | *.tsbuildinfo 32 | 33 | /next/package-lock.json 34 | -------------------------------------------------------------------------------- /.docker/scripts/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Ensure the TARGET_WORKDIR environment variable is set 4 | export TARGET_WORKDIR=${TARGET_WORKDIR:-/project} 5 | 6 | # Path to the bootstrap script 7 | BOOTSTRAP_SCRIPT=${TARGET_WORKDIR}/.docker/scripts/bootstrap.sh 8 | 9 | # Check if the bootstrap script exists 10 | if [ -f "$BOOTSTRAP_SCRIPT" ]; then 11 | # Make sure it's executable 12 | chmod +x "$BOOTSTRAP_SCRIPT" 13 | # Run the bootstrap script 14 | "$BOOTSTRAP_SCRIPT" 15 | else 16 | echo "Bootstrap script not found at $BOOTSTRAP_SCRIPT" 17 | exit 1 18 | fi 19 | 20 | # Navigate to the project directory 21 | cd "${TARGET_WORKDIR}/next" 22 | 23 | # Execute the CMD passed from the Dockerfile or docker-compose 24 | exec "$@" 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | nextjs-app: 3 | user: '${MY_UID}:${MY_GID}' 4 | container_name: node-nextjs 5 | build: 6 | context: . 7 | dockerfile: ./.docker/Dockerfile 8 | args: 9 | MY_UID: ${MY_UID} 10 | MY_GID: ${MY_GID} 11 | TARGET_WORKDIR: ${TARGET_WORKDIR} 12 | ports: 13 | - 3000:3000 14 | env_file: 15 | - path: ./.env 16 | required: true # default 17 | tty: true 18 | volumes: 19 | # Mount project directory, but ensure it's mounted properly 20 | # to not overwrite the initialized project 21 | - .:${TARGET_WORKDIR}:delegated 22 | # Persist npm cache to avoid refetching on every container build/rebuild 23 | - npm_cache:/home/container_user/.npm 24 | # VS Code server for dev container usage 25 | - vscode-server:/home/container_user/.vscode-server 26 | # keep node_modules away from host machine project dir 27 | - node_modules:${TARGET_WORKDIR}/next/node_modules 28 | command: ['npm', 'run', 'dev'] 29 | 30 | volumes: 31 | npm_cache: 32 | vscode-server: 33 | node_modules: 34 | -------------------------------------------------------------------------------- /.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-bullseye 2 | 3 | # Declare build arguments and set default values 4 | ARG TARGET_WORKDIR=/project 5 | ARG MY_UID=1000 6 | ARG MY_GID=1000 7 | 8 | # Set the TARGET_WORKDIR environment variable for bootstrap script 9 | ENV TARGET_WORKDIR=${TARGET_WORKDIR} 10 | 11 | WORKDIR ${TARGET_WORKDIR} 12 | 13 | RUN apt update && apt upgrade -y 14 | 15 | # Create a user and group if they don't already exist 16 | RUN \ 17 | if ! getent group ${MY_GID} >/dev/null; then \ 18 | groupadd -g ${MY_GID} container_group; \ 19 | else \ 20 | groupmod -n container_group $(getent group ${MY_GID} | cut -d: -f1); \ 21 | fi && \ 22 | if ! getent passwd ${MY_UID} >/dev/null; then \ 23 | useradd -u ${MY_UID} -g ${MY_GID} -m container_user; \ 24 | else \ 25 | usermod -d /home/container_user -l container_user $(getent passwd ${MY_UID} | cut -d: -f1); \ 26 | fi 27 | 28 | # Ensure the home directory is owned by container_user 29 | RUN mkdir -p /home/container_user 30 | RUN chown -R container_user:container_group /home/container_user 31 | 32 | # Ensure the project directory is prepped for the node_modules mount and owned by container_user 33 | RUN mkdir -p ${TARGET_WORKDIR}/next/node_modules 34 | RUN chown -R container_user:container_group ${TARGET_WORKDIR} 35 | 36 | # Copy entrypoint script into the image 37 | COPY ./.docker/scripts/entrypoint.sh /usr/local/bin/entrypoint.sh 38 | RUN chmod +x /usr/local/bin/entrypoint.sh 39 | 40 | # Switch to the non-root user 41 | USER container_user 42 | 43 | # Set NPM directories to user's home directory 44 | RUN mkdir -p /home/container_user/.npm /home/container_user/.npm-global 45 | RUN npm config set prefix /home/container_user/.npm-global 46 | RUN npm config set cache /home/container_user/.npm 47 | 48 | # Update PATH environment variable 49 | ENV PATH=$PATH:/home/container_user/.npm-global/bin 50 | 51 | # Install global npm packages as non-root user 52 | RUN npm install -g npm 53 | 54 | # Set the entrypoint 55 | ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] 56 | 57 | # Expose port 3000 58 | EXPOSE 3000 59 | 60 | # Default command 61 | CMD ["bash"] -------------------------------------------------------------------------------- /.docker/scripts/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ -z "$TARGET_WORKDIR" ]; then 6 | echo "Error: TARGET_WORKDIR is not set." 7 | exit 1 8 | fi 9 | 10 | next_project_dir=${TARGET_WORKDIR}/next 11 | next_version=${NEXT_VERSION} 12 | 13 | # during build let's initialise a default next js project with sensible defaults (each to their own ofc, modify as necessary before running) 14 | # we have to bootstrap the app in the /tmp dir and copy files into the next_project_dir due to the the node_modules docker compose mount which prevents create-next-app from succeeding due to existing files in the target dir, so we skip install here, copy everything over then install node modules later 15 | 16 | # test if the target next project dir is empty (minus the node_modules mount) and if so start bootstrapping 17 | if [ ! -d "$next_project_dir" ] || [ -z "$(ls -A "$next_project_dir" | grep -v '^node_modules$' | head -n1)" ]; then 18 | 19 | # this uses `create-next-app` for bootstrapping, run `npx create-next-app@latest` 20 | # to see available options 21 | 22 | echo "Initializing Next.js project..." 23 | 24 | # Initialize in a temporary directory 25 | tmp_dir="/tmp/next_app" 26 | 27 | echo "Clearing any existing temporary files" 28 | rm -rf "$tmp_dir" || { 29 | echo "Failed to remove temporary files" 30 | exit 1 31 | } 32 | 33 | echo "Ensuring temp dir exists" 34 | mkdir -p "$tmp_dir" || { 35 | echo "Failed to create temp dir" 36 | exit 1 37 | } 38 | 39 | echo "Switching to temp dir" 40 | cd "$tmp_dir" || { 41 | echo "Failed to change directory to temp dir" 42 | exit 1 43 | } 44 | 45 | # Initialize Next.js project 46 | echo "Running create-next-app for version $NEXT_VERSION..." 47 | 48 | if ! npx --yes create-next-app@$NEXT_VERSION . --yes \ 49 | --ts \ 50 | --tailwind \ 51 | --eslint \ 52 | --app \ 53 | --src-dir \ 54 | --use-npm \ 55 | --import-alias "@/*" \ 56 | --skip-install; then 57 | echo "Next.js project initialization failed." 58 | exit 1 59 | fi 60 | 61 | # it can easily be swapped out with t3 starter, 62 | # see https://create.t3.gg/en/installation for options 63 | # note: not tested, and may have issues re src/ dir 64 | # npx create t3-app@latest \ 65 | # --noGit 66 | # --CI \ 67 | # --trpc \ 68 | # --prisma \ 69 | # --nextAuth \ 70 | # --tailwind \ 71 | # --dbProvider mysql 72 | 73 | # ensure working directory exists 74 | mkdir -p $next_project_dir || { 75 | echo "Failed to create directory: $next_project_dir" 76 | exit 1 77 | } 78 | 79 | cp -R . "$next_project_dir" || { 80 | echo "Failed to copy files across" 81 | exit 1 82 | } 83 | 84 | rm -rf "$tmp_dir" || { 85 | echo "Failed to remove temporary files" 86 | exit 1 87 | } 88 | 89 | # chown -R "$(id -u):$(id -g)" "$next_project_dir" 90 | 91 | else 92 | echo "Next.js project already initialized, skipping bootstrapping." 93 | fi 94 | 95 | if [ -z "$(ls -A "$next_project_dir/node_modules" | head -n1)" ]; then 96 | # Install node modules (required to run separately to the above if the docker volume is destroyed after the project has been initialised or an existing project has been dropped in) 97 | 98 | echo "Installing node modules..." 99 | 100 | # navigate to the working directory 101 | cd $next_project_dir || { 102 | echo "Failed to navigate to directory: $next_project_dir" 103 | exit 1 104 | } 105 | 106 | npm install --loglevel verbose 107 | 108 | fi 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js Project Starter Kit with docker compose for VS Code Dev Containers 2 | 3 | **UPDATE 2024-10-31: Happy Halloween 🎃** Now supports Next.js 15 (or another other version you'd like to use) 4 | 5 | This repository provides a rapid one command setup for developing a new Next.js application in a docker compose environment. It's designed to facilitate VS Code container development without cluttering your host machine with Node.js modules, the Node runtime, npm, or dependencies other than docker itself. Using Docker volumes for `node_modules` and VS Code container dev dependencies allows for faster build times, especially when repeatedly removing and rebuilding the Docker container should you need to for whatever reason, e.g. os level packages, other deps or customisations. The dev container is now pretty much a replica of what you'd use in a production k8s cluster... with some required tweaks, a build process and testing of course. 6 | 7 | By default it sets up a new project with the following enabled: 8 | 9 | - Typescript 10 | - Src dir 11 | - App dir 12 | - Eslint 13 | - Tailwind 14 | - NPM 15 | 16 | Theoretically it should be possible to drop an existing next.js project into the ./next/ directory and have it boot but this is untested as yet. 17 | 18 | ## Getting Started 19 | 20 | 1. Clone this repository to your local machine and run the below (preferably in a terminal separate to VS Code, as we'll be attaching to the container with that later on): 21 | 22 | ```sh 23 | git clone https://github.com/coredevel/nextjs-docker-compose.git 24 | cd nextjs-docker-compose 25 | # optionally delete the git folder and reinitialise 26 | rm -rf .git 27 | git init 28 | ``` 29 | 30 | 2. Run the Docker container: 31 | 32 | ```sh 33 | ./dc.sh 34 | ``` 35 | 36 | 3. Once it's built you'll see it's serving, simply go to [http://localhost:3000](http://localhost:3000) and the next.js boiler plate is served up good to go. 37 | 38 | 4. Next, access the development environment through VS Code by attaching to container and opening the `/project/next` folder and you're ready to develop 39 | 40 | **Note**, remember to delete the .git folder, and reinitialise if you wish to version control your own project/work! 41 | 42 | ### Using the Run Script (./dc.sh) 43 | 44 | There's a script at `./dc.sh` (_*short for docker compose*_ :) ) which contains some helper and convenient functions to easily manage your dockerised Next.js project: 45 | 46 | - Build images 47 | - Run containers 48 | - Stop services 49 | - Clean up resources 50 | 51 | When running the script it'll perform some preflight checks and create a .env file if one doesn't exist, and set a bunch of defaults, there's no need to create this manually to use this repo as is to get a Next.js project in a docker compose environment up and running but there is a .env.example provided which can be copied to .env and values modified. 52 | 53 | #### Usage: 54 | 55 | ```bash 56 | ./dc.sh [OPTION] 57 | ``` 58 | 59 | #### Options: 60 | 61 | - -u --up 62 | Run Docker environment (note, without detached mode) 63 | 64 | - -s --stop 65 | Stop the environment 66 | 67 | - -d --down 68 | Down the environment (stop and remove containers and network, but not volumes) 69 | 70 | - -b --build 71 | Build Docker images 72 | 73 | - -r --rebuild 74 | Rebuild Docker images (without using cached layers) 75 | 76 | - -da --drop-all 77 | Drop all, including the volumes associated with the project 78 | 79 | - -h --help 80 | Show the help message 81 | 82 | ### Adding New NPM Dependencies 83 | 84 | To add new npm dependencies, either: 85 | 86 | 1. Open a VS Code integrated terminal while attached to the running container and run `npm install package-name` as usual; or 87 | 88 | 2. Open a terminal to the container via docker exec: 89 | 90 | ```sh 91 | docker exec -it node-nextjs bash 92 | npm install package 93 | ``` 94 | 95 | ### Changing Next version 96 | 97 | By default this will run npx create-next-app@latest, where latest is pulled from the .env file, that's initialised when first running `./dc.sh`. 98 | Copy .env.example to .env and update the `NEXT_VERSION` value to your desired version. 99 | Ensure that the DOCKER_COMPOSE_PROJECT value is reflective of the directory name the repo was cloned to, e.g. if cloned to `/home/user/projects/nextjs-docker-compose`, the value would be `nextjs-docker-compose`. This is required for volume clean ups where they are filtered on the `com.docker.compose.project` label and removed accordingly. 100 | 101 | ### Changing the install options 102 | 103 | In the `./.docker/scripts/bootstrap.sh` you'll see the `npx` command, modify as necessary, but be aware that some versions of `create-next-app` the --yes flag doesn't work and causes the script to appear as if it's 'hanging', whereas it's waiting on interactive input which we don't have available when running `docker compose up`. So it's best to ensure all options/flags are set to avoid any install questions being asked. 104 | 105 | ### Issues 106 | 107 | Any issues, feedback or changes please feel free to open tickets or PRs. Thanks. 108 | -------------------------------------------------------------------------------- /dc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Ensure script is exec'd from project root 4 | if [ ! -f "docker-compose.yml" ]; then 5 | echo "Error: Script must be run from the project root" 6 | exit 1 7 | fi 8 | 9 | # Ensure Docker is installed and is at least version 20.10 10 | if ! command -v docker &>/dev/null; then 11 | echo "Error: Docker is not installed" 12 | exit 1 13 | fi 14 | 15 | # Get Docker version 16 | docker_version=$(docker --version | awk '{print $3}' | cut -d, -f1) 17 | required_version="20.10" 18 | 19 | if [ "$(printf '%s\n' "$required_version" "$docker_version" | sort -V | head -n1)" != "$required_version" ]; then 20 | echo "Error: Docker version must be at least $required_version" 21 | exit 1 22 | fi 23 | 24 | echo "Welcome to the Next.js Containerised Project Management Script! 25 | 26 | Easily manage your dockerised Next.js project: 27 | - Build images 28 | - Run containers 29 | - Stop services 30 | - Clean up resources 31 | 32 | Get a fully up and running environment with minimal effort. 33 | " 34 | 35 | echo -e "Docker version: $docker_version. OK!\n" 36 | 37 | main() { 38 | ( 39 | local MY_UID=$(id -u) # could use $UID or $EUID as they're already set 40 | local MY_GID=$(id -g) 41 | local DOCKER_COMPOSE_PROJECT=$(basename "$(pwd)") 42 | local DEFAULT_TARGET_WORKDIR=/project # default target dir on containter, override in .env 43 | local ENV_FILE=".env" 44 | local NEXT_DIR="./next" 45 | local NODE_MODULES_DIR="$NEXT_DIR/node_modules" 46 | local DEFAULT_NEXT_VERSION=latest 47 | 48 | # Check if .env file exists, create it if not 49 | if [ ! -f "$ENV_FILE" ]; then 50 | touch "$ENV_FILE" 51 | fi 52 | 53 | # Ensure MY_UID and MY_GID values are up to date in .env file 54 | if grep -q "MY_UID=" "$ENV_FILE"; then 55 | sed -i "s/MY_UID=.*/MY_UID=$MY_UID/" "$ENV_FILE" 56 | else 57 | echo "MY_UID=$MY_UID" >>"$ENV_FILE" 58 | fi 59 | if grep -q "MY_GID=" "$ENV_FILE"; then 60 | sed -i "s/MY_GID=.*/MY_GID=$MY_GID/" "$ENV_FILE" 61 | else 62 | echo "MY_GID=$MY_GID" >>"$ENV_FILE" 63 | fi 64 | 65 | # Set compose project name based on the parent working project dir, used for filtering / removing volumes 66 | # Beware, could present trouble if the base dir is renamed and this entry deleted from the .env file 67 | if ! grep -q "DOCKER_COMPOSE_PROJECT=" "$ENV_FILE"; then 68 | echo "DOCKER_COMPOSE_PROJECT=$DOCKER_COMPOSE_PROJECT" >>"$ENV_FILE" 69 | fi 70 | 71 | if ! grep -q "TARGET_WORKDIR=" "$ENV_FILE"; then 72 | echo "TARGET_WORKDIR=$DEFAULT_TARGET_WORKDIR" >>"$ENV_FILE" 73 | fi 74 | 75 | if ! grep -q "NEXT_VERSION=" "$ENV_FILE"; then 76 | echo "NEXT_VERSION=$DEFAULT_NEXT_VERSION" >>"$ENV_FILE" 77 | fi 78 | 79 | # Ensure the ./next/node_modules directory exists and has the correct ownership 80 | if [ ! -d "$NODE_MODULES_DIR" ]; then 81 | echo "Creating $NODE_MODULES_DIR" 82 | mkdir -p "$NODE_MODULES_DIR" 83 | fi 84 | 85 | echo "Recursively setting ownership of $NEXT_DIR to UID:GID $MY_UID:$MY_GID" 86 | chown -R "$MY_UID:$MY_GID" "$NEXT_DIR" 87 | 88 | show_help() { 89 | echo "Usage: $0 [OPTION]" 90 | echo "" 91 | echo "Options:" 92 | printf "\t%-5s %-15s %-s\n" "-s" "--stop" "Stop the environment" 93 | printf "\t%-5s %-15s %-s\n" "-d" "--down" "Down the environment (stop and remove containers and network, but not volumes)" 94 | printf "\t%-5s %-15s %-s\n" "-b" "--build" "Build Docker images" 95 | printf "\t%-5s %-15s %-s\n" "-r" "--rebuild" "Rebuild Docker images" 96 | printf "\t%-5s %-15s %-s\n" "-u" "--up" "Run Docker environment with -d flag (default)" 97 | printf "\t%-5s %-15s %-s\n" "-da" "--drop-all" "Drop all, including the volumes associated with the project" 98 | printf "\t%-5s %-15s %-s\n" "-h" "--help" "Show this help message" 99 | } 100 | 101 | stop_containers() { 102 | docker compose stop 103 | } 104 | 105 | down_containers() { 106 | docker compose down 107 | } 108 | 109 | build_images() { 110 | docker compose build 111 | } 112 | 113 | rebuild_images() { 114 | docker compose build --no-cache 115 | } 116 | 117 | run_environment() { 118 | docker compose up 119 | } 120 | 121 | drop_all() { 122 | # Get the DOCKER_COMPOSE_PROJECT from the .env file 123 | local DOCKER_COMPOSE_PROJECT 124 | DOCKER_COMPOSE_PROJECT=$(grep "^DOCKER_COMPOSE_PROJECT=" "$ENV_FILE" | cut -d '=' -f2-) 125 | 126 | if [ -n "$DOCKER_COMPOSE_PROJECT" ]; then 127 | echo -e "Dropping all, including the volumes associated with the project...\n" 128 | 129 | # Downing the environment 130 | printf "%-35s" "Downing the environment..." 131 | 132 | if docker compose down >/dev/null 2>&1; then 133 | echo -e "\e[32mOK\e[0m" 134 | else 135 | echo -e "\e[31mFailed\e[0m" 136 | fi 137 | 138 | # Removing the volumes 139 | printf "%-35s" "Removing the volumes..." 140 | 141 | if docker volume ls --filter "label=com.docker.compose.project=$DOCKER_COMPOSE_PROJECT" -q | xargs -r docker volume rm >/dev/null 2>&1; then 142 | echo -e "\e[32mOK\e[0m" 143 | else 144 | echo -e "\e[31mFailed\e[0m" 145 | fi 146 | 147 | else 148 | echo "Error: DOCKER_COMPOSE_PROJECT not found in .env file" 149 | exit 1 150 | fi 151 | } 152 | 153 | # Check if any flags were provided 154 | if [ -z "$1" ]; then 155 | echo "Starting container" 156 | run_environment 157 | else 158 | # Parse flags 159 | while [[ $# -gt 0 ]]; do 160 | case $1 in 161 | -s | --stop) 162 | stop_containers 163 | shift 164 | ;; 165 | -d | --down) 166 | down_containers 167 | shift 168 | ;; 169 | -b | --build) 170 | build_images 171 | shift 172 | ;; 173 | -r | --rebuild) 174 | rebuild_images 175 | shift 176 | ;; 177 | -u | --up) 178 | run_environment 179 | shift 180 | ;; 181 | -da | --drop-all) 182 | drop_all 183 | shift 184 | ;; 185 | -h | --help) 186 | show_help 187 | exit 0 188 | ;; 189 | *) 190 | echo -e "Unknown option: $1\n" 191 | show_help 192 | exit 1 193 | ;; 194 | esac 195 | done 196 | fi 197 | ) 198 | } 199 | main "${@}" 200 | --------------------------------------------------------------------------------