If you also want a database UI on your tailnet, start with Drizzle Studio and Gateway on Railway with Tailscale. Its templates come with the same Tailscale service.
Connect Drizzle Kit to a private Railway database with Tailscale
This tutorial connects drizzle-kit on your device to a PostgreSQL database on Railway that has public access turned off.
You add a Tailscale service to the project that holds the database. Every device on your tailnet can then access the database.
-
You have a Railway account.
-
You have a Tailscale account.
-
You have the Tailscale client installed on the device you work from, and that device is signed in to your tailnet. You can find the downloads on the Tailscale website.
-
MagicDNS is on in your tailnet, so that the machine name resolves. Tailnets created on or after 2022-10-20 have it on by default. If your tailnet is older, check the DNS page.
-
You have installed Drizzle ORM and Drizzle Kit.
npm i drizzle-orm@rc
npm i -D drizzle-kit@rc
How it works
Railway gives every service a private domain that ends in .railway.internal. Those domains resolve only inside the project, so drizzle-kit on your device cannot use them.
The Tailscale service connects the two networks. It runs in the same Railway project, so it resolves the private domains. It also joins your tailnet as a machine, so your own devices reach it by its machine name. Traffic goes from drizzle-kit over the tailnet to that machine, and from there to the database over Railway’s private network.
Set up the Railway project
Create a project with a PostgreSQL database that has no public access. If your project already has a database, skip to Add the Tailscale service.
Create a Railway project
Log in to your Railway dashboard and click the New Project button.

Provision a PostgreSQL database
On the project canvas, click the New button in the top right corner and select Database → PostgreSQL. You can also use the command palette ( ⌘K CtrlK ) and search for PostgreSQL.

Keep the database on the private network
Click on the PostgreSQL service, go to the Settings tab, and find the Networking section. Verify that public access is disabled. Under Private Networking you will see the private domain of the service, for example postgres.railway.internal.

