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.
-
Create a new Railway project and add a Docker image service.
-
Set the image to
ghcr.io/corebunch/instatic:0.0.13(orlatest). -
Attach a volume with mount path
/app/storage. -
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
-
Set the health check path to
/health. -
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.
-
Fork the template repository (e.g.,
corebunch/instatic-render-sqlite) or create a new Blueprint file. -
Click Deploy to Render or manually create a web service using the Docker image
ghcr.io/corebunch/instatic:latest. -
Attach a persistent disk mounted at
/app/storage. -
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
- For custom domains, add
PUBLIC_ORIGINas a comma-separated list including bothRENDER_EXTERNAL_URLand 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:
- SQLite: Add
compose.sqlite.yml - PostgreSQL: Add
compose.postgres.yml(or use default Postgres variables) - TLS: Add
compose.tls.ymlto enable Caddy on port 443
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, setPORT=8080andRAILWAY_RUN_UID=0, and mount/app/storagefor SQLite persistence. - Render: Deploy via Blueprint (
render.yaml) using port10000and persistent disks at/app/storage. - Custom VPS: Use Docker Compose with
compose.prod.ymland environment-specific overlays (compose.sqlite.yml,compose.tls.yml), generating secrets viascripts/generate-secret-key.ts. - All deployments require
INSTATIC_SECRET_KEYfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →