Step-by-Step Guide to Dockerizing Dart and Flutter Web for Deployment

Step-by-Step Guide to Dockerizing Dart and Flutter Web for Deployment

Introduction

Welcome to part two of my series on deploying Dockerized Dart containers on Ubuntu VPS. This series is designed to guide you through every step of the process, from selecting the most cost-effective infrastructure to creating Docker images and setting up your server for deployment. By the end of it, you'll have the knowledge and confidence to deploy robust Dart & Flutter applications in a production environment.

In this article, I’ll focus on Dockerizing your Dart & Flutter code so it can be deployed anywhere.

So, what is Docker?

It’s a platform that allows developers to package applications and their dependencies into containers. These containers run consistently across different environments, making deployment and scaling straightforward.

In this article, we'll explore how to dockerize Dart and Flutter web applications using various frameworks and tools such as Shelf, DartFrog, Jaspr, and Flutter Web.

Let’s dive in!

If you don’t have docker installed, check out the official install instructions.

Docker Crash Course

Docker containers run images, which are prepackaged applications with dependencies. To create a Docker image, we have to use a Dockerfile. You can add one to your repo by executing docker init.

Dockerfile is used to describe the process of building an app and fetching dependencies needed for it to run. In essence, it will almost always follow a similar structure:

  1. Select the base image image (the build environment)

  2. Get all the dependencies required for building

  3. Build the app

  4. Create a clean image

  5. Copy the app and the dependencies required for running

  6. Run the app.

Docker uses a few essential commands for managing containers and images:

  • docker build: Builds a Docker image from a Dockerfile.

  • docker run: Runs a container from a Docker image.

  • docker pull: Downloads a Docker image from a repository.

  • docker push: Uploads a Docker image to a repository.

  • docker ps: Lists running containers.

  • docker stop: Stops a running container.

  • docker rm: Removes a stopped container.

These commands help you create, run, and manage Docker containers, ensuring your applications are portable and consistent.

Now, let’s learn how to create a Dockerfile for Dart backend frameworks.

Dart Server

There are many Dart backend frameworks available, but I’ll use Shelf and DartFrog (which relies on Shelf) to demonstrate the basics because they are minimalistic and don’t have too many additional things to keep in mind.

Shelf

A Dockerfile for Shelf will typically look like this:

# Use latest stable channel SDK.
FROM dart:stable AS build

# Resolve app dependencies.
WORKDIR /app
COPY pubspec.* ./
RUN dart pub get

# Copy app source code (except anything in .dockerignore) and AOT compile app.
COPY . .
RUN dart pub get --offline
RUN dart compile exe bin/server.dart -o bin/server

# Build minimal serving image from AOT-compiled `/server`
# and the pre-built AOT-runtime in the `/runtime/` directory of the base image.
FROM scratch
COPY --from=build /runtime/ /
COPY --from=build /app/bin/server /app/bin/

# Start server.
EXPOSE 8080
CMD ["/app/bin/server"]

This probably seems intimidating at first, but it’s actually not that complicated. This resembles the process you would manually have to do to build a Shelf backend.

Let’s go through commands one by one and explain what is going on:

  • FROM dart:stable as build: This tells Docker to use the dart with the stable version as a base image. This image is actually minimal Debian with Dart preinstalled.

  • WORKDIR /app: Sets the working directory inside the container to /app.

  • COPY . .: Copies all files from the current directory to the container's /app directory. This is probably the most confusing part. When you are building a docker image, Docker has access to your current directory. This can be very useful if you are using CI/CD because a simple git clone will give you access to the code.

  • RUN dart pub get --offline: Installs the Dart dependencies.

  • RUN dart compile exe bin/server.dart -o bin/server: Compiles the server.

  • FROM scratch: Starts with a minimal image for the final stage. This is used so we can start with a clean image to minimize the final size.

  • COPY --from=build /runtime/ /: Copies runtime files from the build stage.

  • COPY --from=build /app/build/bin/server /app/bin/server: Copies the compiled server.

  • EXPOSE 8080: Exposes port 8080 to the host.

  • CMD ["dart", "bin/server.dart"]: Defines the command to run the Dart server.

These steps ensure the application and its dependencies are correctly set up and the server is started.

DartFrog

DartFrog is made by VeryGoodVentures and uses Shelf as a base. It adds all the basic features you might need without compromising too much on flexibility.

Dockerfile setup looks very familiar to Shelf, so I’ll just explain the parts that are different.

# This stage will compile sources to get the build folder
FROM dart:stable AS build

# Install the dart_frog cli from pub.dev
RUN dart pub global activate dart_frog_cli

# Set the working directory
WORKDIR /app

# Copy Dependencies in our working directory
COPY pubspec.* /app/
COPY routes /app/routes
# Uncomment the following line if you are serving static files.
# COPY --from=build public /app/public

# Add all of your custom directories here, for example if you have a "models" directory:
# COPY models /app/models

# Get dependencies
RUN dart pub get

# 📦 Create a production build
RUN dart_frog build

# Compile the server to get the executable
RUN dart compile exe /app/build/bin/server.dart -o /app/build/bin/server

# Build minimal serving image from AOT-compiled `/server` and required system
# libraries and configuration files stored in `/runtime/` from the build stage.
FROM scratch

COPY --from=build /runtime/ /
COPY --from=build /app/build/bin/server /app/bin/server
# Uncomment the following line if you are serving static files.
# COPY --from=build /app/build/public /public/

# Expose the server port (useful for binding)
EXPOSE 8080

# Run the server
CMD ["/app/bin/server"]

The difference is in commands:

  • RUN dart pub global activate dart_frog_cli: Installs the DartFrog CLI globally.

  • COPY routes /app/routes: Copies the routes to the container. DartFrog uses file-system routing.

  • RUN dart_frog build: Builds the DartFrog application.

Deploying Shelf and DartFrog is super easy, as is for any other backend that relies on pure Dart code like Pharaoh, Vania, Serinus, and Gazzele.

Serverpod is a different story because it also manages a Postgres database and Redis, so it uses Terraform for deployment of the whole environment. Serverpod Mini follows the same principles as the rest.

Jaspr

Jaspr is a modern web framework for building websites in Dart with support for both client-side and server-side rendering. For the purposes of this article, we will be looking at how to deploy the server-side rendering Jaspr app.

FROM dart:stable as build

RUN dart pub global activate jaspr_cli

WORKDIR /app
COPY . .

# Resolve app dependencies.
RUN rm -f pubspec_overrides.yaml
RUN dart pub get

# Build project
RUN dart pub global run jaspr_cli:jaspr build --verbose

FROM scratch

COPY --from=build /runtime/ /
COPY --from=build /app/build/jaspr/ /app/

WORKDIR /app

# Start server.
EXPOSE 8080
CMD ["./app"]

Since Jaspr is based on Dart only, there are once again, only a handful of differences to the Dockerfile Shelf uses:

  • RUN dart pub global activate jaspr_cli: Installs the Jaspr CLI globally.

  • RUN dart pub global run jaspr_cli:jaspr build --verbose: Builds the Jaspr project.

  • COPY --from=build /app/build/jaspr/ /app/: Copies the built Jaspr files.

Your Jaspr website should now be ready for users.

Flutter Web

Android and iOS have standard formats of binaries and Flutter provides commands for building them. Flutter Web is a special case here since it needs to adhere to the web standard. Once again, Docker will help us streamline this process.

There are multiple ways to deploy Flutter web, but running an Ubuntu or Debian base for building and then installing Flutter and building the web app seems to be reliable.

FROM debian:latest AS build-env

