Run a Deno HTTP service under systemd, verify it on loopback, and publish it through an outbound Localtonet tunnel
Self-hosting a Deno application involves more than copying TypeScript files to another computer. You need a compatible runtime, explicit permissions, protected configuration, durable storage, process supervision, local verification, and a rollback plan. This tutorial provides a complete deployment path for a Linux host using Deno, systemd, and a Localtonet HTTP tunnel. It also explains which parts of a managed Deno deployment do not automatically move with the runtime.
๐ What's in this guide
Scope and deployment architecture
This tutorial uses one supported, reproducible deployment path: a Linux machine with systemd. The machine can be a physical server, virtual machine, home server, or always-on workstation. It must have outbound internet access and enough resources for the Deno application, its data, and the Localtonet client.
The example application uses Deno's built-in Deno.serve API and listens on 127.0.0.1:8000. A systemd service starts the process at boot and restarts it after an unexpected failure. The Localtonet client runs on the same machine, establishes an outbound connection to our relay server, and forwards requests from an assigned public HTTPS address to the loopback service.
macOS and Windows can also run Deno and the Localtonet client, but their service-management procedures differ. The installation section lists current official Deno installation paths for those operating systems. The complete supervision configuration in this tutorial is intentionally limited to Linux with systemd so that the startup workflow is concrete rather than pseudocode.
Localtonet publishes an HTTP service that is already running and reachable from the selected client device. It does not install the application, migrate its database, reproduce managed provider services, or replace application authentication.
Separate Deno runtime lifecycle from Deno Deploy migration
Self-hosting the open-source Deno runtime and migrating away from Deno Deploy are related only when an application currently depends on both. Running deno run on your own Linux host means that you operate the runtime process, operating system, updates, data, logs, and recovery workflow. Moving from Deno Deploy can additionally require replacing provider-specific storage, queues, schedules, deployment APIs, networking behavior, or other managed services.
A secondary report supplied during editorial review describes announcements dated October 9, 2026 concerning future Deno runtime development and Deno Deploy. That report says Deno Deploy would operate for six months and that the runtime would receive another year of monthly bug-fix and security releases. However, the supplied evidence does not include the primary Deno and Cloudflare announcements themselves, and the report also says those announcements did not publish calendar-day shutdown dates.
Six months from October 9, 2026 would nominally be April 9, 2027, and twelve months would nominally be October 9, 2027. Those are calculated planning dates, not verified service deadlines. Confirm the wording, effective dates, eligibility for migration support, and any later updates directly in the primary Deno and Cloudflare announcements before relying on them operationally.
This distinction matters because a hosted service shutdown does not automatically make an open-source runtime stop working on a self-managed machine. Conversely, an executable continuing to run does not mean it will receive indefinite security maintenance. A self-hosted operator must track runtime releases, test updates, monitor dependencies, and maintain a migration option if official maintenance changes.
For an application currently on Deno Deploy, inventory managed dependencies before assuming that copying the entry file is enough. For an application that already runs as a conventional Deno process, this tutorial provides a direct way to operate that process on Linux. In both cases, preserve a known-good runtime version and application revision while you validate the new host.
Plan what the self-hosted deployment must include
A Deno application is not necessarily a single portable unit. Its request handler may run anywhere a compatible Deno runtime is available, while the complete system depends on databases, object storage, queues, scheduled jobs, identity services, secrets, callback URLs, and provider-specific APIs.
Begin by recording the current deployment as a set of dependencies. Decide which components will move to the Linux host, which will remain remotely managed, and which need replacements. This prevents a misleading result where the HTTP process starts but cannot perform useful work.
Find runtime-specific code
Web APIs such as Request, Response, URL, and fetch are usually the most portable parts of a Deno application. Package imports need version and lock-file review. Calls through the Deno namespace deserve direct inspection because they can involve environment variables, files, network access, subprocesses, or Deno-specific services.
From the project root, use this search as an inventory aid:
grep -R -n --include="*.ts" "Deno\." .
A result is not automatically a migration blocker. For example, Deno.env.get works on a self-hosted Deno runtime when the appropriate permission is granted. A provider-specific storage API may need replacement even if it does not appear in this search.
| Dependency | Questions to answer | Migration action |
|---|---|---|
| Deno runtime | Which release, entry point, tasks, and permissions are required? | Install a compatible release and test the existing command first. |
| Environment | Which variables are required, optional, or secret? | Create protected host configuration with only the required values. |
| Database | Is it portable, remotely managed, or tied to the current provider? | Retain it temporarily or migrate schema and data independently. |
| File storage | Where are uploads and generated files stored? | Copy and verify them, then use a deliberate persistent path. |
| Jobs and queues | Could duplicate workers process the same work? | Define ownership and a controlled handover point. |
| Public endpoint | Which users, webhooks, redirects, and clients depend on it? | Test the replacement address before updating consumers. |
| Observability | Where are logs, health checks, and alerts recorded? | Provide host-level logging and failure detection. |
Prepare the Linux host
The commands below assume an account with sudo access, systemd, curl, and standard shell tools. Confirm that the system clock is correct, install operating-system security updates, and make sure you have a tested way to access the machine if the application fails.
Record a baseline from the old deployment before changing traffic. Capture representative status codes, response bodies, relevant headers, authentication behavior, and at least one reversible write where safe. Back up persistent data using the storage system's supported consistency procedure, then test restoration rather than assuming the backup is usable.
Do not place database passwords, API keys, session secrets, or Localtonet device tokens in source control. Create replacement credentials where practical, grant the minimum required access, and revoke obsolete credentials only after a successful cutover.
Install and validate Deno
Deno publishes operating-system-specific installation methods. Use an official installer or supported package manager, then verify the executable and version before deploying the application. The complete service path later in this tutorial uses the official shell installer on Linux.
Linux installation used by this tutorial
Install Deno for your current Linux account with the official installation script:
curl -fsSL https://deno.land/install.sh | sh
The installer normally places the executable under $HOME/.deno/bin and prints the environment change needed for your shell. Apply the printed change or open a new terminal. Then verify the installation:
deno --version
command -v deno
The second command prints the executable path. Record the reported Deno version with the deployment record. If the source environment uses a different release, test the application against its known-good release before combining migration with a runtime upgrade.
macOS alternatives
On macOS, the official shell installer can be used in the same way:
curl -fsSL https://deno.land/install.sh | sh
Homebrew users can instead install Deno through Homebrew:
brew install deno
These paths install the runtime, but they do not provide the Linux systemd configuration used later in this guide. Use the macOS service-management procedure appropriate to your environment if you deploy there.
Windows alternatives
From PowerShell, Deno documents an official installation script:
irm https://deno.land/install.ps1 | iex
Supported Windows package-manager paths also include:
winget install DenoLand.Deno
scoop install deno
choco install deno
Choose one installation method rather than layering several package managers over the same executable. Open a new terminal and run deno --version after installation.
Current installation choices should be checked against the official Deno installation documentation before production use, especially if your operating system or package manager has changed.
Restore and inspect the application
Check out or securely copy the exact application revision selected for deployment. Prefer a version-control commit or repeatable release artifact over an untracked working directory. Preserve the project's Deno configuration and lock information when present.
List defined Deno tasks before attempting to run a task by name:
deno task
If the project actually defines a test task, run it:
deno task test
Do not assume that every project uses that task name. Run the formatting, linting, type-checking, test, build, or migration commands defined by the repository you are deploying.
Deno permissions are part of the application's security boundary. Determine which environment variables, directories, network addresses, and subprocesses the service needs, then grant those resources as narrowly as the application permits.
Create an explicit Deno HTTP service
The following sample is compatible with the current Deno.serve interface used by Deno 2 releases. It reads one environment variable, validates the port, binds only to IPv4 loopback, exposes a health route, and returns a clear not-found response for unknown paths.
const hostname = "127.0.0.1";
const port = Number(Deno.env.get("PORT") ?? "8000");
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error("PORT must be an integer between 1 and 65535");
}
Deno.serve({ hostname, port }, (request) => {
const url = new URL(request.url);
if (url.pathname === "/health") {
return Response.json({ ok: true });
}
if (url.pathname === "/") {
return new Response("Deno app is running\n", {
headers: {
"content-type": "text/plain; charset=utf-8",
},
});
}
return new Response("Not found\n", { status: 404 });
});
Save the file as main.ts. The server reads only PORT and listens only on 127.0.0.1:8000, so the matching scoped command is:
PORT=8000 deno run --allow-env=PORT --allow-net=127.0.0.1:8000 main.ts
The environment permission allows access only to the named variable. The network permission allows the process to use the specified loopback address and port. The sample does not read files at runtime, write data, execute subprocesses, or connect to a remote database, so it does not receive permissions for those operations.
If you change the configured port, update all of the following together:
- The
PORTenvironment value. - The
--allow-netaddress and port. - The local verification URL.
- The systemd service command.
- The Localtonet local target.
A real application may need additional scoped permissions. A database client, for example, needs network access to its database host and port. An upload handler may need read and write access to one persistent directory. Add each permission because the application requires it, not because a broad flag is more convenient.
Design configuration as an interface
| Configuration | Recommended handling | Failure to avoid |
|---|---|---|
| Port and bind address | Set and validate them explicitly. Prefer loopback when the tunnel client is local. | A target mismatch prevents Localtonet from reaching the app. |
| Secrets | Load them from protected host or supervisor configuration. | Do not commit credentials or device tokens. |
| Dependency endpoints | Configure database, storage, and API addresses by environment. | The old provider endpoint may be unavailable from the new host. |
| Filesystem paths | Use absolute persistent paths with deliberate ownership. | A relative path may place data in an unexpected working directory. |
| Public origin | Set it after the public address is known and tested. | Redirects, cookies, and callbacks may continue using localhost. |
Place persistent data deliberately
Application data should not depend on a temporary directory, dependency cache, or source checkout that is replaced during deployment. Define a persistent path, its owner, backup procedure, retention policy, and restoration steps. Grant Deno access only to the required directory when the application reads or writes files.
Databases, queues, object stores, and provider-specific services require separate migration plans. Similar method names do not guarantee identical consistency, transaction, indexing, or failure behavior. Test the replacement under real application workflows before moving production traffic.
Verify the service locally before tunneling it
Local verification separates application problems from tunnel problems. Keep the server running in the foreground with the scoped command, then open a second terminal.
Start the exact deployment command
Run PORT=8000 deno run --allow-env=PORT --allow-net=127.0.0.1:8000 main.ts and keep its output visible.
Call the health route
Request http://127.0.0.1:8000/health using the same address and port that the tunnel will target.
Test normal and negative behavior
Check the root route and a missing route. For a real application, also test authentication, reads, a reversible write, assets, uploads, and dependencies.
Restart and repeat
Stop and restart the process, then confirm that required data survives and no undeclared interactive setup is needed.
Run these checks:
curl -i http://127.0.0.1:8000/health
curl -i http://127.0.0.1:8000/
curl -i http://127.0.0.1:8000/not-found
The health response body should be:
{"ok":true}
The root route should return Deno app is running, and the final request should return HTTP status 404. For a production application, compare status codes, headers, redirects, session behavior, and data changes with the baseline recorded from the old deployment.
If the Localtonet client runs on another device, 127.0.0.1 refers to that device rather than the Deno host. You would need to bind the application to a suitable LAN interface and use the Deno host's reachable LAN address as the tunnel target. That changes the exposure boundary, so apply appropriate host firewall and authentication controls.
Run the Deno application under systemd
The following configuration gives the example a dedicated system account, fixed working directory, protected environment file, automatic startup, restart-on-failure behavior, and journal logging. Run these commands only after the foreground command has passed local verification.
1. Create a dedicated service account
sudo useradd --system \
--create-home \
--home-dir /opt/deno-app \
--shell /usr/sbin/nologin \
denoapp
This account owns the runtime and application but cannot be used as a normal interactive login.
2. Install Deno for the service account
sudo -u denoapp -H sh -c 'curl -fsSL https://deno.land/install.sh | sh'
Verify the exact executable that systemd will use:
sudo -u denoapp /opt/deno-app/.deno/bin/deno --version
3. Install the application files
sudo install -d -o denoapp -g denoapp -m 0750 /opt/deno-app/app
sudo install -o denoapp -g denoapp -m 0640 main.ts /opt/deno-app/app/main.ts
For a real project, deploy the complete tested release instead of only main.ts. Preserve its directory layout, configuration, static assets, and lock data.
4. Create the environment file
printf '%s\n' 'PORT=8000' | sudo tee /etc/deno-app.env > /dev/null
sudo chown root:root /etc/deno-app.env
sudo chmod 0600 /etc/deno-app.env
Add only variables required by the application. Avoid placing secrets directly in the unit file because unit definitions are commonly inspected during troubleshooting. Protect the environment file and restrict who can read service configuration.
5. Create the systemd unit
[Unit]
Description=Self-hosted Deno HTTP application
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=denoapp
Group=denoapp
WorkingDirectory=/opt/deno-app/app
EnvironmentFile=/etc/deno-app.env
ExecStart=/opt/deno-app/.deno/bin/deno run --allow-env=PORT --allow-net=127.0.0.1:8000 /opt/deno-app/app/main.ts
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.target
Save this as /etc/systemd/system/deno-app.service. The sample does not write files, so ProtectSystem=strict is suitable. If your application needs persistent file writes, create a dedicated data directory and adapt the service with a narrowly scoped writable path rather than disabling protection without review.
6. Enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable --now deno-app.service
sudo systemctl status deno-app.service
The status should report an active service. Verify the application through systemd:
curl -i http://127.0.0.1:8000/health
7. Inspect logs and test recovery
sudo journalctl -u deno-app.service -n 100 --no-pager
sudo systemctl restart deno-app.service
curl -i http://127.0.0.1:8000/health
During a maintenance window, reboot the host and repeat the health check. This proves that the service does not depend on an interactive terminal or login shell.
Routine service commands
sudo systemctl start deno-app.service
sudo systemctl stop deno-app.service
sudo systemctl restart deno-app.service
sudo systemctl status deno-app.service
sudo journalctl -u deno-app.service -f
Record the deployed application revision, Deno version, deployment time, database migration state, and rollback artifact for every release. Review logs for failures, but avoid logging passwords, authorization headers, session tokens, private request bodies, or Localtonet authentication tokens.
Publish the verified service with Localtonet
Install the Localtonet client on the Deno host so that the tunnel can target 127.0.0.1:8000. The supported client package and launch procedure depend on the operating system and client version, so use the Linux installation option presented by the current Localtonet application or documentation rather than copying an installer intended for another platform.
The maintained setup path is:
- Sign in to Localtonet and obtain the current client for the Linux host.
- Install and launch that client using the Linux instructions presented with the download.
- Authenticate the device with its own device-specific token.
- Confirm in the dashboard that the intended device is connected.
Do not place the device token in source files, service examples, screenshots, or support messages. If you run the Localtonet client as a background service, use only the startup mechanism supported by the installed client version. Do not invent a systemd command or client flag from an unrelated version.
Once the client is installed and connected, follow the documented HTTP tunnel sequence below.
Install and run the Localtonet client
Install our Linux client on the Deno host, launch it, authenticate the device, and confirm that it appears connected in the dashboard.
Open the HTTP tunnel page
In the Localtonet dashboard, open the HTTP tunnel configuration for the locally verified web service.
Select Process Type
Choose Random Sub Domain, Custom Sub Domain, or Custom Domain according to the options currently available for your workflow. These process types publish the content at a public HTTPS address.
Select AuthToken and server
Select the AuthToken belonging to the connected Linux device, then choose an available relay server from the current dashboard. Do not guess or hardcode token or server values.
Enter the local IP address and port
For this deployment, enter 127.0.0.1 as the local IP address and 8000 as the local port. These values must match the endpoint already verified with curl.
Press Start and test the assigned address
Start the tunnel, wait until it is running, and open the assigned public HTTPS address. Creating or saving the tunnel alone does not start it.
See our HTTP tunnel documentation alongside the workflow above for the current interface. The dashboard is the authoritative place to obtain the available server values and assigned public address.
Test the same health route locally and publicly:
curl -i http://127.0.0.1:8000/health
curl -i https://your-assigned-public-address.example/health
Replace the second URL with the address shown in your dashboard. The example hostname is only a placeholder. Compare the status, content type, body, redirects, cookies, and application behavior across both requests.
Do not expose development consoles, database browsers, debug routes, administrative panels, or unauthenticated write APIs merely because they are easy to tunnel. Require application-level authentication and authorization, validate input, apply least privilege, and expose only intended public functionality.
The Deno service, selected Localtonet client, and HTTP tunnel must all be running. A saved tunnel is not automatically active. Stopping the tunnel or disconnecting its selected device makes the public route unavailable.
Cut over traffic with a tested rollback path
Tunnel creation begins public testing. It does not complete the migration. Keep the previous deployment available while validating the replacement whenever both environments can coexist safely.
Run a pre-cutover test
- Test health and readiness behavior.
- Verify authentication, logout, session expiry, and account recovery.
- Exercise expected HTTP methods, routes, query strings, and request bodies.
- Check static assets, downloads, uploads, and generated links.
- Test database reads and a reversible write.
- Verify outbound API, email, storage, and payment integrations as applicable.
- Test inbound webhooks with a non-production integration first.
- Update and test OAuth callbacks and allowed origins.
- Restart the Deno service and confirm the public route recovers.
- Verify failure behavior when a dependency becomes unavailable.
Prevent duplicate processing
Running old and new HTTP processes simultaneously may be safe for read-only tests. It can be dangerous when both execute scheduled jobs, consume queues, send notifications, or write to separate database copies. Decide which deployment owns each write path during every phase.
If data was copied rather than shared, define a final synchronization or maintenance window. Writes accepted by the old deployment after the final copy will otherwise be absent from the replacement.
Update dependent endpoints carefully
External systems may need the new public address. Update webhook destinations, OAuth redirect URLs, CORS origin lists, application base URLs, generated-link settings, and client configuration as applicable. Change one integration at a time and verify delivery.
For a custom domain, check the current Localtonet dashboard and documentation before changing DNS. Exact record and validation requirements can change and should not be inferred from an old configuration.
Write rollback triggers before cutover
Define the conditions that cause rollback, such as failed authentication, missing data, repeated process restarts, dependency errors, failed callbacks, or inability to restore service within the maintenance window. Identify who can authorize the rollback.
| Phase | Primary action | Fallback |
|---|---|---|
| Local validation | Test the systemd service on loopback. | Fix the host without affecting the old public service. |
| Public validation | Test the assigned Localtonet address. | Stop the new tunnel while retaining the old endpoint. |
| Integration update | Move callbacks and clients in controlled groups. | Restore their previous endpoint settings. |
| Traffic cutover | Direct intended users to the replacement. | Return traffic to the old deployment when triggers are met. |
| Decommissioning | Retire the old service after observation. | Retain tested backups and deployment records. |
A rollback may involve restoring the old endpoint, re-enabling its workers, reverting callbacks, and reconciling writes accepted by the replacement. Do not delete the previous environment, revoke its credentials, or discard its final backup until the new deployment has passed the agreed observation period.
Troubleshoot Deno, systemd, and tunnel failures
Deno is not found
Run deno --version and command -v deno in your shell. For the systemd path in this tutorial, verify /opt/deno-app/.deno/bin/deno --version. A service does not automatically inherit the executable search path from your interactive shell, which is why the unit uses an absolute path.
The application reports a permission error
Identify whether the failure concerns an environment variable, network endpoint, file, directory, or subprocess. Grant only that resource. The sample requires --allow-env=PORT and --allow-net=127.0.0.1:8000. A real application may need additional scoped permissions.
The systemd service will not start
Inspect its status and recent journal:
sudo systemctl status deno-app.service
sudo journalctl -u deno-app.service -n 100 --no-pager
Check the executable path, working directory, source-file ownership, environment-file permissions, and ExecStart command. If you changed the unit, run sudo systemctl daemon-reload before restarting it.
The port is already in use
Another process may be listening on port 8000, or an old application instance may still be running outside systemd. Identify the listener with operating-system networking tools. Stop the unintended process or select another port, then update the environment file, Deno permission, health checks, systemd unit, and tunnel target consistently.
Localhost returns connection refused
Confirm that deno-app.service is active and inspect the journal for startup errors. Verify that the server is bound to 127.0.0.1:8000. A request to that address will fail if the application is listening on another port, another address family, or another machine.
The service starts manually but fails under systemd
Compare the manual working directory, executable path, environment variables, file permissions, and command flags with the unit. An interactive shell may provide variables or path entries that systemd does not. Keep the unit explicit rather than importing an entire login environment.
The app works locally but not through Localtonet
Confirm that the Localtonet client is connected, the correct AuthToken is selected, and the HTTP tunnel was started. Check that the local target is exactly 127.0.0.1 and 8000. If the client is on another device, loopback cannot reach the Deno host.
The public address redirects to localhost
Review the application's public-origin, canonical-host, callback, and URL-generation settings. Do not hardcode a temporary public address in source. If a framework interprets forwarded request headers, follow that framework's current proxy-trust documentation before enabling trust.
Authentication cookies fail publicly
Check cookie domain, path, secure, and same-site attributes together with the application's configured public origin. A cookie scoped to localhost or the old hostname does not automatically apply to the assigned public hostname.
Data disappears after restart
The application may be using a transient directory, unexpected working directory, in-memory store, or unmounted storage location. Stop accepting writes if necessary, determine the actual path, restore from a tested backup, and move persistent data to a deliberate location with documented ownership.
The service repeatedly restarts
The systemd policy restarts only after failure, so repeated starts usually indicate an application error. Inspect the first failure in the journal rather than only the final restart message. Common causes include invalid configuration, missing files, unreachable dependencies, permission errors, and a port conflict.
The tunnel disappears after a reboot
First confirm that deno-app.service recovered and the local health route works. Then confirm that the Localtonet client has started using the supported startup behavior for the installed version and that the selected device is connected. Finally, check whether the tunnel itself is running. The client reconnecting does not mean every saved tunnel has necessarily been started.
Frequently asked questions
Can I self-host a Deno application without rewriting it?
Often, yes, when it already runs as a conventional Deno process and its dependencies are reachable from the new host. You still need to reproduce the runtime, permissions, environment, data, working directory, and startup behavior. Provider-specific storage, queues, schedules, identity services, and deployment APIs may require separate migration work.
Is self-hosting Deno the same as migrating from Deno Deploy?
No. Self-hosting runs the open-source runtime on infrastructure you operate. A Deno Deploy migration may also need replacements for managed services and platform behavior. Inventory those dependencies before moving the HTTP process.
Are April 9 and October 9, 2027 confirmed Deno deadlines?
Not from the evidence available for this revision. They are nominal calculations from a secondary report describing six-month and twelve-month periods beginning October 9, 2026. Confirm the primary Deno and Cloudflare announcements and any later updates before treating a calendar date as authoritative.
Does Localtonet move my application or database?
No. You install and run the application on infrastructure you control. Databases and other dependencies must be retained or migrated separately. Our HTTP tunnel publishes the service that is already running at the configured local target.
Do I need router port forwarding or a public IP address?
No. The Localtonet client establishes an outbound connection to our relay server. The tunnel can provide a public URL without inbound router port forwarding, firewall changes, VPN setup, or a public IP address.
Why bind Deno to 127.0.0.1?
When Deno and the Localtonet client run on the same host, loopback allows the client to reach the application without making the application listen directly on LAN interfaces. If the client runs elsewhere, use a reachable LAN address and protect that additional network exposure.
Does the sample require unrestricted Deno permissions?
No. It receives permission to read only the PORT environment variable and use only 127.0.0.1:8000 for networking. Add other permissions only when the real application requires them.
Is saving a Localtonet tunnel enough to make it available?
No. The tunnel must be started, the selected client device must be connected, and the Deno service must be running at the configured local target. You can later stop or delete the tunnel when access is no longer needed.
Does the public HTTPS address replace authentication?
No. Protect sensitive routes with application-level authentication and authorization. Remove development endpoints, apply least privilege, validate input, and expose only the functionality intended for remote users.
What should I preserve for rollback?
Preserve the previous deployment, exact source revision, runtime version, protected configuration, tested data backup, database migration state, and previous integration endpoint settings. If both environments accept writes, define how those writes will be reconciled.
Publish your verified Deno service with Localtonet
Install and supervise the application first, confirm its loopback health endpoint, then create an HTTP tunnel to the tested local address and port. With Localtonet, the service can receive requests through a public HTTPS address without inbound router port forwarding or a public IP.
Get Started Free โ