Iman Sugirman

Deploy Nextjs Menggunakan Docker Secara Efisien dengan Multi Stage Docker

20 Juli 2022

Jadi, katakanlah Anda telah menulis aplikasi yang luar biasa di Next.js dan Anda ingin menerapkannya ke platform kemas yang bagus seperti Digital Ocean atau Fly.io. Tetapi katakanlah Anda, seperti saya pada awal minggu lalu, belum pernah membuat aplikasi Node dalam container sebelumnya dan membutuhkan kursus kilat tentang cara melakukannya?

Inilah yang saya pelajari melalui proses ini untuk menyebarkan Tweet Sapu ke fly.io - baik langkah pertama yang naif untuk membuat wadah berfungsi sama sekali dan kemudian juga beberapa pengoptimalan yang diperlukan untuk itu.

Ikuti Setup Docker Untuk Nextjs

Jika Anda ingin mengikuti, Anda harus menginstal Docker Desktop dan Yarn. Agar semuanya dapat direplikasi, saya menggunakan contoh Next.js Blog-Starter-Typescript dalam instruksi ini. Anda dapat mengaturnya secara lokal dengan perintah ini:

yarn create next-app --example blog-starter-typescript blog-starter-typescript-app

Sebagai catatan tambahan, tips dan trik di sini bersifat umum untuk semua aplikasi Node dalam container, tetapi Dockerfile itu sendiri hanya akan berfungsi sebagai copy-paste yang tidak di-tweak jika Anda menggunakan Next.js. Jadi, jika Anda menggunakan platform yang berbeda, Anda mungkin harus mengubah file mana yang disimpan di penampung akhir Anda.

Dasar-Dasar Persiapan

Jadi mari kita mulai dengan 101 - apa itu Docker dan mengapa Anda ingin menggunakannya. Pada intinya, Docker Containers adalah komputer virtual kecil yang diserialisasi ke disk dalam format standar. Untuk membuatnya, Anda membutuhkan tiga bahan:

  • Gambar awal untuk dibangun - biasanya ini adalah gambar sistem operasi lengkap dengan beberapa perangkat lunak pra-instal dari Docker Hub.
  • File baru untuk ditambahkan - dalam hal ini kode untuk aplikasi Anda.

  • Langkah-langkah untuk menggabungkan dua komponen pertama. Inilah yang disimpan dalam file Docker dan file .dockerignore.

Dengan menggunakan ketiga komponen ini, Anda dapat membungkus perangkat lunak Anda ke dalam wadah standar yang dapat dijalankan di mesin apa pun yang menginstal perangkat lunak Docker. (Perhatikan bahwa ini memiliki peringatan besar "dalam teori" terlampir - jika Anda melakukan operasi yang kompleks dan canggih maka Anda mungkin mengalami batas kemampuan Docker. Namun, untuk aplikasi Next.js langsung seperti yang saya gunakan menggunakan di sini, ini bekerja dengan sangat baik.)

File Docker yang Naif

Jadi seperti apa instruksi ini untuk aplikasi Next.js kami?

COPY # Naively Simple Node Dockerfile FROM node:14.17-alpine RUN mkdir -p /home/app/ && chown -R node:node /home/app WORKDIR /home/app COPY --chown=node:node . . USER node RUN yarn install --frozen-lockfile RUN yarn build EXPOSE 3000 CMD [ "yarn", "start" ]

Letakkan ini di file bernama Dockerfile di folder root aplikasi Anda.

Memahami Dockerfile

Jadi apa fungsinya? Nah, Docker akan melewati instruksi ini satu per satu dan melakukan hal berikut:

COPY FROM node:14.17-alpine

Ini memberi tahu Docker bahwa aplikasi Anda sedang dibangun di atas wadah yang telah diinstal sebelumnya dengan Alpine Linux dan Node 14.17 (dengan npm dan benang).

RUN mkdir -p /home/app/ && chown -R node:node /home/app WORKDIR /home/app COPY --chown=node:node . . USER node

Ini adalah instruksi nyata pertama kami - kami membuat direktori bernama /home/app, memberikan kepemilikannya kepada pengguna bernama node, menjadikannya "direktori kerja" untuk wadah kami (di mana Docker mengharapkan file program utama kami untuk hidup), dan salin file di direktori tempat kami menjalankan docker build ke dalam wadah. Ingat wadah pada dasarnya adalah komputer kecil virtual, jadi kita harus menyalin file kita di sana untuk mengaksesnya!

Kami kemudian menjadi pengguna simpul itu. Secara default Docker berjalan sebagai root pada mesin yang ada. Tapi itu cukup berbahaya karena memberikan hak akses root ke kode apa pun yang kita jalankan, yang berarti sedikit kelemahan keamanan di Node atau salah satu dependensi NPM kita berpotensi memberikan akses ke seluruh server kita. Jadi, untuk menghindari itu, kami beralih ke pengguna non-root.

RUN yarn install --frozen-lockfile RUN yarn build

Kami menginstal dependensi NPM kami dan membangun server Next.js kami dalam mode produksi.

EXPOSE 3000 CMD [ "yarn", "start" ]

Dan akhirnya kedua perintah ini memberikan instruksi Docker yang akan digunakannya ketika mencoba menjalankan software ini. Yang pertama memberi tahu Docker bahwa wadah ini mengharapkan koneksi pada port 3000, jadi itu harus mengekspos bahwa meninggalkan wadah (kami akan menghubungkannya sebentar dengan flag -p). Yang kedua memberi tahu Docker bahwa perintah untuk menjalankan untuk memulai wadah ini adalah yarn start.

Bangun dan Jalankan!

Sekarang saatnya untuk menjalankan langkah-langkah itu dan membuat wadah Anda. Jalankan perintah berikut di terminal di direktori proyek Anda (Anda dapat mengganti beberapa-nama dengan tag pribadi seperti zacks-blog-1.0):

docker build -t some-name .

Gambar bawaan Anda, berisi mesin virtual yang siap menjalankan aplikasi web Anda, sekarang akan muncul secara lokal jika Anda memeriksa docker image ls:

$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE some-name latest 4c73a8c8d35c 2 minutes ago 622MB

Mari kita mulai:

docker run -p 3000:3000 some-name

(Anda dapat menambahkan flag -d setelah dijalankan untuk menjalankan server di latar belakang.)

Anda akan melihat log yang sama seperti jika Anda menjalankan yarn start secara normal. Dan, karena flag -p 3000:3000, container Anda sekarang akan terhubung ke port 3000 lokal Anda, jadi jika Anda mengunjungi http://localhost:3000 Anda akan melihat template blog Anda:

Optimalkan - Mempersiapkan produksi ini

Besar! Anda sekarang telah mengemas aplikasi Anda. Tetapi sebelum Anda menerapkannya ke platform hosting favorit Anda, ada beberapa hal yang perlu kita lakukan.

Anda mungkin telah memperhatikan di atas bahwa ukuran gambar yang kami buat lebih dari 600MB - itu lebih dari 4x ukuran proyek kami di disk di luar wadah! Masalah ini hanya bertambah ketika aplikasi Anda menjadi lebih kompleks - versi bawaan dari wadah Tweet Sapu Frontend lebih dari 5GB pada saat ini! Itu banyak data untuk diunggah ke server Anda!

Hampir semua masalah ukuran ini terkait dengan satu kekhasan Docker - hampir setiap baris di Dockerfile membuat "lapisan" baru di gambar Docker akhir Anda. Setiap lapisan menangkap perubahan yang dibuat pada mesin virtual setelah baris itu berjalan. Ini adalah alat pengoptimalan yang kuat karena memungkinkan Docker untuk menggunakan kembali pekerjaan yang telah dilakukan - misalnya jika Anda memiliki beberapa pengaturan yang tidak pernah berubah seperti baris mkdir kami, Docker dapat menghitung lapisan itu sekali dan menggunakannya kembali untuk semua pembangunan berikutnya. Namun, itu juga dapat menyebabkan masalah ukuran gambar (karena banyak file yang tidak dibutuhkan mungkin akan disimpan di lapisan tersebut) dan masalah keamanan (karena Anda mungkin menangkap nilai rahasia di lapisan tersebut yang dapat disedot oleh seseorang yang mendapatkan akses ke file Anda. gambar akhir).

Anda dapat melihat lapisan dan ukurannya masing-masing menggunakan perintah ini (kredit ke posting ini tempat saya mendapatkannya):

docker history --human --format "{{.CreatedBy}}: {{.Size}}" some-name
CMD ["yarn" "start"]: 0B EXPOSE map[3000/tcp:{}]: 0B RUN /bin/sh -c yarn build # buildkit: 10.6MB RUN /bin/sh -c yarn install --frozen-lockfil…: 340MB USER node: 0B COPY . . # buildkit: 155MB WORKDIR /home/app: 0B RUN /bin/sh -c mkdir -p /home/app/ && chown …: 0B /bin/sh -c #(nop) CMD ["node"]: 0B /bin/sh -c #(nop) ENTRYPOINT ["docker-entry…: 0B /bin/sh -c #(nop) COPY file:238737301d473041…: 116B /bin/sh -c apk add --no-cache --virtual .bui…: 7.62MB /bin/sh -c #(nop) ENV YARN_VERSION=1.22.5: 0B /bin/sh -c addgroup -g 1000 node && addu…: 104MB /bin/sh -c #(nop) ENV NODE_VERSION=14.17.0: 0B /bin/sh -c #(nop) CMD ["/bin/sh"]: 0B /bin/sh -c #(nop) ADD file:282b9d56236cae296…: 5.62MB

Dari sini kita dapat melihat bahwa sekitar 117MB ukuran gambar terjadi sebelum perintah pertama kita - ini adalah ukuran dasar dari gambar Alpine-Node yang sedang kita bangun sehingga tidak banyak yang dapat kita lakukan tentang itu. Tapi mari kita fokus pada dua pengoptimalan utama yang dapat kita lakukan setelah titik itu:

Mudah: Abaikan Barang

Di Dockerfile naif kami, kami menjalankan perintah COPY --chown=node:node . .. Ini menyalin semua file di direktori kami saat ini ke dalam wadah Docker. Ini hampir selalu bukan yang Anda inginkan! Misalnya, Anda mungkin memiliki file .env dengan rahasia di dalamnya yang akan berakhir dalam teks biasa di gambar Docker akhir. (Anda harus menggunakan fitur rahasia env di platform hosting Anda.)

Dalam kasus aplikasi ini, ini tidak perlu menyalin folder node_modules (sejak kita memasangnya lagi dengan benang) dan folder .next (karena kita membangun kembali aplikasi di dalam wadah). Kami dapat memperbaikinya dengan file .dockerignore. File ini, di root proyek kami, memberi tahu Docker untuk melewati file dan folder tertentu saat menjalankan COPY.

COPY # .dockerignore file .DS_Store .next node_modules

Lanjutan: Dapatkan Kontainer Anda sebuah Kontainer

Sekarang otak galaksi bergerak di sini adalah menggunakan wadah untuk wadah kita. Kami akan membuat dua yang hanya digunakan untuk membangun aplikasi secara terpisah dari yang diunggah ke server. Ini menyelamatkan kita dari keharusan mengunggah lapisan yang berisi semua file yang digunakan atau dibuat dalam perjalanan ke tujuan itu. Inilah Dockerfile untuk itu (dengan komentar yang menjelaskan apa yang dilakukan setiap blok):

# Double-container Dockerfile for separated build process. # If you're just copy-pasting this, don't forget a .dockerignore! # We're starting with the same base image, but we're declaring # that this block outputs an image called DEPS that we # won't be deploying - it just installs our Yarn deps FROM node:14-alpine AS deps # If you need libc for any of your deps, uncomment this line: # RUN apk add --no-cache libc6-compat # Copy over ONLY the package.json and yarn.lock # so that this `yarn install` layer is only recomputed # if these dependency files change. Nice speed hack! WORKDIR /app COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile # END DEPS IMAGE # Now we make a container to handle our Build FROM node:14-alpine AS BUILD_IMAGE # Set up our work directory again WORKDIR /app # Bring over the deps we installed and now also # the rest of the source code to build the Next # server for production COPY --from=deps /app/node_modules ./node_modules COPY . . RUN yarn build # Remove all the development dependencies since we don't # need them to run the actual server. RUN rm -rf node_modules RUN yarn install --production --frozen-lockfile --ignore-scripts --prefer-offline # END OF BUILD_IMAGE # This starts our application's run image - the final output of build. FROM node:14-alpine ENV NODE_ENV production RUN addgroup -g 1001 -S nodejs RUN adduser -S nextjs -u 1001 # Pull the built files out of BUILD_IMAGE - we need: # 1. the package.json and yarn.lock # 2. the Next build output and static files # 3. the node_modules. WORKDIR /app COPY --from=BUILD_IMAGE --chown=nextjs:nodejs /app/package.json /app/yarn.lock ./ COPY --from=BUILD_IMAGE --chown=nextjs:nodejs /app/node_modules ./node_modules COPY --from=BUILD_IMAGE --chown=nextjs:nodejs /app/public ./public COPY --from=BUILD_IMAGE --chown=nextjs:nodejs /app/.next ./.next # 4. OPTIONALLY the next.config.js, if your app has one # COPY --from=BUILD_IMAGE --chown=nextjs:nodejs /app/next.config.js ./ USER nextjs EXPOSE 3000 CMD [ "yarn", "start" ]