Skip to content

Instantly share code, notes, and snippets.

@524c
Forked from aradalvand/DockerfileForSvelteKit.md
Created March 13, 2023 18:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 524c/ff4508e277beb8ad37416b9b123d546e to your computer and use it in GitHub Desktop.
Save 524c/ff4508e277beb8ad37416b9b123d546e to your computer and use it in GitHub Desktop.
Dockerfile and .dockerignore for SvelteKit:

This Dockerfile is intended for SvelteKit applications that use adapter-node.

Dockerfile:

FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --production

FROM mhart/alpine-node:slim-16
WORKDIR /app
COPY --from=builder /app/build build/
COPY --from=builder /app/node_modules node_modules/
COPY package.json .
EXPOSE 3000
ENV NODE_ENV=production
CMD [ "node", "build" ]

.dockerignore:

Dockerfile
.dockerignore
.git
.gitignore
.gitattributes
README.md
.npmrc
.prettierrc
.eslintrc.cjs
.graphqlrc
.editorconfig
.svelte-kit
.vscode
node_modules
build
package
**/.env

How it works:

Let's break down this Dockerfile and explain its various parts:

Build Stage:

Note that we're doing what's called a multistage build, hence the two FROM statements and the AS builder part in the first one. This is highly recommended for applications that have a build step (such as SvelteKit apps), as it drastically reduces the size of the final image.

To learn more about multistage builds, see official Docker documentation on multistage builds.

FROM node:16-alpine AS builder

We are setting node:16-alpine as the base image of our first stage, but you can of course change this, or use another version, if you see fit.

The AS builder part is simply assigning a custom name to this first stage, so that we can reference it later in the second stage more easily.

The alpine variation is based on Alpine Linux, which is a very minimal Linux distro, built exactly to be used in containers. And even though the size of the variation you use in the first stage, makes no difference to the final image size, since the second stage is what matters in that regard, it's good to use a minimal variant like this, so that your build times will be shorter, as Docker has to download every base image, and the smaller the base image, the less time and bandwidth will have to be spent for downloading it, obviously.


WORKDIR /app

This instruction is pretty self-explanatory. We're simply setting the working directory to /app, so that we don't have to repeat it for each subsequent COPY instruction.


COPY package*.json .

We copy the package.json + package-lock.json files into the working directory. The wildcard character * here is for convenience, it's to avoid explicitly naming both files, like COPY package.json package-lock.json ..


RUN npm ci

We run the npm ci command to install all the dependencies, based on the newly copied package-*.json file(s).

As you can see, we're using npm ci as opposed to the regular npm install, since the former is more fit for productions environments. For more information, see npm ci in official NPM documentations.

Note that we can't do npm ci --production — which doesn't install the dev dependencies — because the SvelteKit package itself is a dev dependency and we need it for building the app.


COPY . .

This instruction copies the rest of the source files into the working directory.

The reason we copied the pakcage-*.json file(s) and installed the dependencies first, is because we want to take advantage of Docker's layer caching mechanism, which we couldn't do if we copied everything in one go. See this.

This is a very common practice when writing Dockerfiles for Node applications and you'll see it pretty much everywhere.


RUN npm run build
RUN npm prune --production

We run the npm run build command so that SvelteKit generates the build directory, containing a standalone Node server that serves our application — assuming, of course, that you're using adapter-node.

We subsequently run the npm prune --production command to delete all the dev dependencies from the "node_modules" folder, since we no longer need any of them.


Final Stage:

Now that we have our build directory and its contents ready, we can begin the final stage, which is responsible for running our application.

FROM mhart/alpine-node:slim-16

The base image I'm using here may have caught your attention, as it is not the official node image.

The official Node image doesn't have a variant that's both alpine-based AND slim, this is why we're using this third-party image called mhart/alpine-node, which has a variant called slim (what we're using here), which is precisely what we want, namely alpine-based + slim.

With this, the resulting image will be smaller than it would've been had we used node:16-alpine or node:16-slim — also interestingly, node:<version>-slim is actually bigger than node:<version>-alpine so there would've been no point in using the former. But a combination of the two is smaller than either one of them, which is, once again, what this image provides.


WORKDIR /app

Same as the one in the first stage. Sets the working directory to /app.


COPY --from=builder /app/build build/
COPY --from=builder /app/node_modules node_modules/
COPY package.json .

The adapter-node documentation states the following:

You will need the output directory (build by default), the project's package.json, and the production dependencies in node_modules to run the application.

And so here, we are copying the node_modules directory (stripped of all the dev dependencies, since we did npm prune --production in the previous stage), the package.json file, and of course the build directory, from the previous stage into the working directory.

Note that some Dockerfiles (the one in this article, for instance) make the mistake of copying everything from the first stage over to the second one, including the source files, this is completely redundant, unnecessarily increases the size of the image, and basically defeats the whole point of using multistage builds. Only the artifacts that are strictly necessary for running the application should be copied over to the final stage, everything else should be left out.


EXPOSE 3000

According to the adapter-node documentation, the default port that the application will run on is 3000, assuming this, we are exposing the port 3000 via this instruction.

You can also change the port by assigning a different number to the PORT environment variable, but there's no need to do that in this case.


ENV NODE_ENV=production

Here we set the NODE-ENV variable to production, to let Node.js and other code that we're about to run know that this is a production environment.


CMD [ "node", "build" ]

We finally run the node build command — equivalent to node build/index.js — to start the server.


Also, here's a great YouTube video on this subject, building the same Dockerfile step-by-step: Containerize SvelteKit NodeJs with Docker

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment