Deployment
Deployment
Shovel applications can be deployed to various platforms. This guide covers production deployment patterns.
Building for Production
shovel build src/server.ts
This creates a dist/ directory with your bundled application:
dist/
├── server/
│ ├── worker.js # Bundled ServiceWorker
│ ├── server.js # Server entry point
│ └── package.json # Dependencies
└── public/ # Static assets
Node.js
Direct Execution
cd dist/server
node server.js
With Process Manager (PM2)
npm install -g pm2
cd dist/server
pm2 start server.js --name my-app -i max
PM2 ecosystem file (ecosystem.config.js):
module.exports = {
apps: [{
name: "my-app",
script: "server.js",
cwd: "./dist/server",
instances: "max",
exec_mode: "cluster",
env: {
NODE_ENV: "production",
PORT: 7777,
},
}],
};
Environment Variables
PORT=8080 HOST=0.0.0.0 node dist/server/server.js
Bun
Direct Execution
cd dist/server
bun server.js
With Multiple Workers
Configure workers in shovel.json:
{
"workers": 4
}
Or via environment variable:
WORKERS=4 bun dist/server/server.js
Docker
Dockerfile (Node.js)
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx shovel build src/server.ts
FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist/server ./
COPY --from=builder /app/dist/public ../public
ENV NODE_ENV=production
ENV PORT=7777
ENV HOST=0.0.0.0
EXPOSE 7777
CMD ["node", "server.js"]
Dockerfile (Bun)
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run shovel build src/server.ts
FROM oven/bun:1-slim
WORKDIR /app
COPY --from=builder /app/dist/server ./
COPY --from=builder /app/dist/public ../public
ENV NODE_ENV=production
ENV PORT=7777
ENV HOST=0.0.0.0
EXPOSE 7777
CMD ["bun", "server.js"]
Docker Compose
version: "3.8"
services:
app:
build: .
ports:
- "7777:7777"
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://user:pass@db:5432/myapp
depends_on:
- db
db:
image: postgres:15
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Cloudflare Workers
Configuration
Set platform in shovel.json:
{
"platform": "cloudflare"
}
Build
shovel build src/server.ts --platform cloudflare
Output structure:
dist/
├── server/
│ └── server.js # Single bundled worker
└── public/ # Static assets
wrangler.toml
name = "my-app"
main = "dist/server/server.js"
compatibility_date = "2024-01-01"
[site]
bucket = "./dist/public"
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxx"
[[r2_buckets]]
binding = "UPLOADS"
bucket_name = "my-uploads"
Deploy
npx wrangler deploy
Bindings
Configure Cloudflare bindings in shovel.json:
{
"databases": {
"main": {
"binding": "DB"
}
},
"directories": {
"uploads": {
"binding": "UPLOADS"
}
}
}
Reverse Proxy
Nginx
upstream shovel_app {
server 127.0.0.1:7777;
keepalive 64;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://shovel_app;
proxy_http_version 1.1;
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_set_header Connection "";
}
location /static/ {
alias /app/dist/public/;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Caddy
example.com {
reverse_proxy localhost:7777
handle /static/* {
root * /app/dist/public
file_server
header Cache-Control "public, max-age=31536000, immutable"
}
}
Health Checks
Add a health check endpoint:
router.route("/health").get(() => {
return Response.json({ ok: true });
});
router.route("/ready").get(async () => {
try {
// Check database connection
const db = databases.get("main");
await db.get`SELECT 1`;
return Response.json({ ok: true });
} catch {
return Response.json({ ok: false }, { status: 503 });
}
});
Docker Health Check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:7777/health || exit 1
Kubernetes
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
image: my-app:latest
livenessProbe:
httpGet:
path: /health
port: 7777
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 7777
initialDelaySeconds: 5
periodSeconds: 10
Environment Configuration
Production shovel.json
{
"port": "$PORT || 7777",
"host": "$HOST || 0.0.0.0",
"workers": "$WORKERS || 4",
"databases": {
"main": {
"module": "$PLATFORM === bun ? @b9g/zen/bun : @b9g/zen/better-sqlite3",
"url": "$DATABASE_URL"
}
},
"logging": {
"sinks": {
"console": {
"module": "@logtape/logtape",
"export": "getConsoleSink"
}
},
"loggers": [
{
"category": "app",
"level": "$NODE_ENV === production ? info : debug",
"sinks": ["console"]
}
]
}
}
Required Environment Variables
Document required variables for deployment:
# Required
DATABASE_URL=postgres://user:pass@host:5432/db
# Optional (with defaults)
PORT=7777
HOST=0.0.0.0
WORKERS=4
NODE_ENV=production
Graceful Shutdown
Shovel handles SIGINT and SIGTERM for graceful shutdown:
- Stop accepting new connections
- Wait for in-flight requests to complete
- Close database connections
- Exit cleanly
Configure shutdown timeout in your process manager or orchestrator.
See Also
- CLI - Build commands
- shovel.json - Configuration reference
- Databases - Database configuration
- Directories - File storage