# How Instatic Deployment to Railway and Render Works: Complete Environment Variable Guide

> Learn how Instatic deploys to Railway and Render. Understand essential environment variables like DATABASE_URL and PUBLIC_ORIGIN for seamless setup. Get your Instatic instance running fast.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: how-to-guide
- Published: 2026-07-27

---

**Instatic deploys to Railway and Render using the official Docker image `ghcr.io/corebunch/instatic` and requires specific environment variables—`DATABASE_URL`, `PUBLIC_ORIGIN`, `INSTATIC_SECRET_KEY`, and volume mounts at `/app/storage`—with Railway needing additional root-user permissions via `RAILWAY_RUN_UID` that Render does not require.**

Instatic is an open-source content management system that supports one-click deployment to managed platforms. According to the Instatic source code in [`docs/deployment/railway.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/deployment/railway.md) and [`docs/deployment/render.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/deployment/render.md), both platforms run identical containerized images but handle filesystem permissions and service discovery differently, requiring distinct environment variable configurations for proper database access and security.

## How Instatic Deployment to Railway Works

Railway deploys Instatic by pulling the `ghcr.io/corebunch/instatic:<version>` image and injecting configuration through environment variables at runtime. The platform handles TLS termination at the edge, so the container receives plain HTTP traffic while `PUBLIC_ORIGIN` ensures secure CSRF validation.

### Required Railway Environment Variables

Configure these variables in your Railway service settings:

- **`PORT`** – The TCP port the server binds to. Set to `8080`.
- **`DATABASE_URL`** – Connection string for the database. Use `sqlite:/app/storage/data/cms.db` for SQLite or `${{Postgres.DATABASE_URL}}` when using Railway's managed Postgres.
- **`UPLOADS_DIR`** – Absolute path for media storage. Set to `/app/storage/uploads`.
- **`STATIC_DIR`** – Path to built frontend assets. Set to `/app/dist`.
- **`INSTATIC_SECRET_KEY`** – 32-byte base-64 encryption key for cookies and tokens. Railway can generate this via secret expressions: `${{secret(43, "...")}}=`.
- **`PUBLIC_ORIGIN`** – The public HTTPS URL of your deployment. Use `https://${{RAILWAY_PUBLIC_DOMAIN}}` to automatically resolve Railway's generated domain.
- **`RAILWAY_RUN_UID`** – **Critical for Railway**: Set to `0` to run the container as root. Railway mounts volumes with root ownership, and without this setting, SQLite operations and file uploads will fail with `EACCES` permission errors.
- **`TRUSTED_PROXY_CIDRS`** (optional) – Railway's ingress proxy CIDR range if you need accurate client IP addresses in logs.

### Railway Volume Configuration

Railway requires a persistent volume mounted at `/app/storage`. This path stores the SQLite database file (if used) and all uploaded media. The `RAILWAY_RUN_UID=0` setting in [`docs/deployment/railway.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/deployment/railway.md) ensures the Node.js process can write to this root-owned mount point.

## How Instatic Deployment to Render Works

Render uses a similar container-based approach documented in [`docs/deployment/render.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/deployment/render.md), but handles user permissions and disk ownership differently than Railway.

### Required Render Environment Variables

Set these environment variables in your Render service dashboard:

- **`PORT`** – The port Render forwards to your container. Set to `8080`.
- **`DATABASE_URL`** – Database connection string. Use `sqlite:/app/storage/data/cms.db` for SQLite on the attached disk, or `${{POSTGRES_URL}}` when using Render's managed Postgres.
- **`UPLOADS_DIR`** – Path for uploaded files. Must be on the persistent disk: `/app/storage/uploads`.
- **`STATIC_DIR`** – Location of static assets: `/app/dist`.
- **`INSTATIC_SECRET_KEY`** – 43-character base-64 secret for encryption. Generate this via Render's secret generator or locally using the project's script.
- **`PUBLIC_ORIGIN`** – Your site's public HTTPS origin. Render provides `RENDER_EXTERNAL_URL`, so use `https://${{RENDER_EXTERNAL_URL}}` or your custom domain.
- **`TRUSTED_PROXY_CIDRS`** (optional) – Set to Render's proxy CIDRs if you need real client IPs for rate limiting.

