Blog Tutorial: Deployment
In this final chapter, we'll prepare our blog for production and deploy it.
Production Checklist
Before deploying, let's ensure our app is production-ready:
1. Environment Variables
Create .env.production:
NODE_ENV=production
DATABASE_URL=./data/blog.dbUpdate your code to use environment variables:
// src/lib/db.ts
import Database from 'better-sqlite3'
const dbPath = process.env.DATABASE_URL || 'blog.db'
const db = new Database(dbPath)
// ... rest of the file2. Security Headers
Add security middleware. Create src/middleware/security.ts:
import type { MiddlewareHandler } from '@ereo/core'
export const securityMiddleware: MiddlewareHandler = async (request, context, next) => {
const response = await next()
// Add security headers
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-XSS-Protection', '1; mode=block')
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
return response
}3. Error Pages
Create a root error page at app/routes/_error.tsx. The simplest approach receives the error as a prop:
// app/routes/_error.tsx
export default function RootError({ error }: { error: Error }) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Something went wrong
</h1>
<p className="text-gray-600 mb-8">
{error?.message || 'An unexpected error occurred.'}
</p>
<a href="/" className="btn">Go Home</a>
</div>
</div>
)
}For more advanced error handling (e.g., distinguishing 404s from 500s), you can use the useRouteError hook:
// app/routes/_error.tsx
import { useRouteError, isRouteErrorResponse } from '@ereo/client'
export default function RootError() {
const error = useRouteError()
if (isRouteErrorResponse(error)) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-6xl font-bold text-gray-900 mb-4">
{error.status}
</h1>
<p className="text-xl text-gray-600 mb-8">
{error.status === 404 ? 'Page not found' : error.statusText}
</p>
<a href="/" className="btn">Go Home</a>
</div>
</div>
)
}
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Something went wrong
</h1>
<p className="text-gray-600 mb-8">
We're sorry, an unexpected error occurred.
</p>
<a href="/" className="btn">Go Home</a>
</div>
</div>
)
}4. Meta Tags
Add SEO meta tags. Update app/routes/_layout.tsx:
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="A blog about web development and EreoJS" />
<meta property="og:title" content="My Blog" />
<meta property="og:description" content="A blog about web development" />
<meta property="og:type" content="website" />
<link rel="icon" href="/favicon.ico" />
<title>My Blog</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body className="bg-gray-50 text-gray-900 min-h-screen">
{/* ... rest of layout */}
</body>
</html>
)
}Build for Production
# Build CSS and application
bun run buildThis creates an optimized build in dist/.
Deployment Options
Option 1: Self-Hosted with Bun
The simplest deployment is running Bun directly:
# On your server
bun install --production
bun run build
bun ereo startUse a process manager like PM2:
pm2 start "bun ereo start" --name blogOption 2: Docker
Create a Dockerfile:
FROM oven/bun:1
WORKDIR /app
# Install dependencies
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
# Copy source
COPY . .
# Build
RUN bun run build
# Expose port
EXPOSE 3000
# Start
CMD ["bun", "ereo", "start"]Build and run:
docker build -t my-blog .
docker run -p 3000:3000 my-blogOption 3: Docker Compose
Create docker-compose.yml:
version: '3.8'
services:
blog:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
volumes:
- ./data:/app/data
restart: unless-stoppedRun:
docker-compose up -dOption 4: Fly.io
Create fly.toml:
app = "my-blog"
primary_region = "sjc"
[build]
dockerfile = "Dockerfile"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
[mounts]
source = "data"
destination = "/app/data"Deploy:
fly launch
fly deployAdd a Health Check
Create app/routes/api/health.ts:
export function GET() {
return Response.json({
status: 'ok',
timestamp: new Date().toISOString()
})
}Set Up Reverse Proxy (Nginx)
If using Nginx as a reverse proxy:
server {
listen 80;
server_name blog.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name blog.example.com;
ssl_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}Monitoring
Add basic logging:
// src/middleware/logger.ts
import type { MiddlewareHandler } from '@ereo/core'
export const loggerMiddleware: MiddlewareHandler = async (request, context, next) => {
const start = Date.now()
const response = await next()
const duration = Date.now() - start
console.log(JSON.stringify({
method: request.method,
url: request.url,
status: response.status,
duration: `${duration}ms`,
timestamp: new Date().toISOString()
}))
return response
}Final Project Structure
blog/
├── src/
│ ├── islands/
│ │ └── LikeButton.tsx
│ ├── lib/
│ │ └── db.ts
│ ├── middleware/
│ │ ├── logger.ts
│ │ └── security.ts
│ ├── routes/
│ │ ├── api/
│ │ │ ├── health.ts
│ │ │ └── posts/
│ │ │ └── [id]/
│ │ │ └── like.ts
│ │ ├── posts/
│ │ │ ├── _error.tsx
│ │ │ ├── index.tsx
│ │ │ ├── new.tsx
│ │ │ ├── [slug].tsx
│ │ │ └── [slug]/
│ │ │ └── edit.tsx
│ │ ├── _error.tsx
│ │ ├── _layout.tsx
│ │ └── index.tsx
│ ├── client.ts
│ └── index.ts
├── public/
│ ├── favicon.ico
│ └── styles.css
├── data/
│ └── blog.db
├── .env
├── .env.production
├── Dockerfile
├── docker-compose.yml
├── ereo.config.ts
├── package.json
├── tailwind.config.js
└── tsconfig.jsonCongratulations!
You've built a complete blog application with EreoJS! You've learned:
- Project setup - Creating and configuring an EreoJS project
- File-based routing - Pages, dynamic routes, layouts
- Data loading - Loaders for fetching data
- Form handling - Actions for mutations, validation, error handling
- Islands - Interactive components with selective hydration
- Styling - Tailwind CSS integration
- Deployment - Production builds and hosting
Next Steps
- Add user authentication
- Implement search functionality
- Add categories and tags
- Set up a proper database (PostgreSQL, etc.)
- Add image uploads
- Implement RSS feed