Deploying an Expo Web Application with Kamal
2025-03-09 19:10:41 -0300
Background
At Switch Dreams, a client approached us with the need to rebuild their website to improves the UI/UX, security and scalability. We decided to use Expo for the frontend, allowing a unified development experience across Android, iOS, and web. For the backend, we chose Rails, leveraging its reliability and productivity to streamline development while ensuring a solid foundation for future growth.
Everything was running smoothly until we reached the point of deploying the Expo web application in a way that aligned with our stack. While we could have opted for a static file hosting service, we chose Kamal to ensure better compatibility with our deployment workflow and streamline everything within a single pipeline.
The lack of resources on deploying a static website with Expo—especially using Kamal—motivated me to share this article.
Steps Overview
- Dockerfile setup
- Health Check workaround
- Kamal configuration
Dockerfile
First, we need to understand the three Expo export options, as they significantly impact the Dockerfile structure. For Expo (SDK 50+), we can build the web app using three different output modes:
Expo Documentation - Publishing Websites
- Single: The default option that exports the application as a standard SPA.
- Server: Generates both client (HTML) and server files for a Node.js custom server. This option is unnecessary for our use case since we only need a client-side application and do not use API routes.
- Static: Exports HTML for every route in the app directory. This is better for SEO but is not ideal for a private app. For now, SPA is the best choice.
Choosing the Serving Method
After selecting the Single output mode, we had to decide how to serve the static files. We opted for the most common approach: Nginx.
Dockerfile Configuration
FROM node:20.17.0 AS build
WORKDIR /app
ENV EXPO_PUBLIC_API_URL="API_URL"
ENV PORT=80
# Copy package files
COPY package.json yarn.lock ./
# Install dependencies
RUN yarn install
COPY . .
# Fix a known issue with NativeWind
RUN yarn tailwindcss -i ./src/global.css -o ./node_modules/.cache/nativewind/global.css.web.css && npx expo export -p web
FROM nginx:stable-alpine
WORKDIR /app
RUN rm -rf /usr/share/nginx/html
COPY --from=build /app/dist /usr/share/nginx/html
COPY config/nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Nginx Configuration
A key part of this setup is configuring Nginx properly. One critical fix was ensuring static files were served with the
correct MIME types. Write this config inside: /config/nginx.conf
events {
worker_connections 1024;
}
http {
server {
listen 80;
server_name your-host;
root /usr/share/nginx/html;
index index.html;
# Serve static files from Expo under the correct path
location /_expo/static/ {
include /etc/nginx/mime.types;
alias /usr/share/nginx/html/_expo/static/;
}
# Serve asset files
location /assets/ {
include /etc/nginx/mime.types;
alias /usr/share/nginx/html/assets/;
}
location / {
try_files $uri $uri/ /index.html;
}
}
}
Health Check Workaround
As of Kamal version 2.3, health checks cannot be disabled. Since Kamal was initially designed for full-stack applications, it assumes that every app should have a health check.
For a client-side application, this requirement is often unnecessary. To work around this, we created a simple up
page inside the app router.
// src/app/up.jsx
import {View} from "react-native";
const UpPage = () => <View className="flex-1 bg-green-500"/>;
export default UpPage;
Another possible solution would be configuring the health check inside the Nginx setup.
Kamal Configuration
The Kamal configuration follows the standard setup. Below is a complete example:
# config/deploy.yml
# Name of your application.
service: yourapp
# Container image.
image: switchdreams/yourapp
# Deploy to these servers.
servers:
web:
- SERVER_IP
# Enable SSL via Let's Encrypt.
proxy:
ssl: true
host: your-host
# kamal-proxy connects to your container over port 80.
# app_port: 3000
# Registry credentials.
registry:
username: your-registry-username
password:
- KAMAL_REGISTRY_PASSWORD
# Builder setup.
builder:
arch: amd64
# Environment variables.
env:
clear:
EXPO_PUBLIC_API_URL: your-host
NODE_ENV: production
PORT: 80
# SSH configuration.
ssh:
user: root
keys: [ "path_to_your_key" ]
Conclusion
By leveraging Kamal for deployment, we streamlined our process, kept our infrastructure cohesive, and gained greater control over our stack. Hopefully, this guide helps others facing similar challenges deploy their Expo Web applications more efficiently.