Repository navigation
Deploy a Next.js app with Postgres on your own server in 10 minutes #1239
thegdsks
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
You have a Next.js app on GitHub and a cheap Linux server. This walkthrough takes you from a blank box to
https://your-domain.comwith a Postgres database attached, a build on every push, a health check that gates each release, and a rollback command for the day something breaks. There is no Dockerfile to write, no nginx config, and no certbot cron job. It comes down to one install command, one import command, and one domain.Contents
What you will have at the end
git push, using the Railpack builder when your repo has no DockerfileHow the path works
flowchart LR A["git push"] --> B["Detect stack<br/>Next.js, port, env keys"] B --> C["Build<br/>Dockerfile or Railpack"] C --> D{"Health gate<br/>readiness probe"} D -->|passes| E["Traffic moves<br/>TLS domain"] D -->|fails| F["Old version<br/>keeps serving"]The gate is the part to remember. A new container has to answer its readiness probe before it takes traffic. If it never does, the version that was already running keeps serving, and you get a structured reason for the failure instead of a blank 502.
Before you start
curl. The installer warns below 1 GB of RAM, and 2 GB gives a Next.js build room. The preflight check requires 10 GB of free disk.Tip
No domain yet? The dashboard can issue a real Let's Encrypt certificate for a
<dashed-ip>.sslip.iohostname with no DNS at all, so you can finish this whole tutorial before you buy one.Step 1: Install Levelrail
SSH into the server and run the installer:
curl -fsSL https://levelrail.com/install.sh | sudo shIt checks the machine first (root, architecture, systemd, Docker version, RAM, disk, ports), installs Docker if it is missing, downloads the release and verifies its SHA-256 checksum, installs
levelrail-cliinto/usr/local/bin, starts alevelrailsystemd service, and waits for its health endpoint. When it finishes it prints your dashboard URL and a one-time setup token.Open the
http://<ip>:8080/login?setup=<token>link from the summary, create the admin account, and let the setup wizard run its server checks.While you are here, copy
master.keyout of the data directory to somewhere safe. It encrypts every secret you store, and the installer reminds you to back it up for that reason.Next, sign the CLI in. Set the control plane URL, then approve a short device code in the dashboard, the same flow as
gh auth login:Credentials are saved to
~/.config/levelrail-cli/credentials. Add--profile NAMEto keep several control planes side by side. For CI, create an API token under Settings, API tokens, and pass it with--tokenorAPP_API_TOKEN. Everything below works from the dashboard too.Then turn on HTTPS for the dashboard itself, from the Domains page or with the CLI:
levelrail-cli settings ingress https enable --email you@yourdomain.comThat gives the dashboard a
<dashed-ip>.sslip.ioaddress with a real certificate. If you want your own domain on it later, pick a dashboard-only subdomain such asconsole.example.comand keep your app's domain separate. UpdateAPP_API_URLto the new https address afterwards.Step 2: Let it read your repo
Before anything is created, ask Levelrail what it sees. This reads files through the GitHub API, clones nothing, and creates nothing:
You get a table with these rows: Name, Stack, Build, Port, Health, Memory, Domain, Database and Env. For a Next.js repo with no Dockerfile, the interesting ones look like this:
railpack/Every suggestion comes with its evidence, so you can tell a real finding from a guess.
What the detection looks at
package.jsondependencies (alsogo.mod,requirements.txt,Gemfileand others)EXPOSE, a--portflag in your scripts, then the framework defaultHEALTHCHECK, then the framework default.env.example,.env.sample,.env.template,.env.dist, README tablesDATABASE_URL,POSTGRES_*and similar names, plus driver dependenciesIf your repo has a
.env.example, the analysis lists the variable names it finds, flags the ones that look like secrets, and proposes a database. A repo that mentionsPOSTGRES_URLgets this suggestion:Only names are read, never values. A required variable blocks the next step until you supply a value, which you can do inline with
--env KEY=VALUE(repeat it for more than one).Step 3: Create the app, the database and the first build
Happy with the plan? Run the same command with
--apply, adding a value for anything the analysis marked as required:It creates the database, the app, the database connection and the repository link, then starts the first build. The last line looks like this:
Copy that
dep_...id and follow the build:Or block until the deploy finishes, which suits scripts:
levelrail-cli apps wait shopapps waitexits 0 when the deploy converged (it printsrolled out), 5 when it converged as a failure, and 6 on timeout. The default timeout is 5 minutes.Your readiness probe decides whether the deploy succeeds. If your app answers
200on/, you are done. If you have a dedicated route, set it:levelrail-cli apps health set shop --probe readiness --path /healthzPrefer clicks? The New app page in the dashboard runs the same analysis and shows the same evidence, then a single Deploy button opens the live build log.
Step 4: Set your env vars and secrets
App env vars and secrets are injected when the container is created, not during
next build. That matters for exactly one family of variables:NEXT_PUBLIC_*values are compiled into the browser bundle at build time. Commit them in a.env.productionfile in your repo (they end up in public JavaScript anyway) andnext buildpicks them up, with Railpack or a Dockerfile. Everything else is read at runtime and is set as follows.levelrail-cli apps env import shop --file .env.production --applylevelrail-cli apps secrets set shop NEXTAUTH_SECRET 'a-long-random-string' --applyapps env importshows which keys are new, changed or unchanged, and skips keys that are already secrets. Add--dry-runto preview without saving, or--keep-existingto leave changed keys alone.--applyrestarts the app so the running container picks up the change.Tip
Bulk-load secrets from a file with
levelrail-cli apps secrets set shop --env-file .env.secrets. Each key becomes its own encrypted secret.Step 5: Point your domain at it
Create an A record for
app.example.compointing at the server's public IP. Then attach the domain and wait for it to go live:--waitprints each go-live step and polls for 2 minutes by default (change it with--timeout), exiting non-zero if the domain is not live by then. The certificate is requested and renewed for you, so there is no separate proxy to manage. If you have connected Cloudflare or Route53,apps domains addcreates the DNS record too.If DNS is still propagating, check what the control plane sees. The domain has to be attached to the app first, which is why the check comes second:
It does a real DNS lookup and reports
connected,not_resolving,resolves_elsewhereorunconfigured.To see every address the app answers on:
Step 6: Turn on push to deploy
--applylinked the repository and ran the first build. To have pushes trigger new builds, connect GitHub once in the dashboard (the GitHub App settings page), then register the repo as the app's source. That call also installs the push webhook. The--build-typeflag matches what the analysis picked in step 2, since the default here isdockerfile:From then on, pushes to the default branch deploy. Make a visible change, push it, and watch:
Open a pull request and, with previews enabled, you also get a temporary environment per PR that is torn down when the PR closes. The previews page covers it.
Logs and rollback
Search the stored logs, or follow them live:
levelrail-cli apps logs shop --q "error" --since 1h --tail 100 levelrail-cli apps logs shop --followCheck where the app stands:
When a deploy goes wrong, list the history and send the app back to one that worked. Rollback redeploys that deploy's exact image, pinned by digest or by a rollback pin that image cleanup never removes, and brings back its env, health checks, replicas and resources. Secrets, domains and volumes stay as they are now.
--watchfollows it through the readiness gate:If a deploy fails, read the structured reason instead of scrolling raw logs. Without an id it shows the newest attempt:
Add Postgres to an app you already deployed
Already running a Next.js app without a database? Create one and connect it:
apps connectinjects the full connection string into the variable you name, at the app's next container start. Without--env-varit defaults to<DATABASE>_DATABASE_URL, so name itDATABASE_URLif that is what Prisma or Drizzle expects. The connections list also shows a reachability badge, so you can tell whether the app and database share a node. If you imported with--applyin step 3, this database already exists and is connected. Scheduled backups and restore are covered on the backups page.Run migrations after a deploy
You can run a command in the app's running container and get its real exit code back:
levelrail-cli apps exec shop -- npx prisma migrate deploySwap in your own migration command. The command is a plain argv with no shell, so put
--before it when it takes flags.Troubleshooting
port_not_listening0.0.0.0on the app port. ThePORTenv var is injected.health_check_failedapps health set, or raise--ready-timeoutfor a slow start.compile_errornext buildapps deploys show shop, fix it, and confirmnext buildpasses locally.build_out_of_memorymissing_envapps env importorapps secrets set, then redeploy.oom_killeddockerfile_errorCOPYsource is not committedrailpackwith--build-type railpack.NEXT_PUBLIC_value is wrong in the browser.env.productionand push a new build.domains check, and open both ports in your firewall.APP_ALLOW_INSECURE_LOGIN=truethroughsudo systemctl edit levelrailtemporarily.The full list of failure codes
dockerfile_error,dependency_install_failed,compile_error,build_config_error,build_out_of_memory,build_timeout,image_pull_failed,port_not_listening,health_check_failed,container_crashed,oom_killed,missing_env,registry_push_failed,disk_full,docker_unreachable,freeze_window_hold,approval_pending,scan_gate_blocked,rollback_target_gone, andunknown. Each one has a cause and a suggested fix on the deploy failures page.Rollbacks need the old image to still exist. If you ever see
rollback_target_gone, the image was garbage collected: redeploy from source and raise the retained rollback image count so the next one is covered.What to do next
levelrail-cli apps sleep enable shop --after 30mTry it, then tell us how it went
If this saved you an afternoon, a star on glincker/levelrail helps other people find it. Running a stack I did not cover, or hit a snag? Reply in this thread with your framework and what the analysis step printed. I read every one, and the real repos people bring here shape what detection learns next.
All reactions