Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back.
In our previous lesson, we explored deploying a Tanstack Start application to a serverless platform, Vercel. We saw how platforms like Vercel abstract away infrastructure management, offering automatic scaling and high performance through Edge functions.
Today, we'll explore a different, but equally powerful, deployment paradigm: containerization. Our learning outcome is to create a production-ready Dockerfile for a Tanstack Start application using the Bun runtime. This lesson will equip you with the skills to package your application into a portable, consistent, and efficient container, contrasting the "managed" serverless approach with the control and flexibility of Docker.
1. Why Containerize? Docker vs. Serverless
Before we write our Dockerfile, let's briefly touch on the "why." While serverless platforms like Vercel are excellent for their simplicity and auto-scaling, containerization with Docker offers a different set of advantages:
- Portability: A Docker container packages your application and all its dependencies into a single unit that can run anywhere Docker is installed—on your local machine, a cloud VM, or a container orchestration platform.
- Consistency: It guarantees that the environment your application runs in is identical from development to production, eliminating "it works on my machine" problems.
- Control: You have fine-grained control over the operating system, system libraries, and runtime environment.
For a Tanstack Start application, this means you can create a highly optimized, self-contained server ready for deployment on a wide variety of hosting providers.
2. Principles of a Production-Ready Dockerfile
Creating a Dockerfile is easy, but creating a good one requires adhering to best practices that optimize for size, build speed, and security.
To get a solid overview of these principles, let's watch a video that covers the most important techniques.
Docker Image BEST Practices - From 1.2GB to 10MB
The video 'Docker Image BEST Practices' from Better Stack provides a fantastic, concise overview of how to drastically reduce image size and improve build times. The concepts apply to any application, including ours.
Please watch the following segments. Focus on the core idea behind each practice: Choosing a Minimal Base Image (00:35 - 02:04): Understand why starting with a small image like alpine is crucial. Leveraging Docker Layer Caching (02:04 - 03:10): Pay attention to how the order of commands in your Dockerfile impacts rebuild speed. Using .dockerignore (03:10 - 04:02): See how this file prevents unnecessary files from being included in your image. Implementing Multi-Stage Builds (05:26 - 06:29): This is the most critical technique for production images. Understand how it separates build-time dependencies from the final runtime image.
In addition to the practices from the video, there's one more critical security principle for production environments: running your container as a non-root user.
Top 8 Docker Best Practices for using Docker in Production
The video 'Top 8 Docker Best Practices' by TechWorld with Nana reinforces the concepts we just saw and adds a crucial point about security.
Please watch the section on Run Containers with Non-Root Users (14:08 - 16:18). This explains why running as root is a security risk and how to avoid it.
These practices can be summarized visually.
3. Creating the Dockerfile for Tanstack Start with Bun
Now, let's apply these principles to create a Dockerfile specifically for our Tanstack Start application running on Bun. We will use a multi-stage build to create a lean, secure, and efficient final image.
The official Bun documentation provides an excellent template for a multi-stage Dockerfile, which we will adapt for the specific needs of a Tanstack Start project.
Containerize a Bun application with Docker
The official Bun documentation, 'Containerize a Bun application with Docker', provides a solid, multi-stage Dockerfile that we can use as our foundation. It demonstrates excellent practices for dependency management.
First, review the Dockerfile example provided at the top of the page. Notice its multi-stage structure (base, install, prerelease, release). Then, look at the .dockerignore file example. We will combine these patterns with the specifics of a Tanstack Start build.
A standard Tanstack Start build (tanstack-start build) produces a dist directory containing client and server subdirectories. For production, we can't just run the source code; we need a dedicated server entrypoint that serves the client assets and wires up the server-side rendering handler.
A community discussion on the TanStack Discord, archived on AnswerOverflow, provides the exact Dockerfile structure and server entrypoint needed for this.
Serving in production via Bun - TanStack
This discussion, 'Serving in production via Bun', contains a production-ready, multi-stage Dockerfile specifically for a Tanstack Start application. It's a practical example from the community that directly addresses our goal.
Please review the following three pieces of information from this discussion: The Dockerfile: Find the comment by user deep-jade (it starts with # 1. Base image). This is the core Dockerfile we will analyze. The Server Build Command: In the reply to that comment, note the build:server script. This shows how the server entrypoint is compiled. The Server Code: Find the code block from user ratty-blush that starts with // server.js. This shows the simplest way to create a production server using srvx.
4. The Complete Production Setup
By combining the best practices from the videos and the specific examples from the Bun docs and the Tanstack community, we can assemble a complete, production-ready setup.
Step 1: The Production Server Entrypoint
Create a file named server.ts in the root of your project. This script will act as the entrypoint for your production server inside the Docker container. It uses srvx, a lightweight server utility that is already a dependency of Tanstack Start.
// server.ts
import { serve } from 'srvx';
import { serveStatic } from 'srvx/static';
import server from './dist/server/server.js';
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
console.log(`Server starting on port ${PORT}...`);
serve({
port: PORT,
fetch: server.fetch,
middleware: [
// Serve static files from the 'dist/client' directory
serveStatic({
dir: 'dist/client',
}),
],
});
Step 2: Update package.json
Modify your package.json to include scripts for building the application and the custom server entrypoint.
{
"scripts": {
"dev": "tanstack-start dev",
"build:start": "tanstack-start build",
"build:server": "bun build ./server.ts --outfile ./dist/server.js --target bun --minify",
"build": "npm run build:start && npm run build:server",
"start": "bun run ./dist/server.js"
}
}
build:start: Runs the standard Tanstack Start build.build:server: Compiles ourserver.tsinto a single JavaScript file.build: A convenience script that runs both build steps in sequence.start: The command to run the production server. This is what Docker will use.
Step 3: The .dockerignore File
Create a .dockerignore file in your project root to prevent unnecessary files from being copied into the build context, which speeds up builds and reduces image size.
# .dockerignore
# Ignore node_modules, it will be installed inside the container
node_modules
# Ignore build output
dist
# Ignore local environment files and Docker artifacts
.env*
Dockerfile*
docker-compose*
.dockerignore
# Ignore Git and IDE/editor configuration
.git
.gitignore
.vscode
.idea
# Other common files to ignore
README.md
Step 4: The Final Dockerfile
Here is the complete, multi-stage Dockerfile that integrates all these pieces.
# Dockerfile
# ---- Base Stage ----
# Use the official Bun image. The -slim variant is smaller.
FROM oven/bun:1-slim AS base
WORKDIR /app
# ---- Install Stage ----
# Install ALL dependencies needed for building and checking the app.
FROM base AS install
COPY package.json bun.lockb ./
# Use --frozen-lockfile for reproducible installs
RUN bun install --frozen-lockfile
# ---- Build Stage ----
# Build the application.
FROM install AS build
COPY . .
# The 'build' script in package.json runs both the Tanstack and server builds
RUN bun run build
# ---- Release Stage ----
# The final, lean production image.
FROM base AS release
WORKDIR /app
ENV NODE_ENV=production
# Install ONLY production dependencies.
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
# Copy the build artifacts from the 'build' stage.
COPY --from=build /app/dist ./dist
# The official Bun images include a non-root 'bun' user.
USER bun
# Expose the port the app runs on.
EXPOSE 3000
# The command to run the application.
CMD ["bun", "run", "start"]
Analysis of the Dockerfile:
base: We start withoven/bun:1-slim, a minimal official image.install: This stage is dedicated to installing all dependencies. By copying onlypackage.jsonandbun.lockbfirst, we leverage Docker's layer caching. This layer will only be rebuilt if our dependencies change.build: This stage copies the entire source code and runsbun run build. This executes the full build process, creating thedistdirectory with both client and server assets.release: This is our final, optimized image.- It starts from the clean
baseimage. - It installs only production dependencies, resulting in a smaller
node_modulesfolder. - It uses
COPY --from=buildto copy only thedistfolder from the build stage, leaving behind all source code and development dependencies. USER bunswitches to a non-root user for enhanced security.CMD ["bun", "run", "start"]executes the production server using the script we defined inpackage.json.
- It starts from the clean
Conclusion
Today we constructed a complete, production-grade containerization strategy for a Tanstack Start application using Bun and Docker.
Key Takeaways:
- Multi-stage builds are essential for creating small and secure production images by separating build-time concerns from runtime necessities.
- Layer caching is key to fast and efficient image rebuilds. Structure your
Dockerfilefrom least-to-most-frequently-changing instructions. - A custom server entrypoint is required to serve a production Tanstack Start build, and
srvxprovides a simple, built-in solution. - Running as a non-root user (
USER bun) is a critical security best practice. - This Docker-based approach provides portability and consistency, allowing you to deploy your application to any platform that supports containers.
In our next lesson, we will complete the deployment lifecycle by learning how to deploy the containerized Bun application to a suitable hosting platform like Fly.io, putting our newly created Dockerfile to use.