RUN apt-get update
# Install necessary dependencies for running Flutter on web
RUN apt-get install -y libxi6 libgtk-3-0 libxrender1 libxtst6 libxslt1.1 curl git wget unzip libgconf-2-4 gdb libstdc++6 libglu1-mesa fonts-droid-fallback lib32stdc++6 python3
RUN apt-get clean

RUN git clone <https://github.com/flutter/flutter.git> /usr/local/flutter

# Set Flutter path
ENV PATH="/usr/local/flutter/bin:/usr/local/flutter/bin/cache/dart-sdk/bin:${PATH}"

RUN flutter doctor -v
RUN flutter channel master
RUN flutter upgrade

# Enable web support
RUN flutter config --enable-web

RUN mkdir /app/
COPY . /app/
# Set the working directory inside the container
WORKDIR /app/

# Build the Flutter web application
RUN flutter build web --release --web-renderer html

FROM nginx:1.21.1-alpine
COPY --from=build-env /app/build/web /usr/share/nginx/html

# EXPOSE <EXPOSE PORT THAT YOU WANT>
EXPOSE 80

Here's a line-by-line explanation of what is going on:

  • FROM debian AS build-env: Starts with the latest Debian base image and names this stage build-env. This stage will be used to compile the Flutter application.

  • RUN apt-get update , RUN apt-get install -y [packages] , RUN apt-get clean : Standard Linux commands to update the packages, install new ones, and clean up afterward.

  • RUN git clone <https://github.com/flutter/flutter.git> /usr/local/flutter: Clones the Flutter SDK from its GitHub repository into the /usr/local/flutter directory.

  • ENV PATH="/usr/local/flutter/bin:/usr/local/flutter/bin/cache/dart-sdk/bin:${PATH}": Sets the environment variable PATH to include the Flutter and Dart SDK binaries, making Flutter and Dart commands available in the terminal.

  • RUN flutter doctor -v: Runs flutter doctor to verify the Flutter installation and dependencies, using the v flag for verbose output. If building the image fails because Flutter is missing dependencies, this will help us fix the image.

  • RUN flutter channel master: Switches Flutter to the master channel, which includes the latest changes and features. You could use use channel stable if you want the latest stable version.

  • RUN flutter upgrade: Upgrades Flutter to the latest version available in the specified channel.

  • RUN flutter config --enable-web: Enables Flutter's web support.

  • RUN mkdir /app/: Creates a directory named /app where the Flutter application will be copied.

  • COPY . /app/: Copies all files from the current directory on the host machine to the /app directory in the Docker image.

  • WORKDIR /app/: Sets /app as the working directory. All subsequent commands will be run from this directory.

  • RUN flutter build web --release --web-renderer html: Builds the Flutter web application in release mode using the HTML renderer. This generates optimized files for deployment.

  • FROM nginx:1.21.1-alpine: Starts with the Nginx 1.21.1 image based on Alpine Linux. This lightweight image will serve the built Flutter web application.

  • COPY --from=build-env /app/build/web /usr/share/nginx/html: Copies the built Flutter web application from the build stage (build-env) to the Nginx HTML directory where it will be served.

  • EXPOSE 80: Exposes port 80, the default HTTP port, so the application can be accessed via a web browser.

This is, of course, not the only way of doing it, and you can also not use the HTML renderer. In that case, your image would have to expose and script executable like in this example.

Another way of deploying Flutter Web is by using -d web-server option. This approach deserves its own explanation so I’ll just leave the article about it here.

Conclusion

Dockerizing Dart and Flutter web applications ensures consistent deployment and easy scalability across various environments. By encapsulating your applications and their dependencies in Docker containers, you achieve high portability and reliability.

This article covered the steps to Dockerize Dart and Flutter web applications using frameworks like Shelf, DartFrog, Jaspr, and Flutter web, providing clear Dockerfile examples and explanations.

The last article in the series will cover building the image, pushing it to the repository, and then setting up the VPS for deployment.

If you have found this useful, make sure to like and follow for more content like this. To know when the new articles are coming out, follow me on Twitter or LinkedIn.