How Instatic Deployment to Railway and Render Works: Complete Environment Variable Guide
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 and 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 to8080.DATABASE_URL– Connection string for the database. Usesqlite:/app/storage/data/cms.dbfor 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. Usehttps://${{RAILWAY_PUBLIC_DOMAIN}}to automatically resolve Railway's generated domain.RAILWAY_RUN_UID– Critical for Railway: Set to0to run the container as root. Railway mounts volumes with root ownership, and without this setting, SQLite operations and file uploads will fail withEACCESpermission 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 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, 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 to8080.DATABASE_URL– Database connection string. Usesqlite:/app/storage/data/cms.dbfor 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 providesRENDER_EXTERNAL_URL, so usehttps://${{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, 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:
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 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/instaticimage using environment variable configuration rather than build-time settings. - Railway requires
RAILWAY_RUN_UID=0because it mounts volumes as root; Render works with the default non-root user. - Persistent storage must be mounted at
/app/storageon both platforms to preserve SQLite data and uploaded media across restarts. PUBLIC_ORIGINmust match your HTTPS domain for CSRF validation to succeed on both platforms.INSTATIC_SECRET_KEYrequires exactly 43 base-64 characters and can be generated usingscripts/generate-secret-key.ts.- Health checks should use the
/healthendpoint 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →