Outgrowing Deno Deploy or Val Town? How to Move to Kubernetes With a Real Database and Persistent State
A practical migration path from Deno Deploy or Val Town to Kubernetes when your app needs a real Postgres database, persistent volumes, background workers, and long-running processes. Includes a primitive-by-primitive mapping table, a managed-vs-StatefulSet decision table, a connection-pool sizing formula, and a seven-step cutover you can roll back with a DNS change.
Deno Deploy and Val Town run your code on demand, then stop it. No disk you can rely on between runs, a cap on run time, no always-on worker, and a datastore the platform operates. Those four limits are the only good reasons to move a stateful app to Kubernetes.
Run three migrations in order: containerize on the official denoland/deno image, move state into a managed Postgres (Amazon RDS, Google Cloud SQL, Azure Database for PostgreSQL, Scaleway Managed Database), then re-point secrets, cron, webhooks and domains.
Move the data before the traffic. Export Deno KV with kv.list({ prefix: [] }), design the Postgres schema first, backfill, verify, then cut DNS with a TTL lowered 24 hours earlier.
Size connection pools before you scale:(pool size x replicas) + migration jobs must stay below max_connections (default 100), with PgBouncer in transaction mode in front.
App on Kubernetes, data on managed Postgres is the right split for most teams. Qovery runs it in your own AWS, GCP, Azure, or Scaleway account, or on your existing Kubernetes cluster.
Every isolate-style platform has the same four walls, and a stateful app hits all four within about a year. No disk you can trust between requests. A ceiling on how long one run can last. No process that stays up when traffic stops. A datastore whose backups, location and restore drills belong to someone else.
I have watched a lot of small teams build something great on Deno Deploy or Val Town, then wake up with a relational schema, a background job queue and a customer asking for SOC 2 evidence. This guide is the migration path I would follow in that position, primitive by primitive, with the data moved before the traffic.
Why do teams outgrow Deno Deploy or Val Town once they need persistent state?
Teams outgrow Deno Deploy and Val Town because of four structural limits of on-demand runtimes: no durable writable filesystem between runs, a hard ceiling on execution time, no always-on background worker, and a built-in datastore (Deno KV or Val Town SQLite) that the platform operates for you. Both platforms are well built. If none of those four limits hurts you today, moving to Kubernetes is a downgrade.
Deno Deploy starts your app when a request arrives and stops it when traffic goes quiet. Its runtime docs say the idle timeout is "between 5 seconds and 10 minutes", that instances "do not share CPU, memory, or disk resources", and that on shutdown the app gets 5 seconds after SIGINT before a SIGKILL (Deno Docs, Runtime). The current Deno Deploy lets you write to the filesystem, but that disk belongs to one instance that can disappear at any time. Memory is capped at 512 MB per application (Deno Docs, Pricing and limitations).
Val Town is honest about what it is for. Its docs say "You can make websites, APIs, MCP servers, crons, Slack bots, email automations, webhook handlers, and more" (Val Town Docs). Every val gets "its own private SQLite database", which "is powered by Turso", with 10 MB of storage on the free plan and up to 1 GB on paid plans (Val Town Docs, SQLite). Runs are capped at 1 minute of wall-clock time on Free and 10 minutes on Pro (Val Town Pricing).
Run this 60-second checklist. If you tick two or more, start planning:
You need a relational schema with joins, foreign keys and migrations.
An import, PDF render or video job runs longer than the platform's run limit.
You hold websockets or other long-lived connections that must survive instance restarts.
You want pgvector or another Postgres extension.
A customer asks about data residency, SOC 2 or where your backups live.
You want backups you own and have actually restored.
By "persistent state" I mean four things: durable relational data, object storage, queues, and scheduled work that survives the request that created it. A key-value cache does not count.
Staying is the correct call for a scheduled scraper, a webhook receiver that forwards to another system, or a Slack bot. Cost is the other honest reason to wait. Deno Deploy and Val Town bill by usage and scale to zero, while a Kubernetes cluster plus managed Postgres has a monthly floor you pay even with zero traffic.
Capability
Deno Deploy
Val Town
Kubernetes in your cloud account
Execution model
Instances start on request and stop after 5 s to 10 min idle (Deno Docs)
Vals run per trigger: HTTP, cron or email
Long-running pods that stay up until you change them
Documented maximum run time
No published per-request timeout, but instances can be evicted at any time with 5 s to shut down
1 min wall clock per run on Free, 10 min on Pro (Val Town Pricing)
No platform ceiling; you set timeouts and terminationGracePeriodSeconds (default 30 s)
Always-on background worker
No, Deno Deploy stops idle instances
No, Val Town crons run at most every minute on Pro and every 15 min on Free
Yes, a Deployment with at least one replica
Built-in datastore
Deno KV, Prisma-hosted Postgres, or one linked external Postgres
Val-scoped SQLite (Turso) and blob storage, 10 MB on Free, 1 GB on Pro
None; you attach a database
Persistent writable volume
Writable filesystem, but local to one instance and lost when it stops
No disk; blob storage instead
PersistentVolumeClaim, ReadWriteOnce means one node at a time
Connection to your own database
Postgres only, and it must accept connections from any IP (Deno Docs, Databases)
Over the public internet from val code
Private networking inside your VPC
Who owns backups and point-in-time recovery
Deno for KV, Prisma for hosted Postgres
Val Town
You, or your managed database provider in your account
Data residency control
Limited to Deno Deploy's regions (2 in the current Deploy)
Decided by Val Town
Any region your cloud provider offers
Scale to zero
Yes
Yes
No by default; nodes cost money while idle
What exactly do you need to replace when you leave Deno Deploy or Val Town?
Every Deno Deploy and Val Town primitive you use has a one-line Kubernetes or cloud equivalent, and writing that mapping down is 80% of the migration plan. Write it before any YAML, because the primitives teams forget (cron, blob storage, preview URLs, automatic TLS, outbound IP allowlists) are the ones that break in production after cutover.
Two rows deserve extra attention. Kubernetes CronJobs default to concurrencyPolicy: Allow, keep 3 successful and 1 failed Job by default, and the docs warn that "two Jobs might be created, or no Job might be created", so your jobs "should be idempotent" (Kubernetes Docs, CronJob). Deno Deploy's cron, by contrast, skips an invocation if the previous run is still going (Deno Docs, Cron). Set concurrencyPolicy: Forbid and startingDeadlineSeconds explicitly if you want the old behavior.
Secrets are the second trap. "Kubernetes Secrets are, by default, stored unencrypted in the API server's underlying data store (etcd)" (Kubernetes Docs, Secrets). Turn on encryption at rest or use External Secrets Operator backed by your cloud's secret manager.
Deno Deploy or Val Town primitive
Kubernetes or cloud equivalent
What your team now operates
What Qovery provisions
Deno KV
Managed Postgres (Amazon RDS, Google Cloud SQL, Azure Database for PostgreSQL, Scaleway Managed Database), plus Redis or Valkey for truly ephemeral keys
Bucket credentials injected as secrets into the environment
Deno.cron and Val Town scheduled vals
Kubernetes CronJob with concurrencyPolicy, startingDeadlineSeconds and history limits set explicitly
Idempotency, time zones, failed-run alerts
Cron job services defined per environment
Platform environment variables and secrets
Kubernetes Secrets with encryption at rest, or External Secrets Operator
Rotation, access control
Environment variables and secrets scoped per environment
Automatic HTTPS and custom domains
Ingress controller plus cert-manager
Certificate renewal, DNS records
Ingress, TLS certificates and custom domains
Per-deploy preview URLs
Ephemeral environment per pull request
Creation, teardown, cost of idle previews
Preview environment per pull request, with auto-stop
Built-in logs and metrics
Prometheus, Grafana, Loki or a SaaS observability tool
Retention, alerting, on-call
Application and deployment logs in the console; long-term observability stays your choice
Unknown platform egress IPs
NAT gateway with static outbound IPs
Allowlists on third-party APIs
Static outbound IP option on the clusters Qovery manages
How do you containerize a Deno app for Kubernetes?
Containerizing a Deno app for Kubernetes takes a Dockerfile of under 15 lines on the official denoland/deno image, with explicit permission flags and the image's built-in non-root deno user. The real work is removing platform-specific APIs and adding two things Kubernetes needs that Deno Deploy and Val Town never asked for: health endpoints for probes and SIGTERM handling for graceful shutdown.
This follows the pattern in the official denoland/deno_docker README and the Deno Docker guide. The image already defines the deno user and wraps the entrypoint so CMD arguments go straight to the deno binary:
DOCKERFILE
FROM denoland/deno:2.9.8
WORKDIR /app
# Cache dependencies as their own layer
COPY deno.json deno.lock* ./
RUN deno install
COPY . .
# Compile once at build time, not at every pod start
RUN deno cache main.ts
USER deno
EXPOSE 8000
CMD ["run", "--allow-net", "--allow-env", "main.ts"]
Pin the tag, and add --allow-read only if you read files. Then make the server Kubernetes-aware. Read the port from the environment, expose /health for liveness and /readyz for readiness, and drain on SIGTERM with Deno.addSignalListener and the server's shutdown() method:
TS
const port = Number(Deno.env.get("PORT") ?? "8000");
let draining = false;
const server = Deno.serve({ port }, (req) => {
const { pathname } = new URL(req.url);
if (pathname === "/health") return new Response("ok");
if (pathname === "/readyz") return new Response(null, { status: draining ? 503 : 200 });
return handler(req); // your existing app
});
Deno.addSignalListener("SIGTERM", async () => {
draining = true;
await server.shutdown(); // stops accepting, lets in-flight requests finish
Deno.exit(0);
});
Kubernetes gives a terminating pod a grace period that "defaults to 30 seconds", sends SIGTERM, then kills whatever is left (Kubernetes Docs, Pod lifecycle). Set terminationGracePeriodSeconds above your longest request. Probes run every 10 seconds and restart or unroute a pod after 3 failures by default (Kubernetes Docs, Probes), so keep /health cheap and never let it query the database.
Then remove the platform APIs. Every Deno.openKv() call becomes a Postgres client, and every Val Town std/sqlite, std/blob and std/email import becomes a Postgres client, an S3 SDK and an email provider. Set CPU and memory requests and limits so the HorizontalPodAutoscaler has a signal to scale on.
Decide where long-running work lives. Short jobs can stay in the web process. Anything longer than a request belongs in a separate worker Deployment that consumes a queue, and scheduled work belongs in a CronJob. Cold starts stop being a per-request concern once pods stay warm, which is one of the few things that gets easier after the move. Idle pods cost money, though, so auto-stop non-production environments.
How do you move your data from Deno KV or Val Town SQLite to a real Postgres database?
Move the data in five ordered phases (export, model, backfill, verify, cut over) and never migrate data and traffic in the same step, because your only safe rollback is a datastore still intact on the old platform. Deno KV is a schemaless key-value store, so moving it to Postgres means redesigning your data model, not running a dump and restore.
Start with what shaped your current data. Deno KV caps keys at 2 KiB and values at 64 KiB, and an atomic operation allows at most 1,000 mutations and 800 KiB in total (Deno Docs, Transactions). Those limits are why your users are probably split across several keys today. Keys are arrays such as ["users", userId, "orders", orderId], "ordered lexicographically" (Deno Docs, Deno KV). Each prefix you list by today points to a table and a composite index in Postgres.
The simplest export is a temporary, secret-protected route on the old Deno Deploy app that streams every entry as NDJSON:
kv.list returns batches of up to 500 entries, and its consistency guarantee "is applied to each batch individually" (Deno Docs, Deno.Kv.list). So run the export during dual-write, not as your single source of truth. For large keyspaces, call it with limit and resume from the iterator's cursor.
Val Town SQLite exports through the built-in admin panel, which can "Dump the database as SQL" or "Export a table as CSV" (Val Town Docs, SQLite admin). Load the SQL dump into a local SQLite file, then run pgloader against it. Its default casting rules map integer to bigint, blob to bytea and datetime to timestamptz (pgloader, SQLite).
Watch for three data-fidelity traps:
SQLite's dynamic typing. A column declared INTEGER can hold text in SQLite. Postgres will reject those rows, so count them before loading.
INTEGER PRIMARY KEY is the rowid. Reset sequences after the load or your first insert collides.
Deno KV tuple keys are not strings. Split them into typed columns instead of storing "users:42" as text.
Pick one migration tool and keep it: drizzle-kit, Prisma Migrate, or plain SQL files run by a Kubernetes Job before each rollout. For zero downtime, dual-write to both stores, backfill history, compare row counts and checksums on a sample, flip reads, then stop writing to the old store.
Then size your connections. Deno Deploy and Val Town opened a connection per invocation and threw it away. A Kubernetes Deployment holds a pool per pod, and PostgreSQL's max_connections "is typically 100 connections" (PostgreSQL Docs). Keep this inequality true:
(pool size per pod x replicas) + migration jobs + admin headroom < max_connections
Ten pods with a pool of 10 already uses up 100. Run SHOW max_connections; on your instance, because managed services often derive it from instance memory. Then put PgBouncer in front in transaction pooling mode, where "a server connection is assigned to a client only during a transaction" (PgBouncer, Features). Transaction mode breaks session-level features such as session advisory locks and SET without LOCAL, so check your ORM settings.
Back up from day one, because when you left the platform you also left its backup policy. Then test the restore, not only the backup. Before cutover, restore into a scratch database once and run your test suite against it.
Ship faster on infrastructure you control.
Qovery gives your team self-service deployments on your own AWS, GCP, Azure, or Scaleway account - or your existing Kubernetes cluster. Start deploying in under 10 minutes.
Should you run Postgres as a Kubernetes StatefulSet or use a managed cloud database?
For almost every team leaving Deno Deploy or Val Town, the answer is a managed cloud database: Amazon RDS, Google Cloud SQL, Azure Database for PostgreSQL Flexible Server, or Scaleway Managed Database. Automated backups, point-in-time recovery, minor-version patching and cross-zone failover come with a published uptime SLA. Run Postgres in-cluster only when you have a specific cost, portability or sovereignty reason and a named person who owns failover drills.
A plain StatefulSet gives you stable network identity, a persistent volume per pod and ordered rollout (Kubernetes Docs, StatefulSet). With ReadWriteOnce, "the volume can be mounted as read-write by a single node" (Kubernetes Docs, Persistent Volumes). Everything else is yours: volume expansion, major-version upgrades, backup verification, failover rehearsal and a VolumeSnapshot policy you actually test.
Operators close most of that gap. CloudNativePG does continuous WAL archiving through the Barman Cloud plugin, supports volume snapshots, and "provides out-of-the-box an RPO ≤ 5 minutes" (CloudNativePG Docs, Backup). It also elects a new primary when the old one fails. Zalando's postgres-operator is similarly mature. Both are good software, and neither removes the on-call rotation.
Option
Automated backups
Point-in-time recovery window
HA failover
Major-version upgrades
Published SLA
Who carries the pager
Best fit
Amazon RDS for PostgreSQL
Yes
Retention 0 to 35 days, console default 7 (AWS Docs)
Object store via Barman Cloud plugin, or volume snapshots
Any point since the first base backup, with WAL archiving
Operator promotes a new primary
You plan and run them with operator tooling
None
You
Portability or cost reasons with a team that knows Postgres
The pattern that works for most of these migrations: app on Kubernetes, data on the managed service, private networking between them, no public database endpoint, and credential rotation handled by the provider. Qovery takes that managed path for you: its managed databases are provisioned through Blueprints backed by the cloud provider's service, such as Amazon RDS for PostgreSQL or Scaleway Managed PostgreSQL, inside your own cloud account (Qovery Docs, Databases).
What does the full Deno Deploy or Val Town to Kubernetes migration look like, step by step?
A realistic Deno Deploy or Val Town to Kubernetes migration is seven steps spread over a few days of engineering work. The cutover itself is a DNS change you can reverse in minutes, as long as the old deployment stays warm for 48 hours. Steps 1 to 4 cost nothing to reverse, and only step 7, decommissioning, is one-way.
Inventory every platform primitive using the mapping table above. Teams skip this step, and it is why migrations slip.
Containerize and run locally with Docker Compose against a local Postgres until parity is boring.
Provision the target: a Kubernetes cluster in your AWS, GCP, Azure, or Scaleway account (or the cluster you already run), plus a managed Postgres in the same VPC with private networking and no public endpoint.
Run schema migrations as a Kubernetes Job, then backfill from Deno KV or Val Town SQLite and verify row counts and checksums.
Deploy the app: Deployment, Service, Ingress with TLS, HPA, injected secrets, liveness and readiness probes, and CronJobs replacing Deno.cron and scheduled vals.
Dual-write or shadow traffic, verify parity on your top five endpoints, and compare p95 latency against the old platform before you touch DNS.
Lower the DNS TTL 24 hours ahead, cut over, keep the old deployment warm for 48 hours, then decommission and revoke platform tokens.
The TTL is "the time interval that the resource record may be cached before the source of the information should again be consulted" (RFC 1035). Lowering it a full old-TTL period ahead means resolvers stop holding your old record for long. Some resolvers can serve stale answers when they cannot reach your nameservers (RFC 8767), which is one more reason to keep the old deployment answering during the rollback window.
Five things teams forget:
Outbound IP allowlists on third-party APIs now see new addresses.
A cron stops silently the day someone deletes the old val.
Webhook URLs registered with Stripe, GitHub and Slack still point at the old host.
The old platform's automatic TLS no longer renews anything for your domain.
Error tracking and uptime checks still monitor a dead hostname.
How do you get Kubernetes without hiring a platform team?
You get Kubernetes without a platform team by putting an internal developer platform on top of a cluster in your own cloud account. Your git-push workflow survives the migration, while cluster upgrades, ingress, TLS, RBAC and database provisioning are handled for you. Raw Kubernetes swaps one problem (platform limits) for a bigger one (cluster operations), and that trade is why teams stay on Deno Deploy and Val Town long past the pain threshold.
Kubernetes is now the default target: 80% of organizations run it in production, up from 66% in 2023, and the CNCF reports that "cultural and operational shifts" have overtaken security as the top hurdle (CNCF Annual Survey 2024). Upgrades never stop either. Kubernetes ships three minor versions a year, and in Datadog's 2023 report the most popular release was already 16 months old (Datadog Container Report 2023).
Raw Kubernetes plus Terraform plus Argo CD means cluster upgrades every few months, VPC and ingress design, certificate rotation, RBAC, autoscaling tuning, cost attribution and an on-call rotation, all before you ship a feature.
Qovery deploys your containerized Deno app to Kubernetes inside your own AWS, GCP, Azure, or Scaleway account, or onto an existing Kubernetes cluster you already run. Because it runs in your account, the cloud bill, Savings Plans and committed-use discounts stay in your name. That is also the only way the data residency and backup ownership reasons you migrated for actually hold.
What maps back to what you had before: git-push deployments, managed databases backed by the cloud provider's service, a preview environment per pull request (the nearest thing to Val Town's instant URLs), auto-stop for non-production environments, managed cluster upgrades and per-environment RBAC.
Qovery is the wrong answer for a single stateless scheduled script, which belongs on Val Town, and for a static site, which belongs on a CDN. A team that already has a platform group may only want Terraform modules.
Destination
Persistent volumes
Managed Postgres
Long-running processes and workers
Runs in your own cloud account
Data residency control
Ops burden on your team
Deno Deploy
No, instance disk is lost when the instance stops (Deno Docs)
Prisma-hosted Postgres or one linked external Postgres
Managed database Blueprints such as Amazon RDS for PostgreSQL or Scaleway Managed PostgreSQL
Yes, applications, workers and cron jobs
Yes: AWS, GCP, Azure, Scaleway, or your own Kubernetes cluster
Full, in your account
Low; Qovery runs cluster operations
How do I run this migration from Claude Code or Cursor?
Install the Qovery Agent Skills once and your coding agent can containerize the Deno app, provision the Kubernetes cluster and managed Postgres in your own cloud account, and deploy it, pausing for your approval on every infrastructure action. The whole setup is four commands, and the MCP Server stays read-only until you explicitly enable write access.
Install the skills globally for every compatible agent (Claude Code, Cursor, Codex, OpenCode, Gemini CLI). The agent still asks before touching any cloud resource:
Add the MCP Server to Claude Code. It uses OAuth and is read-only by default, so the agent can inspect what already exists without changing it:
BASH
claude mcp add --transport http qovery https://mcp.qovery.com/mcp --callback-port 4242
Enable write access only when you want the agent to change things. You also have to approve write access in the Qovery Console:
BASH
claude mcp add --transport http qovery "https://mcp.qovery.com/mcp?read_write=true" --callback-port 4242
In this migration, qovery-onboard connects your cloud account, qovery-deploy ships the containerized Deno app, qovery-preview creates per-pull-request environments in place of platform preview URLs, qovery-troubleshoot debugs probe failures and database connection errors after cutover, and qovery-terraform exports the setup as code. Then ask in plain language: "containerize this Deno app and deploy it to my AWS account with a managed Postgres using Qovery". Details are in the AI agent quickstart and the Claude Code integration page.
Frequently asked questions
Can you run Deno on Kubernetes in production, and what does the Dockerfile look like?
Yes, Deno runs well on Kubernetes using the official denoland/deno image, which ships a non-root deno user. A production Dockerfile pins a version tag, runs deno install and deno cache main.ts at build time, switches to USER deno, and starts with explicit flags such as run --allow-net --allow-env main.ts. Add /health and /readyz endpoints and a SIGTERM handler so rolling updates drain cleanly.
How do I export data out of Deno KV and load it into Postgres?
Export Deno KV by iterating kv.list({ prefix: [] }) and writing each key and value as one NDJSON line, paginating with limit and cursor for large keyspaces. Deno KV is schemaless with ordered tuple keys, so design the Postgres tables first, mapping each key prefix to a table and composite index. Then backfill with a script, and verify row counts and checksums before switching reads.
What replaces Val Town's SQLite and blob storage when you move to Kubernetes?
Val Town SQLite is usually replaced by a managed Postgres database, and Val Town blob storage by S3-compatible object storage such as Amazon S3, Google Cloud Storage, Azure Blob Storage or Scaleway Object Storage. Export the SQLite data from the Val Town admin panel as a SQL dump or CSV, then load it with pgloader or a scripted import. SQLite on a single-replica PersistentVolumeClaim also works if you accept one writer.
Do I need a managed Postgres, or can I run the database inside my Kubernetes cluster with a StatefulSet?
Most small teams should use a managed Postgres such as Amazon RDS, Google Cloud SQL, Azure Database for PostgreSQL or Scaleway Managed Database, because backups, point-in-time recovery and failover come with a published SLA. A StatefulSet, or an operator such as CloudNativePG, is reasonable when you have a cost, portability or sovereignty reason. In that case someone on your team owns backups, upgrades and failover drills.
How do I replace Deno Deploy cron jobs and Val Town scheduled vals on Kubernetes?
Replace Deno.cron and Val Town scheduled vals with Kubernetes CronJobs. Set concurrencyPolicy: Forbid if runs must not overlap, set startingDeadlineSeconds so late runs are skipped, and make every job idempotent, because Kubernetes documents that a scheduled Job can occasionally run twice or not at all. Set .spec.timeZone explicitly, since both Deno Deploy and Val Town evaluate schedules in UTC.
Is Kubernetes overkill for a small Deno app that just needs a database and a background worker?
Kubernetes is overkill for a single stateless scheduled script, which should stay on Val Town or Deno Deploy. For an app with a relational database, a background worker and compliance requirements, Kubernetes becomes reasonable once a platform handles cluster operations for you. The monthly cost floor of a cluster plus managed Postgres is the real reason to wait until you actually hit one of the four limits.
Alessandro leads product at Qovery. He drives the changelog, roadmap, and product strategy - turning customer feedback into platform capabilities.
Next step
Ship faster on infrastructure you control.
Qovery gives your team self-service deployments on your own AWS, GCP, Azure, or Scaleway account - or your existing Kubernetes cluster. Start deploying in under 10 minutes.