Add the Tailscale service
Deploy the Tailscale for Drizzle template into the project that holds your database.
If your project already runs Drizzle Studio with Tailscale or Drizzle Gateway with Tailscale template, it already has a Tailscale service, and you can skip this section. Open that Tailscale service, go to the Variables tab, and set TS_FORWARD_DATABASE: type ${{ and pick RAILWAY_PRIVATE_DOMAIN of your database service. Click Deploy in the top bar and wait for 5432: ready in the new deploy log. The machine name is the first part of the address in the open line, for example drizzle-studio.
Open the template
Press the button below to open the Tailscale for Drizzle template.
Don’t deploy it right away. Set the variables first.
Open the Deploy to dropdown and select the project that holds your database.

Set the variables
| Variable | What to set |
|---|---|
TS_FORWARD_DATABASE | The field is empty. Pick your database service from the list, then pick RAILWAY_PRIVATE_DOMAIN. For a service named Postgres the field then reads ${{Postgres.RAILWAY_PRIVATE_DOMAIN}}. |
TS_HOSTNAME | Change it to railway-postgres, or pick another name for the machine. You connect to the database by this name. |
Some variables, such as TS_HOSTNAME, are in the collapsed Pre-Configured Environment Variables section of the card. Expand it to see or change them.
Deploy the template
On the service card, click Save Config. Then click Review and deploy. Railway opens your project with the template in a side panel. Click Deploy Template there.
Sign in from the deploy log
Open the Tailscale service, then its Deployments tab, and read the deploy log. The last lines of the log hold a sign-in link:
This service is not on your tailnet yet. Open this link and sign in
with the account that owns your tailnet:
https://login.tailscale.com/a/xxxxxxxxxxxxOpen the link and sign in. Anyone who opens that link first gets this machine on their tailnet, so do not forward it. The log is visible only to members of your Railway project.

If your tailnet requires device approval, the log says so and the machine waits until you approve it on the Machines page.
Check the forwarding rule
A few seconds after you sign in, the same log prints:
joined tailnet: example.com
5432: ready
Check that the tailnet name is yours. If it is not, someone else opened the sign-in link first, and their tailnet can now reach this service. To take the service back:
- On the project canvas, click the volume under the Tailscale service, open its
Settingstab, and clickWipe volume. - Type the confirmation text and click
Wipe. Railway deletes the machine’s key and redeploys the service. - Open the sign-in link from the new deploy log yourself.
- Change the database user’s password.
Find the machine name
Open the Machines page. The new machine has the name you set in TS_HOSTNAME. If your tailnet already had a machine with that name, the new name ends in -1, -2 and so on. Use the name that the Machines page shows.
Connect Drizzle Kit from your device
Point drizzle-kit at the machine name and apply your schema.
Set the connection string
Open the PostgreSQL service in Railway, go to the Variables tab, and copy DATABASE_URL.

Replace the ***.railway.internal host with the machine name and keep the rest of the string. Put the result in a .env file:
DATABASE_URL=postgresql://postgres:PASSWORD@MACHINE_NAME:5432/railwayReplace the following:
PASSWORD: the password from theDATABASE_URLthat you copiedMACHINE_NAME: the machine name from the Machines page, for examplerailway-postgres
Tailscale encrypts the connection between your device and the Tailscale service, and Railway encrypts the hop between the Tailscale service and the database. Railway’s PostgreSQL also accepts TLS through the Tailscale service, so most clients connect with their default settings.
Set up the Drizzle config file
Create a drizzle.config.ts file in the root of your project:
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/schema.ts",
out: "./migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});Apply your schema
Generate migrations and apply them:
npx drizzle-kit generatenpx drizzle-kit migrateFor rapid prototyping you can push the schema directly with drizzle-kit push. For production deployments, prefer the generate and migrate workflow, so you keep a versioned history of schema changes. See the Drizzle migrations fundamentals page for details.
Check the tables in Drizzle Studio
Open the database in Drizzle Studio:
npx drizzle-kit studioIt runs on your device and reads the same DATABASE_URL, so it reaches the database through the Tailscale service. Your new tables are there.

For occasional access you do not need Tailscale at all. The Railway CLI opens a temporary tunnel to a private database with railway connect Postgres --tunnel-only. If you need a permanent route, or access from every device on your tailnet, use the Tailscale service.
Use other ports
By default, TS_FORWARD_DATABASE uses port 5432, on the machine and on the database service. To use other ports, write the value in the form [port:]host[:port]:
| Value | Port on the machine | Port on the service |
|---|---|---|
host | The default port | The default port |
host:port | port | port |
source_port:host:target_port | source_port | target_port |
For example, 5433:${{Postgres.RAILWAY_PRIVATE_DOMAIN}}:5432 forwards port 5433 on the machine to port 5432 of the database service. Your connection string then uses port 5433.
Disable key expiry
A machine that joined by signing in has its key expire on your tailnet’s schedule, 180 days by default. When that happens, the Tailscale service prints a new sign-in link and waits. The machine is unreachable until you click it.
To avoid that, open the Machines page once, find the machine, open the menu at the far right of its row, and choose Disable key expiry.

If you automate deployments and want no sign-in at all, set TS_AUTHKEY on the Tailscale service:
- For an auth key, set
TS_AUTHKEYto the key. - For an OAuth client secret, set
TS_AUTHKEYto the secret with?ephemeral=false&preauthorized=trueappended. Without that suffix, Tailscale removes the machine when it disconnects, and the machine waits for approval before it joins. Also setTS_TAGSto a tag you own, for exampletag:drizzle. A tagged machine never has its key expire.
The Tailscale service reads TS_AUTHKEY only before the machine joins. If the machine has already joined, remove the machine and wipe the volume first.
Troubleshoot the Tailscale setup
Open the entry that matches what you see.
The error names a host that ends in .railway.internal
The DATABASE_URL you copied from Railway still carries the private domain, which resolves only inside the project. Replace that host with the Tailscale machine name and keep the rest of the string, as in Set the connection string.
drizzle-kit cannot connect
Run tailscale status on your device and check that it is signed in to the same tailnet the log named. Then open the Tailscale service’s deploy log and find the cause:
- A line starts with
5432:and ends in a network error, such asconnect: connection refusedorno such host. The variable is correct, and the database is unreachable on its private domain. Check that the database service is running and has private networking on. If the database listens on another port, add the port to the value, for example5432:${{Postgres.RAILWAY_PRIVATE_DOMAIN}}:5433. - No line starts with
5432:orTS_FORWARD_DATABASE. The variable is empty, or its reference names a service outside your project. Set it on the Tailscale service’sVariablestab and clickDeployin the top bar. - A line starts with
TS_FORWARD_DATABASE. The next entry explains that line. - The log shows
5432: ready, and the machine name does not resolve. Check that MagicDNS is on, on the DNS page. If it is on, use the machine’sFull domainvalue from the Machines page as the host.
The log prints a line that ends in skipped
The line starts with the variable name, then gives the reason:
does not match [port:]host[:port]: the value has more than two colons.has an empty target host: the value has a port but no host. A reference that names a service outside your project resolves to an empty host.still contains a Railway reference that did not resolve: Railway did not replace the reference. Check the service name inside it.has a port outside 1-65535: a port is not a number from 1 to 65535.reuses port: an earlier forward already uses that port on the machine.uses port 443, which the https listener owns: pick another port on the machine. The address of the database UI already uses 443 when HTTPS certificates are on.
Fix the value on the Tailscale service’s Variables tab and click Deploy in the top bar.
A new sign-in link appeared in the log
The service restarted, for example after the key expired or the machine was removed. Open the newest link. The link works once.
The log says the machine is waiting for approval
Your tailnet has device approval turned on. Open the Machines page and approve it.
It worked for months and then stopped
The machine’s key expired. Open the Tailscale service’s deploy log; it is printing a fresh sign-in link. Click it, then disable key expiry on the machine so it does not happen again.
The machine name gained a -1 suffix
Your tailnet already had a machine with this name, for example from an earlier deploy. Tailscale keeps the suffix. If you want the short name, rename the machine on the Machines page. Otherwise, use the name that the Machines page shows.
Next steps
- Drizzle migrations fundamentals to choose between
generate,migrateandpush. - Drizzle Studio and Gateway on Railway with Tailscale to browse the database from your tailnet.