How to Deploy Instatic to Railway, Render, or a Custom VPS

Deploy Instatic using the official Docker image (ghcr.io/corebunch/instatic) by configuring the DATABASE_URL, INSTATIC_SECRET_KEY, and persistent storage paths, then apply platform-specific settings such as RAILWAY_RUN_UID for Railway or Docker Compose overlays for VPS environments.

Instatic is a self-hosted CMS that runs inside a Bun container, packaging the server runtime, admin SPA, and automatic database migrations into a single deployable unit. Regardless of whether you target Railway, Render, or your own server, the application boots from the same image, reads configuration parsed in server/config.ts, and initializes the database adapter defined in server/db/index.ts before serving traffic.

Deploying to Railway

Railway deploys Instatic using a Docker image source with persistent volumes attached at /app/storage. The platform requires specific environment variables to handle permissions and routing correctly.

Set PORT=8080 to match Railway's default, and configure RAILWAY_RUN_UID=0 to ensure the container runs as root, preventing permission errors when SQLite creates database files in the mounted volume.

  1. Create a new Railway project and add a Docker image service.

  2. Set the image to ghcr.io/corebunch/instatic:0.0.13 (or latest).

  3. Attach a volume with mount path /app/storage.

  4. Configure the following environment variables in the Railway dashboard:

PORT=8080
UPLOADS_DIR=/app/storage/uploads
STATIC_DIR=/app/dist
DATABASE_URL=sqlite:/app/storage/data/cms.db
INSTATIC_SECRET_KEY=${{secret(43, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/")}}=
PUBLIC_ORIGIN=https://${{RAILWAY_PUBLIC_DOMAIN}}
RAILWAY_RUN_UID=0
  1. Set the health check path to /health.

  2. Deploy the service. Railway automatically runs migrations and exposes the admin UI at https://<project>.railway.app/admin.

For PostgreSQL deployments, replace DATABASE_URL with ${{Postgres.DATABASE_URL}} after provisioning a Railway Postgres database, and remove the SQLite path from the volume configuration.

Deploying to Render

Render utilizes Blueprints (render.yaml) to provision the web service and persistent disks declaratively. The container listens on port 10000 and uses RENDER_EXTERNAL_URL for CSRF origin validation.

  1. Fork the template repository (e.g., corebunch/instatic-render-sqlite) or create a new Blueprint file.

  2. Click Deploy to Render or manually create a web service using the Docker image ghcr.io/corebunch/instatic:latest.

  3. Attach a persistent disk mounted at /app/storage.

  4. Configure the environment:

PORT=10000
UPLOADS_DIR=/app/storage/uploads
STATIC_DIR=/app/dist
DATABASE_URL=sqlite:/app/storage/data/cms.db
INSTATIC_SECRET_KEY=<generate-a-32-byte-key>
Health check path=/health
  1. For custom domains, add PUBLIC_ORIGIN as a comma-separated list including both RENDER_EXTERNAL_URL and your custom domain.

The Blueprint files in docs/deployment/render/sqlite/render.yaml and docs/deployment/render/postgres/render.yaml provide complete working templates for one-click deployments.

Deploying to a Custom VPS

For self-managed infrastructure, Instatic provides Docker Compose configurations that support SQLite or PostgreSQL backends, with optional TLS termination via Caddy.

First, generate a secret key using the provided helper script:

bun run scripts/generate-secret-key.ts

Copy the output into your environment file as INSTATIC_SECRET_KEY.

Docker Compose Configuration

The base configuration lives in compose.prod.yml, with environment-specific overrides:

Create a .env file based on .env.production.example:

INSTATIC_IMAGE=ghcr.io/corebunch/instatic:latest
PORT=3001
DATABASE_URL=sqlite:/app/data/cms.db
UPLOADS_DIR=/app/uploads
STATIC_DIR=/app/dist
INSTATIC_SECRET_KEY=<generated-key>
DOMAIN=cms.example.com
LETSENCRYPT_EMAIL=ops@example.com
PUBLIC_ORIGIN=https://cms.example.com

Deploy with the appropriate override files:


# SQLite only

docker compose -f compose.prod.yml -f compose.sqlite.yml up -d

# SQLite with TLS

docker compose -f compose.prod.yml -f compose.sqlite.yml -f compose.tls.yml up -d

# PostgreSQL with TLS

docker compose -f compose.prod.yml -f compose.postgres.yml -f compose.tls.yml up -d

The server/config.ts file parses these variables at startup, while server/db/index.ts initializes the database client and runs pending migrations automatically.

Summary

  • Railway: Use image ghcr.io/corebunch/instatic, set PORT=8080 and RAILWAY_RUN_UID=0, and mount /app/storage for SQLite persistence.
  • Render: Deploy via Blueprint (render.yaml) using port 10000 and persistent disks at /app/storage.
  • Custom VPS: Use Docker Compose with compose.prod.yml and environment-specific overlays (compose.sqlite.yml, compose.tls.yml), generating secrets via scripts/generate-secret-key.ts.
  • All deployments require INSTATIC_SECRET_KEY for encrypting AI credentials and MFA secrets, and expose a health check endpoint at /health.

Frequently Asked Questions

What is the default port for Instatic on each platform?

Railway uses port 8080, Render uses port 10000, and custom VPS deployments default to port 3001 unless overridden in your docker-compose.yml or environment variables. The PORT variable in server/config.ts controls the listening interface.

How does Instatic handle database migrations during deployment?

The container automatically runs migrations on startup via the logic in server/db/index.ts before the Bun server begins accepting requests. This applies to both SQLite (sqlite:/app/storage/data/cms.db) and PostgreSQL connections configured through DATABASE_URL.

Can I use PostgreSQL instead of SQLite on Railway or Render?

Yes. On Railway, provision a Railway Postgres database and set DATABASE_URL=${{Postgres.DATABASE_URL}}. On Render, use the PostgreSQL Blueprint template in docs/deployment/render/postgres/render.yaml. The underlying database adapter in server/db/index.ts detects the connection string dialect and initializes the appropriate client.

How do I secure the admin panel with HTTPS on a custom VPS?

Enable TLS by including compose.tls.yml in your Docker Compose command and setting DOMAIN and LETSENCRYPT_EMAIL in your .env file. Caddy automatically provisions certificates and terminates TLS at the reverse proxy, forwarding plain HTTP to the Instatic container on the internal network.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →