Fresh is Deno's web framework: server-rendered by default, JavaScript shipped to the browser only for interactive "islands," and — notably — no build step in the traditional sense for development. Deploying it outside Deno's own hosting means running the Deno runtime in a container. Here's how to do that with a PostgreSQL database.
How Fresh runs
A Fresh app is a Deno program. Its entry point is main.ts, and you start it with the Deno CLI. There is no node_modules and no bundler config to wrestle with — Deno resolves imports directly, with dependencies pinned in deno.json.
// main.ts (Fresh 2 style)
import { App, staticFiles } from 'fresh';
export const app = new App()
.use(staticFiles());
app.get('/api/health', () => new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'content-type': 'application/json' }
}));
if (import.meta.main) {
await app.listen({ port: Number(Deno.env.get('PORT') ?? 8000) });
}Reading config from Deno.env.get(...) is how you pull in runtime environment variables like the database URL.
Connecting PostgreSQL
Use a Deno-native Postgres driver. postgres (deno.land/x or JSR) and deno-postgres are both common. Example with a pooled client:
// db.ts
import { Pool } from 'https://deno.land/x/postgres/mod.ts';
export const pool = new Pool(Deno.env.get('DATABASE_URL'), 10, true);
export async function getPosts() {
using client = await pool.connect();
const result = await client.queryObject`SELECT * FROM posts LIMIT 20`;
return result.rows;
}The using keyword auto-releases the connection back to the pool — a nice Deno ergonomic that prevents leaks.
Then use it from a route or island data loader:
app.get('/api/posts', async () => {
const posts = await getPosts();
return new Response(JSON.stringify(posts), {
headers: { 'content-type': 'application/json' }
});
});Permissions: Deno's security model
Deno is secure-by-default — a program can't touch the network, environment, or filesystem unless you grant it. In production you run with explicit flags:
deno run --allow-net --allow-env --allow-read main.ts--allow-net— needed to bind the server and reach the database.--allow-env— to readDATABASE_URLandPORT.--allow-read— to serve static assets.
Grant the narrowest set that works. This is a genuine security advantage over runtimes that allow everything by default.
Containerize
FROM denoland/deno:latest
WORKDIR /app
COPY deno.json deno.lock ./
# Cache dependencies
RUN deno cache main.ts || true
COPY . .
RUN deno cache main.ts
ENV PORT=8000
EXPOSE 8000
CMD ["run", "--allow-net", "--allow-env", "--allow-read", "main.ts"]Caching dependencies in a separate layer keeps rebuilds fast. The official denoland/deno image bundles the runtime so there's nothing else to install.
Deploying on PandaStack
- 1Provision a managed PostgreSQL instance. PandaStack injects
DATABASE_URL; yourPoolreads it viaDeno.env.get. - 2Connect the Git repo as a container app. The Dockerfile is auto-detected — this is the reliable path for a Deno runtime.
- 3Ensure your start command includes the right
--allow-*flags and binds to the platformPORT. - 4Add a custom domain; SSL is automatic.
Because Fresh renders on the server and ships minimal client JS, the container stays small and starts quickly — which pairs well with scale-to-zero on lower tiers. For latency-sensitive production traffic, use always-on compute to avoid the first-request cold start.
Production checklist
- [ ]
deno.lockcommitted so dependency versions are pinned. - [ ] Minimal
--allow-*permission flags. - [ ] Server binds to the platform
PORTand0.0.0.0. - [ ] Pooled DB connections, sized under the DB tier's connection limit.
- [ ] Static assets served via
staticFiles()middleware.
Verifying
curl -s https://app.example.com/api/health
# {"status":"ok"}
curl -s https://app.example.com/api/posts | headA healthy /api/health and real rows from the DB confirm the Deno runtime and database wiring.
References
- [Fresh documentation](https://fresh.deno.dev/docs/introduction)
- [Deno permissions](https://docs.deno.com/runtime/fundamentals/security/)
- [Deno official Docker image](https://github.com/denoland/deno_docker)
- [deno-postgres driver](https://deno.land/x/postgres)
---
A Deno Fresh container with a managed PostgreSQL deploys cleanly on PandaStack — Dockerfile auto-detected and DATABASE_URL injected. Try it free at [dashboard.pandastack.io](https://dashboard.pandastack.io).