Back to Blog
Tutorial10 min read2026-07-01

How to Deploy a Deno Fresh App with a Database

Fresh is Deno's island-based web framework with no build step and server-side rendering by default. This guide covers containerizing a Fresh app on the Deno runtime and connecting it to a PostgreSQL database.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

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 read DATABASE_URL and PORT.
  • --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

  1. 1Provision a managed PostgreSQL instance. PandaStack injects DATABASE_URL; your Pool reads it via Deno.env.get.
  2. 2Connect the Git repo as a container app. The Dockerfile is auto-detected — this is the reliable path for a Deno runtime.
  3. 3Ensure your start command includes the right --allow-* flags and binds to the platform PORT.
  4. 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.lock committed so dependency versions are pinned.
  • [ ] Minimal --allow-* permission flags.
  • [ ] Server binds to the platform PORT and 0.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 | head

A 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).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also