---
title: "Building a Remix app locally with Docker"
description: "Building a Remix app locally with Docker"
---

# Building a Remix app locally with Docker

![An illustration of a bird drawing a blueprint of an airplane on a drafting table. In the background is a sunset with the actual airplane in the sky, with the label 'Remix' painted on the side](https://fly.io/javascript-journal/building-remix-app-locally-with-docker/assets/build-remix-app-locally-docker-cover.webp)

Annie Sexton

Fly.io makes it easy to deploy containerized apps in seconds. The more comfortable you are with Docker, the smoother and more reliable your deployments can be. Let’s learn how to leverage Docker when developing your app locally!

Docker can give developers a high degree of confidence that their applications will run exactly as expected in production. However, many people still run into failed deployments or unanticipated problems because when developing locally, the aren’t using Docker. In this article, we’re going to explore how to setup your Remix local environment to use Docker in development.

In the end, we’re going to be able to run our application locally as a Docker image that will **live reload** as we make changes.

## Create a new Remix app

Let’s start by instantiating a new Remix app with the default template:

```
npx create-remix@latest
```

As of right now, the default template for Remix uses Vite. In order the allow the Vite server to be exposed to Docker later on, we need to make a change to our `dev` script in our `package.json` by adding `--host` :

```
"scripts": {
    ...
    "dev": "remix vite:dev --host",
    ...
}
```

If you happen to be using a version of Remix that does *not* use Vite, this step isn’t necessary. With our Remix app initiated, let’s try running it locally:

```
npm run dev
```

With this running, we can visit `localhost:5173` in our browser (or port `3000` if not using Vite) and see our sample app up and running:

Hooray!

Before we set up our local environment to use Docker, let’s deploy our app as-is to Fly.io. This will set us up with the foundational Dockerfile that we’ll rely on in configuring Docker Compose for local development.

## Deploy to Fly.io

If this is your first time deploying to Fly.io, you’ll want to start by downloading the CLI [flyctl](https://fly.io/docs/hands-on/install-flyctl/), as this is the primary method you’ll use to interact with the platform.

Once installed, all you need to do is run two commands:

```
fly launch
```

This auto-generates a `fly.toml` (required config for Fly Apps) and a Dockerfile. The generated Dockerfile is specific to Remix, so no need to change a thing. 😃

Next, deploy you application to Fly.io:

```
fly deploy
```

Once completed, your app should be available at `https://<YOUR-APP-NAME>.fly.dev`

Now that we have our app running on Fly.io we can get to work setting up our local environment. To do so, we’ll be relying on **Docker Compose** .

## What is Docker Compose and why do I need it?

Working with Docker involves two steps:

- **Building** the Docker image
- **Running** the Docker image in a container

These both require separate commands, often with a slew of extra parameters to set things like exposed ports, volumes, bind mounts, environment variables, and more. These commands can get quite long and tedious to remember.

**Docker Compose** is a way of defining params you’d normally pass to your `build` and `run` commands into a single `docker-compose.yml` file. This way, all you need to do to get your app running in a Docker container is:

```
docker-compose up -d --build
```

Docker Compose is an invaluable tool for local development, as you’ll soon see. Let’s try using it with our Remix app.

## Docker Compose for local development

First, we need to make a small change to our Dockerfile so our app runs as expected in development mode. Search for this line, which removes all `devDependencies`:

```
RUN npm prune --omit=dev
```

Replace this with the following, so the command only runs when our app is in production:

```
RUN if [ "$NODE_ENV" = "production" ]; then \
        npm prune --omit-dev; \
    fi
```

Next, create a file called `docker-compose.yml` at the root of your project:

```
version: "3.8" # this the version of Docker Compose
services:
  app:
    build:
      context: ./ 
    command: npm run dev
    environment:
      - NODE_ENV=development
    ports:
      - '5173:5173'
```

A number of these settings allow us to override things defined in our Dockerfile, things like environment variables and the command to start our application. In this case, we’re overriding the `NODE_ENV` variable to `development`, as well as the start script to `npm run dev` . We can now run our app in a container with the command:

```
docker-compose up -d --build
```

Once completed, you’ll be able to access your app at `localhost:5173`. However, any changes that you make to your code won’t be reflected unless you were to rebuild the image! That’s no good for local development, so let’s fix that.

## Making local changes available

We’ll be using a feature of Docker called **bind mounts** to allow our code changes to pass through to the container. To do this, we’ll be setting the top-level `volumes` key in our Docker Compose settings like so:

```
version: "3.8" # this the version of Docker Compose
services:
  app:
    build:
      context: ./ 
    command: npm run dev
    environment:
      - NODE_ENV=development
    ports:
      - '5173:5173'
    volumes:
      - ${APP_DIR}:/app
      - /app/node_modules
```

This `volumes` key in Docker Compose configures both **bind mounts** and **volumes.**

Bind mounts follow the pattern `</local/path>:</path/in/docker/image>`, which tells Docker to use the files from the underlying filesystem *instead* of what’s bundled in the image.

You can think of bind mounts as a way to “cut a hole” in the Docker image to allow the underlying files on your computer to pass through.

In our case, `${APP_DIR}:/app` allows us to make changes to our app code that will be reflected in our running container. You can set `APP_DIR` in a `.env` file, and it should be the *absolute path* to your application directory.

### Using an anonymous volume for `node_modules`

By using a bind mount, we’re now letting our whole application codebase pass through to the container, and this includes `node_modules` , and that’s a problem, but it might not be obvious.

Some npm packages have *platform-specific* versions. For example, on macOS, `esbuild` will install the package `@esbuild/darwin-arm64`, but on Linux systems, `@esbuild/linux-arm64` will be required. For this reason, **we should not rely on the underlying filesystem for `node_modules`.**

So, how do we tell Docker to exclude *only* this folder from our bind mount? Enter **anonymous volumes** .

Let’s look again at this section of our `docker-compose.yml`:

```
    volumes:
      # bind mount 👇
      - ${APP_DIR}:/app
      # anonymous volume 👇
      - /app/node_modules
```

While bind mounts are controlled by the *host machine* , volumes are controlled by *Docker* . In both cases, a parallel folder exists in the host filesystem, but in the case of volumes, instead of the *host* dictating what goes in it, *Docker* controls what goes in.

If we look in our Dockerfile, we’ll see that it runs `npm ci --include-dev` (basically `npm install`), and those dependencies get placed inside the directory `/app/node_modules` in our image . By adding `- /app/node_modules` as a volume, we’re letting Docker override the contents of our local `node_modules` folder in the image, thus avoiding the problem. In other words, we’re saying “Use the underlying host files for everything *except* `node_modules`.”

With our bind mount and volume set up, we can now run `docker-compose up -d --build`, and any changes to our code will be reflected in our container.

### Live reload changes on non-Vite Remix apps

As stated earlier, the default templates for Remix now use Vite, which don’t require any extra setup for live reloading to kick in. However, if *not* using Vite, and your application normally runs on port `3000`, you can add a second `port` entry to include `3001` like so:

```
    # ...
    ports:
      - '3000:3000'
      - '3001:3001'
    #...
```

For non-Vite Remix apps, live reloading operates through a web socket connection on port `3001`. With this extra port exposed in our configuration, your changes will be available instantly in your container.

## Conclusion

Leveraging Docker Compose for local development can give you the confidence that what you build locally will be reflected in production when you deploy your app. Docker gives you the most control over the environment in which your app runs, and Fly.io makes deploying containerized applications easy.
