Fresh, briefly
Fresh is Deno's full-stack web framework built around island architecture: pages render on the server and ship zero JavaScript by default, hydrating only interactive islands. It runs on Deno's built-in server, which makes it a clean fit for a long-running container. While Deno Deploy is the first-party host, deploying Fresh to a general container platform gives you a managed database next to it and full control of the runtime.
Step 1: A Fresh app primer
A standard Fresh project (deno run -A -r https://fresh.deno.dev) has a main.ts entrypoint and a dev.ts for development. The production entry serves the app:
// main.ts
import { start } from "$fresh/server.ts";
import manifest from "./fresh.gen.ts";
import config from "./fresh.config.ts";
await start(manifest, config);Fresh reads the PORT environment variable automatically, so it works with platform-injected ports without changes.
Step 2: Containerize with the official Deno image
FROM denoland/deno:2.1.4
WORKDIR /app
# Cache dependencies first for faster rebuilds
COPY deno.json deno.lock ./
RUN deno cache main.ts || true
COPY . .
RUN deno cache main.ts
EXPOSE 8000
CMD ["run", "-A", "main.ts"]Deno needs explicit permissions; -A grants all. For a tighter security posture, scope permissions: --allow-net --allow-env --allow-read. Caching main.ts at build time avoids re-downloading dependencies on every cold start.
Step 3: Wire a managed Postgres
Use the postgres Deno module (or deno-postgres) and read DATABASE_URL from the environment. On PandaStack, attaching a managed PostgreSQL injects it:
// db.ts
import postgres from "https://deno.land/x/postgresjs/mod.js";
export const sql = postgres(Deno.env.get("DATABASE_URL")!);Use it inside a route handler or island loader:
// routes/index.tsx
import { sql } from "../db.ts";
export const handler = {
async GET(_req, ctx) {
const rows = await sql`SELECT title FROM posts ORDER BY created_at DESC`;
return ctx.render({ posts: rows });
},
};Keep a single sql client to manage the connection pool against your tier's limit (50 on free).
Step 4: Deploy
Connect your Git repo. PandaStack builds with rootless BuildKit in a K8s Job pod, pushes the image to Artifact Registry, and Helm-deploys it:
git push origin mainLive build and app logs stream from self-hosted Elasticsearch, automatic SSL covers your custom domain, and you get server-side metrics without a client SDK.
Step 5: Environment and permissions
Set env vars in the dashboard (keep the same key names across environments). For Deno specifically, decide your permission flags deliberately — running -A is convenient but --allow-net --allow-env --allow-read=. is a safer production default if your app's needs are known.
Step 6: Static assets and caching
Fresh fingerprints static assets in /static. They're served with long cache headers automatically. Because Fresh ships almost no client JS, your pages are light — pair that with edge caching on your custom domain for fast global loads.
Notes and trade-offs
- Cold starts: Free-tier apps scale to zero on spot nodes, so the first request after idle is slower. Deno's startup is fast, which helps, but paid tiers stay warm.
- Ecosystem: Deno's package story (JSR, npm compat) has matured a lot; pin versions in
deno.lockfor reproducible builds. - First-party vs portable: Deno Deploy has tighter Fresh integration; a container platform trades some of that for a co-located managed database and runtime control.
References
- [Fresh documentation](https://fresh.deno.dev/docs/introduction)
- [Deno — Dockerizing your app](https://docs.deno.com/runtime/reference/docker/)
- [Deno — permissions](https://docs.deno.com/runtime/fundamentals/security/)
- [deno-postgres / postgresjs](https://deno.land/x/postgres)
---
Fresh's zero-JS-by-default model plus a co-located managed Postgres makes for fast, lean full-stack apps. Containerize it, push to PandaStack's [free tier](https://dashboard.pandastack.io), and let DATABASE_URL wire itself in.