### Render Disk and User Configuration

Unlike Railway, **Render does not require `RAILWAY_RUN_UID`**. The Render disk attachment system aligns ownership with the container's default non-root user, allowing file writes without root privileges. Mount your persistent disk at `/app/storage` to maintain consistency with the SQLite and uploads paths.

## Common Configuration Patterns

Both platforms share several critical configuration requirements regarding database setup, secret generation, and CSRF handling.

### Database Connection Setup

In [`server/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/index.ts), the application reads `DATABASE_URL` during startup and automatically runs database migrations before binding to the HTTP port. For SQLite deployments, ensure the directory `/app/storage/data` exists on your volume—create it during initialization if needed. For Postgres, the connection string must include full credentials and the database name provided by the managed service.

### Secret Key Generation

The `INSTATIC_SECRET_KEY` must be exactly 43 base-64 characters (representing 32 bytes of entropy). Generate a valid key locally before deploying:

```bash
bun run scripts/generate-secret-key.ts

```

Copy the output into your platform's secret management system. Both Railway and Render provide built-in secret generators that produce cryptographically secure values compatible with this requirement.

### CSRF and Public Origin Handling

Both Railway and Render terminate TLS at their edge routers, forwarding plain HTTP to your container. The [`src/__tests__/server/publicForms.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/publicForms.test.ts) test suite verifies that Instatic uses `PUBLIC_ORIGIN` to validate request origins during CSRF checks. Failure to set this variable results in 403 errors on form submissions when accessed via HTTPS custom domains.

Configure health checks on both platforms to target the `/health` endpoint, which returns a 200 status when the database connection and migrations are complete.

## Summary

- **Railway and Render** both deploy the `ghcr.io/corebunch/instatic` image using environment variable configuration rather than build-time settings.
- **Railway requires `RAILWAY_RUN_UID=0`** because it mounts volumes as root; Render works with the default non-root user.
- **Persistent storage** must be mounted at `/app/storage` on both platforms to preserve SQLite data and uploaded media across restarts.
- **`PUBLIC_ORIGIN`** must match your HTTPS domain for CSRF validation to succeed on both platforms.
- **`INSTATIC_SECRET_KEY`** requires exactly 43 base-64 characters and can be generated using [`scripts/generate-secret-key.ts`](https://github.com/CoreBunch/Instatic/blob/main/scripts/generate-secret-key.ts).
- **Health checks** should use the `/health` endpoint to verify database connectivity before routing traffic.

## Frequently Asked Questions

### What Docker image does Instatic use for Railway and Render deployments?

Instatic uses `ghcr.io/corebunch/instatic:<version>` for both platforms. The image is immutable and read-only at runtime, requiring all configuration—including database connections and security keys—to be provided via environment variables.

### Why does Railway require RAILWAY_RUN_UID=0 while Render does not?

Railway mounts persistent volumes with root ownership permissions, forcing the container to run as root (UID 0) to write SQLite files and uploaded media. Render's disk attachment system properly aligns ownership with the container's default non-root user, eliminating the need for elevated privileges.

### How do I generate a valid INSTATIC_SECRET_KEY?

Run `bun run scripts/generate-secret-key.ts` from the repository root to generate a cryptographically secure 43-character base-64 string. Alternatively, use the built-in secret generation tools in the Railway or Render dashboards, ensuring the output is exactly 43 characters long.

### Can I use Postgres instead of SQLite on these platforms?

Yes. Both platforms support managed Postgres services. For Railway, create a Postgres service and reference its connection string via `${{Postgres.DATABASE_URL}}`. For Render, create a Postgres instance and use `${{POSTGRES_URL}}`. The uploads directory remains on the persistent disk mount at `/app/storage/uploads` regardless of database choice.