Railway Deployment
Deploy LinkDen on Railway with managed infrastructure, automatic SSL, and simple scaling.
Railway Deployment
Railway is a cloud platform that makes it easy to deploy applications with automatic builds, managed databases, and built-in SSL. This guide covers deploying both the LinkDen API and web app on Railway.
Why Railway?
- Simple deployment: Connect a GitHub repo and Railway handles builds and deploys.
- Managed infrastructure: No servers to configure or maintain.
- Automatic SSL: Custom domains get free SSL certificates instantly.
- Built-in databases: One-click SQLite-compatible volumes or PostgreSQL.
- Generous free tier: $5 free credit/month on the Trial plan; Hobby plan starts at $5/month.
- Preview environments: Automatic deploys for pull requests.
Architecture on Railway
Railway runs each service as a separate container. LinkDen uses two services plus a persistent volume for the SQLite database:
┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐
│ Railway Service: │────▶│ Railway Service: │────▶│ Volume Mount │
│ Web (Next.js │ │ API (Hono on │ │ (SQLite DB) │
│ static + Caddy) │ │ Node.js) │ │ │
└──────────────────┘ └──────────────────┘ └──────────────┘Prerequisites
- A Railway account (GitHub sign-in recommended).
- The LinkDen repository forked to your GitHub account (or a private repo connected to Railway).
- Clerk API keys from clerk.com.
- Node.js 20+ and pnpm 9+ installed locally (for initial setup).
Step 1: Create a Railway Project
- Log in to the Railway dashboard.
- Click New Project.
- Select Empty Project.
- Name it
LinkDen.
Step 2: Deploy the API Service
Add the Service
- In your project, click New > GitHub Repo.
- Select the LinkDen repository.
- Railway detects the repo and begins a build. Cancel this initial build -- we need to configure it first.
Configure the Build
- Click on the service to open its settings.
- Go to Settings:
- Service Name:
linkden-api - Root Directory: Leave empty (uses repo root)
- Build Command: Leave empty (uses Dockerfile)
- Dockerfile Path:
Dockerfile.api
- Service Name:
If you have not created Dockerfile.api yet, see the Docker guide for the complete Dockerfile. Alternatively, use a Nixpacks build:
Alternative: Nixpacks Build (No Dockerfile)
Railway supports Nixpacks for automatic builds. Create a railway.json in the repo root:
{
"build": {
"builder": "NIXPACKS"
},
"$schema": "https://railway.app/railway.schema.json"
}And a nixpacks.toml for the API:
[phases.setup]
nixPkgs = ["nodejs_20", "sqlite"]
[phases.install]
cmds = ["corepack enable", "corepack prepare pnpm@10.29.3 --activate", "pnpm install --frozen-lockfile"]
[phases.build]
cmds = ["echo 'Build complete'"]
[start]
cmd = "npx tsx apps/server/src/node-entry.ts"However, the Dockerfile approach gives you more control and is recommended.
Add a Persistent Volume
SQLite needs a persistent volume so data survives redeployments:
- Click on the
linkden-apiservice. - Go to Volumes.
- Click New Volume.
- Set the mount path to
/data. - Click Create.
Set Environment Variables
- Click on the
linkden-apiservice. - Go to Variables.
- Add each variable:
| Variable | Value |
|---|---|
CLERK_SECRET_KEY | sk_live_... |
CLERK_PUBLISHABLE_KEY | pk_live_... |
CORS_ORIGIN | https://yourdomain.com (update after setting up custom domain) |
APP_URL | https://yourdomain.com |
DATABASE_URL | file:/data/linkden.db |
PORT | 8787 |
RESEND_API_KEY | re_... (optional) |
RESEND_FROM_EMAIL | contact@yourdomain.com (optional) |
TURNSTILE_SECRET_KEY | 0x... (optional) |
- Click Deploy to trigger a build with the new configuration.
Expose the Service
- Go to Settings > Networking.
- Under Public Networking, click Generate Domain to get a Railway-provided URL (e.g.,
linkden-api-production.up.railway.app). - Note this URL -- you need it for the web app's
NEXT_PUBLIC_API_URL.
Step 3: Deploy the Web App
Add a Second Service
- In the same Railway project, click New > GitHub Repo.
- Select the same LinkDen repository again.
- Configure it as a separate service.
Configure the Build
- Click on the new service and go to Settings:
- Service Name:
linkden-web - Dockerfile Path:
Dockerfile.web
- Service Name:
Set Environment Variables (Build Args)
Since NEXT_PUBLIC_* variables must be present at build time, add them as variables:
| Variable | Value |
|---|---|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | pk_live_... |
NEXT_PUBLIC_API_URL | https://linkden-api-production.up.railway.app (or your custom API domain) |
NEXT_PUBLIC_SITE_URL | https://linkden-web-production.up.railway.app (or your custom domain) |
NEXT_PUBLIC_SITE_NAME | LinkDen |
NEXT_PUBLIC_TURNSTILE_SITE_KEY | (optional) |
CLERK_SECRET_KEY | sk_live_... |
PORT | 3001 |
Expose the Service
- Go to Settings > Networking.
- Click Generate Domain to get a public URL.
- Visit the URL to verify the web app is running.
Step 4: Set Up Custom Domains
Add a Custom Domain for the Web App
- Click on the
linkden-webservice. - Go to Settings > Networking > Custom Domain.
- Enter your domain (e.g.,
yourdomain.comorlinks.yourdomain.com). - Railway shows you a CNAME record to add at your DNS provider.
At your DNS provider, add the record:
| Type | Name | Target |
|---|---|---|
| CNAME | @ or subdomain | linkden-web-production.up.railway.app |
Add a Custom Domain for the API
- Click on the
linkden-apiservice. - Go to Settings > Networking > Custom Domain.
- Enter your API domain (e.g.,
api.yourdomain.com). - Add the CNAME record at your DNS provider.
Update Environment Variables
After setting up custom domains, update the URLs:
API service variables:
CORS_ORIGIN=https://yourdomain.com
APP_URL=https://yourdomain.comWeb service variables:
NEXT_PUBLIC_API_URL=https://api.yourdomain.com
NEXT_PUBLIC_SITE_URL=https://yourdomain.comRedeploy both services for changes to take effect.
SSL Certificates
Railway automatically provisions and renews SSL certificates for custom domains. No configuration is needed. SSL is active within minutes of DNS propagation.
Step 5: Database Setup
SQLite with Persistent Volume (Default)
The persistent volume mounted at /data stores the SQLite database file. This is the simplest approach and works well for single-instance deployments.
Ensure:
DATABASE_URL=file:/data/linkden.dbis set on the API service.- A volume is mounted at
/data.
PostgreSQL Alternative (Advanced)
If you prefer a managed PostgreSQL database for better concurrency:
- In the Railway project, click New > Database > PostgreSQL.
- Railway provisions a PostgreSQL instance and provides a
DATABASE_URL. - Copy the
DATABASE_URLto the API service variables.
This requires modifying packages/db to use the Drizzle PostgreSQL driver instead of SQLite. This is an advanced configuration and beyond the default LinkDen setup.
Step 6: Verify the Deployment
- Visit your web app URL and confirm the public link page loads.
- Navigate to
/adminand test the Clerk sign-in flow. - Create a test link in the admin panel.
- Visit the public page and verify the link appears.
- Check analytics data is being recorded.
Monitoring
Service Logs
- Click on a service in the Railway dashboard.
- Go to Deployments > click the latest deployment > Logs.
- View real-time application logs.
Metrics
Railway provides built-in metrics for each service:
- CPU usage
- Memory usage
- Network traffic
- Disk usage (for volumes)
Access metrics from the service dashboard.
Deploy Notifications
Railway can send notifications for deployments via:
- Slack (via Railway's integration)
- Webhooks
Configure under Project Settings > Integrations.
Updating LinkDen
Automatic Deployments
If your repository is connected via GitHub, Railway automatically redeploys when you push to the configured branch.
- Push changes to your repo.
- Railway detects the change and triggers a new build.
- The new version is deployed with zero downtime (rolling deployment).
Manual Redeployment
- Go to the service in the Railway dashboard.
- Click Deploy > Redeploy.
Rolling Back
If a deployment has issues:
- Go to the service > Deployments.
- Find the previous working deployment.
- Click the three-dot menu > Rollback.
Costs
Railway pricing (as of 2025):
| Plan | Cost | Resources |
|---|---|---|
| Trial | Free ($5 credit/month) | 512 MB RAM, shared CPU, 1 GB disk |
| Hobby | $5/month | 8 GB RAM, 8 vCPU, 100 GB disk |
| Pro | $20/month per seat | Team features, higher limits |
Usage-based pricing on top of the base plan:
- Compute: $0.000231/min per vCPU
- Memory: $0.000231/min per GB
- Network egress: $0.10/GB
- Volume storage: $0.25/GB per month
Typical LinkDen costs:
A single-user LinkDen instance with low traffic typically uses:
- ~0.1 vCPU average
- ~256 MB RAM per service
- < 1 GB volume storage
- Minimal network egress
This fits comfortably within the Hobby plan ($5/month) with minimal additional usage charges. The Trial plan's $5 free credit may cover it for light usage.
Troubleshooting
Build fails
- Check the build logs in Railway for specific errors.
- Ensure
Dockerfile.apiandDockerfile.webexist in the repository root. - Verify all build arguments are set as environment variables.
"502 Bad Gateway" or "Application failed to respond"
- The service may not be listening on the correct port. Ensure the
PORTenvironment variable matches the port your application listens on. - Check the deploy logs for startup errors.
- Verify the health check is passing.
Database resets on redeployment
- Ensure a persistent volume is attached to the API service at
/data. - Verify
DATABASE_URLpoints to a file inside the volume mount path.
CORS errors
CORS_ORIGINmust exactly match the web app's URL including protocol.- After switching to a custom domain, update
CORS_ORIGINand redeploy the API.
Custom domain shows "DNS not configured"
- Verify the CNAME record at your DNS provider.
- Wait for DNS propagation (can take up to 48 hours, usually minutes).
- Ensure the CNAME target matches what Railway shows in the dashboard.