Deploy a Replit app to a VPS
How to move a Replit app to a VPS you control: push it to GitHub, turn the .replit build and run commands into a Dockerfile, re-create the secrets, and move the production database with pg_dump.
TL;DR
- Replit (opens in a new tab) publishes apps as Autoscale, Static, Reserved VM or Scheduled deployments on its own Google Cloud (opens in a new tab) infrastructure, and on a VPS each becomes a container, static files or a cron job.
- Push the code to GitHub (opens in a new tab) from the Git pane; secrets and App Storage files do not travel with it.
- Your Dockerfile runs the build and run commands from the
[deployment]section of.replit, with the app listening on0.0.0.0, notlocalhost, as a published Replit app already does. - Copy the production database with
pg_dumpand the production connection string from the Database tool, thenpg_restoreit into PostgreSQL (opens in a new tab) on the server.
On this page
To deploy a Replit app on your own VPS, push its code to GitHub from the Git pane, turn the build and run commands in its .replit file into a Dockerfile, enter its secrets again on the server, and copy its production PostgreSQL database across with pg_dump and pg_restore. Run the app, the database and a reverse proxy with automatic HTTPS together with Docker (opens in a new tab) Compose, test on a temporary address, then move the domain. This is one of the builder guides in deploying an AI-built app to your own server.
The Replit facts here come from Replit's documentation, checked on 11 November 2026. The Dockerfile, the Compose file and the database copy were run that day in Docker, on Node.js (opens in a new tab) 22 and PostgreSQL 17, with a small Express and PostgreSQL app standing in for a Replit app and a second PostgreSQL container standing in for Replit's production database.
How Replit deploys apps, and what that becomes on a VPS
Replit runs published apps on Google Cloud in the United States, as one of four deployment types, each with a plain equivalent on your own server:
| Replit deployment | What Replit says it is for | On your VPS |
|---|---|---|
| Autoscale | Apps with variable traffic; resources follow usage | A container that is always running |
| Reserved VM | Apps that must run continuously, such as a bot | A container that is always running |
| Static | Sites that do not change with user input | Static files served by the web server |
| Scheduled | Jobs that run at times you choose | A cron entry that runs the job in a container |
A VPS does not scale up and down on its own the way Autoscale does. For most small apps that is fine: one server sized for the normal load costs the same every month. If the app has sharp peaks, keep them in mind when you choose the size.
Step 1: Read the deployment settings
The .replit file. Its [deployment] section has a build command that runs before the deployment and a run command that starts it, for example:
[deployment]
build = "npm run build"
run = "npm start"Its [[ports]] entries map the port the app listens on (localPort) to the public one (externalPort); Replit binds the first port to external port 80. Autoscale and Reserved VM deployments expose only one external port, and a published app must not listen on localhost, so if your app already works published on Replit, it listens on 0.0.0.0 and will behave the same in a container. Note its localPort: the Dockerfile and the Caddyfile below use 3000, so change both if your app listens on another port.
The Publishing tool. Note the deployment type, the custom domain if you have one, and under Adjust settings the names of the Production app secrets, which you will need on the server.
Step 2: Push the code to GitHub
In the workspace, open Tools, then Git under Setup. If there is no repository yet, choose Initialize Git Repository. Connect it to GitHub, stage and commit your files, and push to a private repository you created there. From the Shell, git remote add origin <url> and git push origin main work too; use a personal access token, not your password.
Replit's guide to copying a project says what does and does not travel:
- Code: GitHub receives only what you commit and push. Ignored files and uncommitted changes stay behind.
- Secrets: secret values never leave the project. You enter them again by hand, deployment secrets included.
- App Storage: buckets do not move. Replit's guide moves them between projects by downloading the objects and uploading them to a new bucket, and the same route takes them to object storage you choose.
- Database: not in the repository. Step 5 copies it.
Clone the repository on your computer and run it once: it is the quickest way to find every environment variable the app reads.
Step 3: Write the Dockerfile
The Dockerfile does what Replit did: install the dependencies, run the build command, start the run command. For a Node.js app whose .replit says build = "npm run build" and run = "npm start", with npm start running node server.js:
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
COPY . .
RUN npm run build --if-present
RUN npm prune --omit=dev
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production PORT=3000
COPY --from=build --chown=node:node /app ./
USER node
EXPOSE 3000
CMD ["node", "server.js"]Replace node server.js with whatever your start script runs, and check two things:
- If the run command needs a development dependency (for example
tsxto run TypeScript directly), remove thenpm pruneline or move that package todependencies. - If the app builds a frontend with Vite, any
VITE_variable it reads is compiled into the build. Pass it as a build argument (ARG VITE_...beforenpm run build), not only at run time.
A .dockerignore keeps Replit's files and local secrets out of the image:
node_modules
.git
.env*
.replit
replit.nix
*.dumpStep 4: Run the app, PostgreSQL and Caddy together
On the server, one compose.yaml runs the app, a PostgreSQL 17 database and Caddy (opens in a new tab), which gets and renews the HTTPS certificate for your domain:
services:
app:
build: .
environment:
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app
env_file: .env.production
depends_on:
- db
restart: unless-stopped
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: app
volumes:
- db_data:/var/lib/postgresql/data
restart: unless-stopped
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
volumes:
db_data:
caddy_data:
caddy_config:The Caddyfile next to it:
app.example.com {
reverse_proxy app:3000
}Replace app.example.com with your domain. Put a long random DB_PASSWORD (letters and digits only, because it goes into a URL: openssl rand -hex 32 makes one) in a .env file next to compose.yaml (Docker Compose reads it for the ${DB_PASSWORD} substitutions), and the app's secrets from step 1 in .env.production. Neither file goes into Git. The database port is not published, so only the app can reach it. Start the database first, so you can load the data before the app starts writing:
docker compose up -d dbA dump restores into the same or a newer PostgreSQL major version, so check the version of your Replit production database (select version();) and use at least that version here.
Step 5: Copy the production database
Replit keeps two databases. The workspace, the Shell and the Agent use the development one; the published app uses a separate production one, with its own DATABASE_URL, and that is where your live data is. The development database cannot be reached from outside the app at all, but the production one can, with its connection string from the Database tool: select Production, then open Settings.
In the Replit Shell, run the export from Replit's own guide, with the production connection string in place of $DATABASE_URL:
pg_dump -Fc "<production connection string>" --no-owner --no-privileges -f backup.dump-Fc writes PostgreSQL's compressed custom format, and the two --no- flags leave out ownership and grants that refer to Replit's database users. Download backup.dump, copy it to the server next to compose.yaml (for example with scp), and restore it into the db service:
docker compose exec -T db pg_restore --no-owner --no-privileges -U app -d app < backup.dumpThen start everything:
docker compose up -d --buildIn our test, a 42-row table dumped from the stand-in production database with the first command and restored with the second came back complete, and the app read all 42 rows through Caddy over HTTPS (with localhost as the site address, where Caddy uses its own local certificate authority; a public certificate for a real domain needs a real server).
Do not paste the connection string into a chat, a ticket or a screenshot: it contains the password. If it leaks, regenerate the production credentials in Replit.
For the final move, dump again right before you switch the domain, so no writes made in between are lost, and pause writes on the Replit app while you do. The server's database already holds the test copy by then, so restore with --clean --if-exists, which drops the objects in the dump before it loads them, as Replit's own guide does:
docker compose exec -T db pg_restore --clean --if-exists --no-owner --no-privileges -U app -d app < backup.dumpIn our test, a row added after the first restore was gone after this one, and the table held the dump's 42 rows again.
Step 6: Check what is Replit-specific
These work on Replit only because Replit is there:
REPLIT_DEPLOYMENT. A published Replit app runs withREPLIT_DEPLOYMENT=1, so code that checks it to decide whether it is in production will think it is in development on your server. Search for it and useNODE_ENVor your own variable.- Replit Auth. Replit Auth signs users in with their Replit accounts, through Replit's shared OAuth apps set up by the Agent. Replit's docs do not describe using it from another host, so plan to replace it; Replit documents a migration from Replit Auth to Clerk, and any other provider works too.
- The
.replit.appaddress. Search the code and the secrets for it: OAuth callbacks, webhooks (Stripe, GitHub) and email links can have it hardcoded. - The port. If the app hardcodes a port instead of reading
PORT, setPORTto that number and change the Caddyfile to match.
Step 7: Test, then switch the domain
Use a temporary subdomain first: point it at the server and put it in the Caddyfile instead of the real domain. Sign in, create and change something, upload a file, and wait for any scheduled job to run. Then point your domain's A record at the server, remove the custom domain from Replit's Publishing settings, and unpublish the Replit app once the new one has run cleanly for a while.
No server yet? How to deploy a web app to a VPS covers creating one, SSH keys, the firewall and Docker; Docker Compose on Hetzner with GitHub Actions shows how to deploy every push automatically; and zero-downtime deployments with Docker Compose keeps updates from dropping visitors. If your Replit app is a Next.js (opens in a new tab) app, the v0 guide has a standalone Next.js Dockerfile, and if it is a static Vite site, the Lovable guide has one for that.
Doing this with OpsCrew
OpsCrew recognises a repository exported from Replit and runs the same steps from it: a free opscrew scan for leaked keys first, then an image built in GitHub Actions (with its own Dockerfile for Node.js and Python apps, or yours), a hardened server on Hetzner Cloud (opens in a new tab) or DigitalOcean (opens in a new tab) or one you already have, and HTTPS on your domain. When the app uses PostgreSQL, it runs the database next to it with a generated password and gives the app its DATABASE_URL, or keeps a hosted database if you prefer. Copying your existing data in is still the pg_dump and pg_restore above. Every change waits for your approval of its exact plan.
For what is the same across every builder, see the guide to deploying an AI-built app to your own server.
Key facts
| Fact | Value | Source |
|---|---|---|
| Replit deployment types | Autoscale, Static, Reserved VM and Scheduled | Publishing (opens in a new tab)Verified on |
| Where Replit hosts published apps | Google Cloud in the United States; Enterprise customers can ask for EU hosting | Publishing (opens in a new tab)Verified on |
| Variable a published Replit app runs with | REPLIT_DEPLOYMENT=1 | Development and production databases (opens in a new tab)Verified on |
| External access to the development database | None; only the production database's connection string works from outside the app | Connection details (opens in a new tab)Verified on |
Frequently asked questions
How does Replit deploy apps?
Publishing saves a snapshot of the app's files and dependencies and runs it on Replit's cloud, separate from the editor, and you publish again to update it.
Can I run a Replit app locally?
Yes. Clone the repository, install the dependencies, set the environment variables the app reads (including a DATABASE_URL for a local PostgreSQL), and run the run command from .replit. For data, use a local database or a dump of the production one.
Sources
- Publishing (opens in a new tab)
- Replit App configuration (opens in a new tab)
- Ports (opens in a new tab)
- Using the Git pane (opens in a new tab)
- Manually copy a project to another workspace (opens in a new tab)
- Development and production databases (opens in a new tab)
- Connection details (opens in a new tab)
- Storage and databases (opens in a new tab)
- Auth (opens in a new tab)
- pg_dump (opens in a new tab)
- Automatic HTTPS (opens in a new tab)
Related articles
- Deploying
How to deploy an app you built with AI to your own server
From a Lovable, Bolt.new, Replit, v0 or Claude Code project to a live URL on a server you control: code, build, backend, secrets, HTTPS and what breaks on the way.
- Deploying
Deploy a Bolt.new app to your own server
How to deploy a Bolt.new app outside Bolt Cloud: get the code, find out which stack Bolt chose, build it in Docker, serve it over HTTPS, and claim the database into Supabase.
- Deploying
Deploy a Lovable app to your own server (Vite or TanStack Start)
How to deploy a Lovable app to a server you control: tell the Vite and TanStack Start stacks apart, sync it to GitHub, build it in Docker, serve it over HTTPS, and keep or move the Supabase backend.