Création d'une image Docker

Nous découvrons l'outil Docker et créons une image Docker pour créer une image de notre application.

7 min read

Nous allons utiliser Docker pour créer un environnement virtuel isolé contenant le code source de notre application.

Nous allons d'abord créer un fichier nommé Dockerfile qui contiendra la configuration de notre image. Ensuite, nous allons créer un fichier nommé .dockerignore pour lister les fichiers à ignorer lors de la création de l'image Docker. Enfin, nous allons créer un fichier nommé docker-compose.dev.yaml pour exécuter notre image Docker

Création du fichier Dockerfile

Avant de coder notre propre image Docker, nous allons d'abord nous inspirer de deux exemples d'images, optimisées pour la production.

Première inspiration : Dockerfile de Turborepo

Nous allons nous inspirer d'un fichier Dockerfile présent sur la documentation officielle de Turborepo.

Voici le fichier non édité trouvé sur la documentation.

./apps/api/Dockerfile
1
FROM node:18-alpine AS base
2
3
FROM base AS builder
4
RUN apk update
5
RUN apk add --no-cache libc6-compat
6
# Set working directory
7
WORKDIR /app
8
# Replace <your-major-version> with the major version installed in your repository. For example:
9
# RUN yarn global add turbo@^2
10
RUN yarn global add turbo@^<your-major-version>
11
COPY . .
12
13
# Generate a partial monorepo with a pruned lockfile for a target workspace.
14
# Assuming "web" is the name entered in the project's package.json: { name: "web" }
15
RUN turbo prune web --docker
16
17
# Add lockfile and package.json's of isolated subworkspace
18
FROM base AS installer
19
RUN apk update
20
RUN apk add --no-cache libc6-compat
21
WORKDIR /app
22
23
# First install the dependencies (as they change less often)
24
COPY --from=builder /app/out/json/ .
25
RUN yarn install
26
27
# Build the project
28
COPY --from=builder /app/out/full/ .
29
RUN yarn turbo run build --filter=web...
30
31
FROM base AS runner
32
WORKDIR /app
33
34
# Don't run production as root
35
RUN addgroup --system --gid 1001 nodejs
36
RUN adduser --system --uid 1001 nextjs
37
USER nextjs
38
39
# Automatically leverage output traces to reduce image size
40
# https://nextjs.org/docs/advanced-features/output-file-tracing
41
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
42
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
43
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
44
45
CMD node apps/web/server.js

Deuxième inspiration : Dockerfile de l'Epic-Stack

Nous allons aussi nous inspirer d'un fichier Dockerfile du repository epic-stack, un projet Remix open-source.

Voici son contenu :

1
# This file is moved to the root directory before building the image
2
3
# base node image
4
FROM node:20-bookworm-slim as base
5
6
# set for base and all layer that inherit from it
7
ENV NODE_ENV production
8
9
# Install openssl for Prisma
10
RUN apt-get update && apt-get install -y fuse3 openssl sqlite3 ca-certificates
11
12
# Install all node_modules, including dev dependencies
13
FROM base as deps
14
15
WORKDIR /myapp
16
17
ADD package.json package-lock.json .npmrc ./
18
RUN npm install --include=dev
19
20
# Setup production node_modules
21
FROM base as production-deps
22
23
WORKDIR /myapp
24
25
COPY --from=deps /myapp/node_modules /myapp/node_modules
26
ADD package.json package-lock.json .npmrc ./
27
RUN npm prune --omit=dev
28
29
# Build the app
30
FROM base as build
31
32
ARG COMMIT_SHA
33
ENV COMMIT_SHA=$COMMIT_SHA
34
35
# Use the following environment variables to configure Sentry
36
# ENV SENTRY_ORG=
37
# ENV SENTRY_PROJECT=
38
39
40
WORKDIR /myapp
41
42
COPY --from=deps /myapp/node_modules /myapp/node_modules
43
44
ADD prisma .
45
RUN npx prisma generate
46
47
ADD . .
48
49
# Mount the secret and set it as an environment variable and run the build
50
RUN --mount=type=secret,id=SENTRY_AUTH_TOKEN \
51
export SENTRY_AUTH_TOKEN=$(cat /run/secrets/SENTRY_AUTH_TOKEN) && \
52
npm run build
53
54
# Finally, build the production image with minimal footprint
55
FROM base
56
57
ENV FLY="true"
58
ENV LITEFS_DIR="/litefs/data"
59
ENV DATABASE_FILENAME="sqlite.db"
60
ENV DATABASE_PATH="$LITEFS_DIR/$DATABASE_FILENAME"
61
ENV DATABASE_URL="file:$DATABASE_PATH"
62
ENV CACHE_DATABASE_FILENAME="cache.db"
63
ENV CACHE_DATABASE_PATH="$LITEFS_DIR/$CACHE_DATABASE_FILENAME"
64
ENV INTERNAL_PORT="8080"
65
ENV PORT="8081"
66
ENV NODE_ENV="production"
67
# For WAL support: https://github.com/prisma/prisma-engines/issues/4675#issuecomment-1914383246
68
ENV PRISMA_SCHEMA_DISABLE_ADVISORY_LOCK = "1"
69
70
# add shortcut for connecting to database CLI
71
RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/database-cli && chmod +x /usr/local/bin/database-cli
72
73
WORKDIR /myapp
74
75
# Generate random value and save it to .env file which will be loaded by dotenv
76
RUN INTERNAL_COMMAND_TOKEN=$(openssl rand -hex 32) && \
77
echo "INTERNAL_COMMAND_TOKEN=$INTERNAL_COMMAND_TOKEN" > .env
78
79
COPY --from=production-deps /myapp/node_modules /myapp/node_modules
80
COPY --from=build /myapp/node_modules/.prisma /myapp/node_modules/.prisma
81
82
COPY --from=build /myapp/server-build /myapp/server-build
83
COPY --from=build /myapp/build /myapp/build
84
COPY --from=build /myapp/package.json /myapp/package.json
85
COPY --from=build /myapp/prisma /myapp/prisma
86
COPY --from=build /myapp/app/components/ui/icons /myapp/app/components/ui/icons
87
88
# prepare for litefs
89
COPY --from=flyio/litefs:0.5.11 /usr/local/bin/litefs /usr/local/bin/litefs
90
ADD other/litefs.yml /etc/litefs.yml
91
RUN mkdir -p /data ${LITEFS_DIR}
92
93
ADD . .
94
95
CMD ["litefs", "mount"]

Notre Dockerfile

C'est finalement cette version qu'on va utiliser (car elle est compatible avec Remix). On pourra ensuite copier les morceaux qui nous intéressent du Dockerfile Turborepo (qui utilise NextJS).

Explications de notre Dockerfile :

  • On télécharge une image de NodeJS (version 18) dans un environnement Linux AMD64
  • Nous effectuons une mise à jour système (avec la commande RUN apk update)
  • On crée un dossier /app à la racine de notre image, et il devient notre point d'entrée (grâce à la commande WORKDIR /app)
  • Nous installons en global la librairie Turborepo avec la commande npm install -g turbo
  • Nous copions ensuite l'intégralité des fichiers dans notre dossier /app (grâce à la commande COPY --chown=node:node . .)
  • Ensuite, nous installons toutes les dépendances du projet (comme on le ferait en local) avec la commande RUN npm install
  • Puis nous effectuons un build de notre application, qui va compiler le code Typescript et l'optimiser pour la production (avec la commande RUN npm run build)
  • Ensuite, nous exécutons le code source du projet (la commande npm run start)

Voici le Dockerfile :

1
FROM --platform=amd64 node:18-alpine As base
2
3
FROM base AS builder
4
5
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
6
RUN apk add --no-cache libc6-compat
7
RUN apk update
8
# Set working directory
9
WORKDIR /app
10
RUN npm install --global turbo
11
COPY --chown=node:node . .
12
RUN turbo prune @virgile/backend --docker
13
14
# Add lockfile and package.json's of isolated subworkspace
15
FROM base AS installer
16
RUN apk add --no-cache libc6-compat
17
RUN apk update
18
WORKDIR /app
19
20
# First install the dependencies (as they change less often)
21
COPY .gitignore .gitignore
22
COPY --chown=node:node --from=builder /app/out/json/ .
23
COPY --chown=node:node --from=builder /app/out/package-lock.json ./package-lock.json
24
RUN npm install
25
26
# Build the project
27
COPY --from=builder /app/out/full/ .
28
COPY turbo.json turbo.json
29
30
# Uncomment and use build args to enable remote caching
31
ARG TURBO_TEAM
32
ENV TURBO_TEAM=$TURBO_TEAM
33
34
ARG TURBO_TOKEN
35
ENV TURBO_TOKEN=$TURBO_TOKEN
36
ENV TZ=Europe/Paris
37
ENV NODE_ENV="production"
38
39
ADD backend/prisma backend/prisma
40
RUN cd backend && npx prisma generate
41
42
RUN npm run build
43
44
FROM base AS runner
45
WORKDIR /app
46
47
# Don't run production as root
48
RUN addgroup --system --gid 1001 nodejs
49
RUN adduser --system --uid 1001 remix-api
50
USER remix-api
51
52
# ENV TZ=Europe/Paris
53
# ENV NODE_ENV="production"
54
55
COPY --chown=remix-api:nodejs --from=installer /app/backend/package.json ./backend/package.json
56
COPY --chown=remix-api:nodejs --from=installer /app/backend/dist ./backend/dist
57
COPY --chown=remix-api:nodejs --from=installer /app/node_modules ./node_modules
58
COPY --chown=remix-api:nodejs --from=installer /app/node_modules/@virgile/frontend ./node_modules/@virgile/frontend
59
COPY --chown=remix-api:nodejs --from=installer /app/node_modules/@virgile/typescript-config ./node_modules/@virgile/typescript-config
60
COPY --chown=remix-api:nodejs --from=installer /app/node_modules/@virgile/eslint-config ./node_modules/@virgile/eslint-config
61
COPY --chown=remix-api:nodejs --from=installer /app/backend/prisma ./backend/prisma
62
63
COPY --chown=remix-api:nodejs --from=builder /app/backend/start.sh ./backend/start.sh
64
65
ENTRYPOINT [ "backend/start.sh" ]

Notez qu'il y a davantage d'étapes, assez redondantes qui permettent simplement d'optimiser le poids de l'image Docker

Nous allons aussi créer un fichier script, nommé start.sh qui va lancer la commande npm run start directement. Ce script nous permet d'ajouter des commandes supplémentaires à exécuter avant de lancer l'application.

On place ce fichier dans le dossier backend.

backend/start.sh
1
#!/bin/sh
2
3
set -ex
4
cd backend
5
npx prisma migrate deploy
6
npm run start

Création du fichier dockerignore

Le fichier .dockerignore nous permet de de déclarer certains fichiers à ignorer (lors de leur copie, avec la commande COPY par exemple). Le rajouter nous permet d'éviter de copier par erreur des fichiers volumineux et inutiles.

Nous en ajoutons un par projet.

Dockerignore pour le projet NestJS

1
Dockerfile
2
.dockerignore
3
node_modules
4
npm-debug.log
5
dist

Dockerignore pour le projet Remix

1
/node_modules
2
*.log
3
.DS_Store
4
.env
5
/.cache
6
/public/build
7
/build

On peut maintenant construire notre image Docker.