You are going to deploy a Hono application to a VPS:
- Hono on Bun (fastest) or Node.js (most compatible)
- minimal Docker image
- HTTPS with automatic SSL
- ready for API workloads
Hono is ultralight (~14KB) and fast. Combined with Bun, you get sub-millisecond routing and minimal resource usage.
What you will have at the end
https://api.your-domain.comserving your Hono API- Docker image under 100MB
- A clean deploy workflow
Step 1: Set up your Hono project
If starting fresh:
bun create hono my-api
cd my-api
Your src/index.ts should export or run a server:
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
const app = new Hono()
app.use('*', logger())
app.use('*', cors())
app.get('/', (c) => c.json({ message: 'Hello from Hono!' }))
app.get('/health', (c) => c.json({ status: 'ok' }))
app.get('/api/users', (c) => {
return c.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
])
})
export default {
port: process.env.PORT || 3000,
fetch: app.fetch,
}
Update package.json:
{
"scripts": {
"dev": "bun run --hot src/index.ts",
"start": "bun run src/index.ts"
}
}
Step 2: Create the Dockerfile (Bun runtime)
FROM oven/bun:1-alpine AS deps
WORKDIR /app
COPY package.json bun.lockb* ./
RUN bun install --frozen-lockfile
FROM oven/bun:1-alpine
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
ENV PORT=3000
USER bun
EXPOSE 3000
CMD ["bun", "run", "src/index.ts"]
Alternative: Node.js runtime (if you need Node.js compatibility):
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
ENV PORT=3000
USER node
EXPOSE 3000
CMD ["node", "--import", "tsx", "src/index.ts"]
For Node.js, add tsx as a dependency or compile TypeScript first.
Step 3: Set up the VPS directory
mkdir -p ~/apps/hono-api
cd ~/apps/hono-api
git clone https://github.com/you/your-hono-api.git .
Step 4: Create docker-compose.yml
services:
traefik:
image: traefik:v2.11
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --certificatesresolvers.letsencrypt.acme.tlschallenge=true
- [email protected]
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- letsencrypt:/letsencrypt
restart: unless-stopped
api:
build: .
restart: unless-stopped
environment:
- NODE_ENV=production
- PORT=3000
- DATABASE_URL=${DATABASE_URL:-}
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
labels:
- traefik.enable=true
- traefik.http.routers.api.rule=Host(`api.your-domain.com`)
- traefik.http.routers.api.entrypoints=websecure
- traefik.http.routers.api.tls=true
- traefik.http.routers.api.tls.certresolver=letsencrypt
- traefik.http.services.api.loadbalancer.server.port=3000
volumes:
letsencrypt:
Replace api.your-domain.com with your domain.
Step 5: Deploy
docker compose up -d --build
Test your API:
curl https://api.your-domain.com/
curl https://api.your-domain.com/api/users
curl https://api.your-domain.com/health
Step 6: Add rate limiting and security
Hono has built-in middleware. Update your app:
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { secureHeaders } from 'hono/secure-headers'
import { rateLimiter } from 'hono-rate-limiter'
const app = new Hono()
// Security headers
app.use('*', secureHeaders())
// Logging
app.use('*', logger())
// CORS - configure for your frontend
app.use('*', cors({
origin: ['https://your-domain.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
}))
// Rate limiting
const limiter = rateLimiter({
windowMs: 60 * 1000, // 1 minute
limit: 100, // 100 requests per minute
keyGenerator: (c) => c.req.header('x-forwarded-for') || 'unknown',
})
app.use('/api/*', limiter)
// Routes
app.get('/health', (c) => c.json({ status: 'ok' }))
// ... rest of your routes
Update workflow:
git pull
docker compose up -d --build
Troubleshooting
Import errors with Bun
Hono works natively with Bun, but check your imports:
// Correct for Bun
import { Hono } from 'hono'
// If using node_modules resolution
// Make sure bun.lockb exists
bun install
CORS errors from frontend
Your CORS config must match your frontend domain exactly:
app.use('*', cors({
origin: ['https://your-frontend.com', 'http://localhost:3001'],
credentials: true,
}))
502 Bad Gateway
Hono must bind to the correct host. When using the default export pattern:
// This works
export default {
port: 3000,
fetch: app.fetch,
}
// Or explicitly
Bun.serve({
port: 3000,
hostname: '0.0.0.0',
fetch: app.fetch,
})
Memory leaks in production
Check for unclosed connections or growing data structures:
docker stats
# Monitor over time
watch -n 5 'docker stats --no-stream'
Hono itself is stateless. Leaks usually come from your code (database connections, caching, etc.).
Internal links (recommended next reads)
- Tutorial: Deploy Bun App - Bun fundamentals
- Tutorial: Deploy Docker Compose - add a database
- Tutorial: Deploy Next.js - full-stack alternative
- Comparison: Vercel Alternatives
- ServerCompass: Deploy Hono APIs
From across the StoicSoft network
Hand-curated reads on the same topic from sister sites in the StoicSoft family.

