DEPLOY, WITHOUT THE GUESSWORK

How to Deploy Hono to a VPS (Docker + HTTPS)

Deploy Hono APIs to a VPS with Bun or Node.js. Fast, lightweight, and perfect for JSON APIs and microservices.

How to Deploy Hono to a VPS (Docker + HTTPS)

You are going to deploy a Hono application to a VPS:

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

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)

From across the StoicSoft network

Hand-curated reads on the same topic from sister sites in the StoicSoft family.