# RE:NODE deploy documentation > How an application server on RE:NODE (renode.space) runs, and how to deploy Node.js, Python, static sites, bots, databases and custom domains to one. Generated from the pages under https://www.renode.space/docs. --- # How a Renode app server runs Source: https://www.renode.space/docs These pages describe how an application server on RE:NODE actually runs, so that you - or an AI assistant working for you - can deploy to one without guessing. They cover the app hosting lines (Node.js, Python, Go, Java, Rust, C#/.NET, Bun, Deno and Laravel / PHP), web hosting and database hosting. Every server is managed from the panel at [panel.renode.space](https://panel.renode.space). If you only read one section, read **The five rules** below. Nearly every failed first deploy breaks one of them. ## The five rules 1. **Listen on `0.0.0.0` and on the port in `SERVER_PORT`.** Exactly one port is reachable from outside: the one your server was given. Never hardcode a port and never bind `localhost` or `127.0.0.1`. 2. **Give the server something to start.** On Node.js that is a `start` script in `package.json`. On Python it is the entry file named on the Startup tab. A web hosting server starts nothing - it serves files. 3. **Build before you serve.** There is no separate build server. Either the start script builds, or the built output is committed. 4. **Keep secrets out of the repository.** Upload `.env` through the Files tab, or set variables on the Startup tab. 5. **Stay inside the memory limit.** It is a hard ceiling. A process that reaches it is killed with no error message - the console simply stops. ## The container Each server is one container. Your process runs as an unprivileged user, so there is no root, `sudo`, `apt`, `systemd` or Docker inside it. Install what you need through your language's own package manager (`npm`, `pip`), which works without root. Your code lives in `/home/container`, and only `/home/container` persists. Anything your app writes at run time - uploads, a SQLite file, logs you want to keep - belongs under that folder. You can browse and edit it in the panel's Files tab or over SFTP. ## The port The panel gives every server an allocation: an address and a port, shown on the server's Network tab. That port is the only one reachable from outside, and it reaches your process through these environment variables: | Variable | Value | Set when | | --- | --- | --- | | `SERVER_PORT` | The server's port | Always | | `PORT` | The same port, unless your project sets `PORT` itself | The panel starts a linked repository | | `HOST`, `HOSTNAME` | `0.0.0.0` | The panel starts a linked repository | Read `SERVER_PORT` (or `PORT`) in your code. A port written into the code is reachable from inside the container and from nowhere else, unless it happens to match by coincidence. If your program needs a second port, add another allocation on the Network tab - a port that is not allocated is not reachable. A web hosting server is the exception: its image already runs a web server bound to the port, so you do not bind anything. ## Deploying from GitHub Application servers deploy from GitHub through the panel's GitHub App, so private repositories work and no token is ever pasted into your code. On the server's GitHub tab you install the app, pick a repository and a branch, and choose two switches: - **Pull the latest commit every time the server starts** - fetch the branch every time the server starts. With it off, the branch is fetched on the first start only and later starts use the files already there. - **Restart the server when someone pushes to this branch** - when somebody pushes to the branch, the server restarts within seconds and comes back on the new commit. A stopped server is started, so the push goes live either way. Turning this on also fetches on every start. The fetch writes the branch into the code folder (`/home/container` unless the GitHub tab says otherwise), over what is already there under the same names. Files elsewhere in the container are left alone. Where the image has `git`, the fetch is a shallow `git fetch` and `reset --hard`; on images without `git`, the branch is downloaded and unpacked over the folder, which means a file you deleted from the branch stays on the server until you remove it. ## How a server boots With a repository linked, a Node.js-capable server goes through these steps every time it starts: 1. The branch is fetched into the code folder (every start, or only the first - see above). 2. If the server type defines a setup command, it runs after the fetch. 3. The environment gets `PORT` (from `SERVER_PORT`, unless your project sets it), `HOST=0.0.0.0` and `HOSTNAME=0.0.0.0`. 4. If `package.json` defines a `start` script: `npm install`, then `npm run start` (Bun's own installer and runner on Bun images). The server type's own startup command is not run. 5. Otherwise, on Node.js, the first file that exists among `index.js`, `server.js`, `app.js`, `main.js`, `bot.js`, `index.mjs`, `index.cjs`, the same names under `src/`, and then their `.ts` versions, is started with `node` - or with `ts-node` for a `.ts` file. 6. If none of that applies, the server type's own startup command runs. Other server types boot their own way. A Python server runs the entry file named on the Startup tab after installing `requirements.txt` - see [Python](https://www.renode.space/docs/python). A web hosting server runs nothing from your repository - see [Static sites and web hosting](https://www.renode.space/docs/web-hosting). A database server starts its engine - see [Databases](https://www.renode.space/docs/databases). ## Secrets and .env A `.env` file is not in your repository, and the fetch does not create one. Upload yours through the Files tab into the code folder, and keep a copy somewhere other than the server. Alternatively, set values as variables on the Startup tab. In the repository, add `.env` to `.gitignore` and commit a `.env.example` that lists every variable with no values. Read configuration from the environment and fail with a clear message when a required value is missing - it turns a confusing crash into a one-line fix. ## When changes go live | Your setting | After a push | | --- | --- | | Restart on push on | The server restarts by itself within seconds, on the new commit | | Pull on every start on, restart on push off | Restart the server when you want the new commit | | Both off | Later starts keep the files from the first fetch | | Web hosting | The panel writes the files on push (with restart on push on) and when you save the GitHub tab. A restart fetches nothing | ## Memory and crashes Each plan has a memory limit, and it is a hard ceiling. When a process reaches it, the kernel stops the container and it restarts clean - there is no stack trace and no final line, the console just stops. Build steps are the usual cause on small plans: a Next.js or TypeScript build can need more memory than the app itself. If the build is what gets killed, build locally and commit the output, or move to a larger plan. The panel watches for crash loops. A server that restarts by itself three times in an hour gets a warning on its page and a support ticket; at six it is suspended. Restarts you asked for are not counted. ## Check before you deploy Once a repository is linked, the panel can run a **pre-flight check**: it reads the repository and reports what will break - a missing start script, a hardcoded port, a build that never runs, a Node version mismatch, a project that expects a `.env`. There is no AI involved; it is the panel reading your files against the way this server boots. ## Where next - [Node.js](https://www.renode.space/docs/nodejs) - start scripts, frameworks, TypeScript and Node versions. - [Python](https://www.renode.space/docs/python) - entry files, `requirements.txt` and production servers. - [Static sites and web hosting](https://www.renode.space/docs/web-hosting) - serving files as they are. - [Discord and Telegram bots](https://www.renode.space/docs/bots) - tokens, restarts and state. - [Databases](https://www.renode.space/docs/databases) - connecting an app, and where passwords live. - [Custom domains](https://www.renode.space/docs/domains) - the proxy slot, DNS and certificates. - [Troubleshooting](https://www.renode.space/docs/troubleshooting) - when a server will not start or cannot be reached. - [Using AI assistants](https://www.renode.space/docs/ai-assistants) - the Connect AI link, connecting your account over MCP, and what an assistant may do. --- # Deploy a Node.js app on Renode Source: https://www.renode.space/docs/nodejs A Node.js server runs your repository from `/home/container`. The reliable way to start anything on it - Express, Fastify, Next.js, NestJS, a bot - is a `start` script in `package.json` that listens on the port in `SERVER_PORT`. ## How it starts With a repository linked on the GitHub tab, every start goes like this: 1. The branch is fetched into `/home/container` (on every start if "Pull the latest commit every time the server starts" or "Restart the server when someone pushes" is on; otherwise on the first start only). 2. `PORT` is set to the server's port (unless your project sets `PORT` itself), and `HOST` and `HOSTNAME` to `0.0.0.0`. 3. If `package.json` has a `start` script: `npm install`, then `npm run start`. 4. Without a `start` script, the first file that exists among `index.js`, `server.js`, `app.js`, `main.js`, `bot.js`, `index.mjs`, `index.cjs`, then `src/index.js`, `src/server.js`, `src/app.js`, `src/main.js`, then the `.ts` versions of those names, is started with `node` - or `ts-node` for a `.ts` file. Relying on step 4 is fragile. Define `start`. `npm install` installs `devDependencies` too, so build tools such as `typescript` can stay there when the start script builds. ## A minimal app Two files are a complete, deployable app: package.json: ```json { "name": "hello-renode", "private": true, "engines": { "node": ">=20" }, "scripts": { "start": "node index.js" } } ``` index.js: ```js const http = require("node:http"); // The server's port. PORT is set from SERVER_PORT when the panel starts the repo. const port = Number(process.env.PORT || process.env.SERVER_PORT); if (!port) { console.error("No port: SERVER_PORT is not set"); process.exit(1); } http .createServer((req, res) => { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Hello from Renode\n"); }) .listen(port, "0.0.0.0", () => { console.log(`Listening on 0.0.0.0:${port}`); }); ``` With Express the only lines that matter are the same two: read the port from the environment and bind `0.0.0.0`. server.js: ```js const express = require("express"); const app = express(); app.get("/", (req, res) => res.send("Hello from Renode")); app.listen(Number(process.env.PORT || process.env.SERVER_PORT), "0.0.0.0"); ``` ## Frameworks that need a build There is no separate build server: the build runs inside your server, with its memory and CPU. Put the build in the start script, or build locally and commit the output. | Framework | A start script that works | | --- | --- | | Next.js | `next build && next start -H 0.0.0.0 -p $SERVER_PORT` | | Nuxt | `nuxt build && node .output/server/index.mjs` | | Vite (preview server) | `vite build && vite preview --host 0.0.0.0 --port $SERVER_PORT` | | Angular | `ng build && npx http-server dist -a 0.0.0.0 -p $SERVER_PORT` | | TypeScript | `tsc && node dist/index.js` | Never use a development server as `start` - `next dev`, `vite`, `nodemon`. They are slow, unminified, watch the filesystem, use more memory, and often bind `localhost`, where nothing outside the container can reach them. > [!WARNING] > Next.js and other bundler builds can need more memory than a small plan has. If the console stops part-way through the build with no error, the build was killed at the memory limit. Build locally and commit the output (for Next.js, the `.next` folder, with `"start": "next start -H 0.0.0.0 -p $SERVER_PORT"`), or use a larger plan. ## TypeScript Compile in the start script, or commit the compiled `dist/`. Do not depend on a global `ts-node`. package.json: ```json { "scripts": { "build": "tsc -p tsconfig.json", "start": "tsc -p tsconfig.json && node dist/index.js" }, "devDependencies": { "typescript": "^5.6.0" } } ``` ## Choosing the Node version The Node.js version is the server's runtime image. Pick it under **Runtime version** on the GitHub tab, or the Docker image on the Startup tab. Declare the version your project expects in `engines.node` in `package.json` or in an `.nvmrc` file - the pre-flight check compares them with the server's runtime and warns when they differ, because packages built for one Node major can fail to install or run on another. ## Environment variables Upload `.env` through the Files tab into `/home/container`, or set variables on the Startup tab. If you use `dotenv`, load it first: ```js require("dotenv").config(); ``` Commit a `.env.example` with every key and no values, and keep `.env` in `.gitignore`. ## Checklist - `package.json` has a `start` script that starts the production server. - The app listens on `process.env.PORT` or `process.env.SERVER_PORT`, on `0.0.0.0`. - Anything that must be built is built by the start script, or the output is committed. - `package-lock.json` is committed; `node_modules` is not. - No password, token or key is committed; `.env` is in `.gitignore`. - The Node version in `engines.node` or `.nvmrc` matches the server's runtime. - Files the app writes at run time go under `/home/container`. ## Common errors **The container starts and immediately exits.** There is no `start` script and none of the entry files exist. Add `"start": "node "`. **`npm run start` cannot find `dist/index.js` or `.next`.** The build never ran. Build in the start script, or commit the output. **The console says it is listening, but the address does not answer.** The app is bound to `localhost` or to a hardcoded port. Use `0.0.0.0` and the port from the environment. **The console stops mid-build with no error.** The build hit the memory limit. See the warning above. **Packages fail to install or crash with a native-module error.** The Node version differs from what the project was built with. Match the runtime version to `engines.node`, and never commit `node_modules` built on Windows or macOS. More in [Troubleshooting](https://www.renode.space/docs/troubleshooting). --- # Deploy a Python app on Renode Source: https://www.renode.space/docs/python A Python server runs one file from `/home/container`: the entry file named on the server's Startup tab. Before it starts, it installs `requirements.txt` when your repository has one. ## How it starts 1. The branch is fetched into `/home/container` (on every start, or only the first - see the [overview](https://www.renode.space/docs)). 2. `requirements.txt` at the repository root is installed with `pip`. Packages go into the user's site directory, so nothing needs root. 3. The entry file named on the Startup tab (often the `PY_FILE` variable) is run. When you link a repository, the panel points the Startup tab at the first of `main.py`, `bot.py`, `app.py`, `run.py` or `__main__.py` that exists. Any other name works too - set it on the Startup tab. Two things that are **not** read: - **`pyproject.toml` dependencies.** Only `requirements.txt` is installed. Export them: `pip freeze > requirements.txt`, or `uv pip compile pyproject.toml -o requirements.txt`. - **A `Procfile`.** The server runs the entry file, not the command in a `Procfile`. ## A minimal web app Start a production server from the entry file itself, bound to `0.0.0.0` and the port in `SERVER_PORT`. With FastAPI: requirements.txt: ```text fastapi==0.115.0 uvicorn==0.30.6 ``` main.py: ```python import os import uvicorn from fastapi import FastAPI app = FastAPI() @app.get("/") def home(): return {"hello": "renode"} if __name__ == "__main__": # The server's port. Never hardcode one, never bind 127.0.0.1. uvicorn.run(app, host="0.0.0.0", port=int(os.environ["SERVER_PORT"])) ``` With Flask, use a production WSGI server rather than `app.run()`, which is Flask's development server: requirements.txt: ```text flask==3.0.3 waitress==3.0.0 ``` app.py: ```python import os from flask import Flask from waitress import serve app = Flask(__name__) @app.get("/") def home(): return "Hello from Renode" if __name__ == "__main__": serve(app, host="0.0.0.0", port=int(os.environ["SERVER_PORT"])) ``` Django works the same way: start `waitress` (or another production server) from the entry file, bound to `0.0.0.0` and `SERVER_PORT`, and add the domain you serve it on to `ALLOWED_HOSTS`. ## Configuration and .env Read configuration from the environment. Upload `.env` through the Files tab into `/home/container`, or set variables on the Startup tab, and load the file with `python-dotenv`: ```python import os from dotenv import load_dotenv load_dotenv() TOKEN = os.environ.get("API_TOKEN") if not TOKEN: raise SystemExit("API_TOKEN is not set - upload .env through the Files tab") ``` ## Logging The console shows what your process writes to stdout. Python buffers it when it is not attached to a terminal, so output can arrive late or not at all before a crash. Print with `flush=True`, or set `PYTHONUNBUFFERED=1`. ## System packages `apt` and other system package managers are not available in the container. Choose packages that ship wheels for Linux rather than ones that compile against system libraries at install time. ## Checklist - The entry file on the Startup tab exists in the repository. - `requirements.txt` is at the repository root, with pinned versions. - The app binds `0.0.0.0` and `int(os.environ["SERVER_PORT"])`, with a production server. - No password, token or key is committed; `.env` is in `.gitignore`. - Output is unbuffered so the console shows it. - Files the app writes at run time go under `/home/container`. ## Common errors **`ModuleNotFoundError` on start.** The package is not in `requirements.txt`, or it is declared only in `pyproject.toml`. **`can't open file 'main.py'`.** The Startup tab names a file the branch does not have. Set the entry file to the script that starts your app. **Running, but the address does not answer.** The app is bound to `127.0.0.1`, or to a hardcoded port such as 5000 or 8000. **Nothing appears in the console.** Output is buffered - see Logging above. More in [Troubleshooting](https://www.renode.space/docs/troubleshooting). --- # Static sites and web hosting on Renode Source: https://www.renode.space/docs/web-hosting A web hosting server (the HTML CSS JS PHP line, and Engines) is different from an app server: its image already runs a web server - nginx, with PHP - bound to the server's port. It serves a folder, the **web root**, exactly as it is. Nothing from your repository is started, and nothing is built. ## What has to be in the repository - **`index.html` (or `index.php`) at the root of the served folder.** That file is your home page. - **The built output, if your project has a build step.** A React, Vue, Svelte or Astro project that builds into `dist/` or `build/` must have that folder committed, or the path on the GitHub tab pointed at it. The server never runs `npm run build`. - **No server process and no port.** Do not add a Node or Python server and do not bind a port: the web server is already bound to it. A typical layout for a site built with Vite: ```text my-site/ src/ your source dist/ committed build output - the folder that is served index.html assets/ package.json ``` Build locally with `npm run build`, commit `dist/`, and set the path on the GitHub tab to `dist`. ## How files reach the server On web hosting the **panel** writes your repository into the web root - not the server itself. It does so: - when you link the repository, - when you save changes on the GitHub tab, - on every push to the branch, with "Restart the server when someone pushes to this branch" on. Restarting the server fetches nothing. And files the branch no longer has are left in place rather than deleted, so remove an old file through the Files tab if it must disappear. You can also skip GitHub entirely and upload files through the Files tab or SFTP. ## Single-page apps A single-page app with client-side routing has one real file, `index.html`. A reader who refreshes on `/about` asks the web server for a file called `about`, which does not exist, and gets a 404. Use hash-based routing (`/#/about`), or ask support about a fallback to `index.html` for unknown paths. ## Never commit secrets here Everything in the web root can be downloaded by anybody who guesses its name. Never put `.env`, credentials or private keys in the served folder. A static site that needs a secret - an API key for a payment provider, a database password - needs a server-side component on an [app server](https://www.renode.space/docs/nodejs) instead. ## Checklist - `index.html` or `index.php` is at the root of the served folder. - The built output is committed, or the path on the GitHub tab points at it. - No server process, no port binding. - Nothing secret is in the served folder. - If routes 404 on refresh, the app uses hash routing or a fallback has been arranged. ## Common errors **A directory listing or the placeholder page instead of the site.** There is no `index.html` at the root of the served folder - usually because the site builds into `dist/` and the path still points at the repository root. **The site did not change after a push.** Restart on push is off, so the panel did not write the new commit. Save the GitHub tab, or turn the switch on. A restart alone does nothing here. **A deleted page still loads.** Files removed from the branch are not removed from the server. Delete it through the Files tab. **Pages other than the home page 404 on refresh.** See Single-page apps above. --- # Host a Discord or Telegram bot on Renode Source: https://www.renode.space/docs/bots A bot runs on a [Node.js](https://www.renode.space/docs/nodejs) or [Python](https://www.renode.space/docs/python) server, and starts exactly the way those pages describe: a `start` script or entry file on Node.js, the entry file on the Startup tab on Python. What is different about a bot is what it does not need, and what it must not do. ## What is different about a bot - **It needs no inbound port.** A Discord bot and a Telegram bot using long polling both connect *out* to the provider. Nothing has to reach the server, so there is nothing to bind. (A Telegram bot using webhooks is a web app: it must listen on `SERVER_PORT` and needs an HTTPS address - see [Custom domains](https://www.renode.space/docs/domains).) - **The token is a secret.** Read it from the environment - a `.env` uploaded through the Files tab, or a Startup tab variable. Never commit it and never log it. A token pushed to a public repository is usually found and abused within minutes. - **It will be restarted.** A push with restart on push on restarts it; so does any other restart. Handle reconnects, and keep anything you need across restarts in files under `/home/container` or in a [database](https://www.renode.space/docs/databases), not in memory. ## A Discord bot in Node.js package.json: ```json { "name": "my-bot", "private": true, "scripts": { "start": "node bot.js" }, "dependencies": { "discord.js": "^14.16.0", "dotenv": "^16.4.0" } } ``` bot.js: ```js require("dotenv").config(); const { Client, Events, GatewayIntentBits } = require("discord.js"); const token = process.env.DISCORD_TOKEN; if (!token) { console.error("DISCORD_TOKEN is not set - upload .env through the Files tab"); process.exit(1); } const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], }); client.once(Events.ClientReady, (c) => console.log(`Logged in as ${c.user.tag}`)); client.on(Events.MessageCreate, (message) => { if (message.content === "!ping") message.reply("pong"); }); client.login(token); ``` .env (uploaded through the Files tab, never committed): ```text DISCORD_TOKEN=your-token-here ``` ## A Discord bot in Python requirements.txt: ```text discord.py==2.4.0 python-dotenv==1.0.1 ``` bot.py: ```python import os import discord from dotenv import load_dotenv load_dotenv() intents = discord.Intents.default() intents.message_content = True client = discord.Client(intents=intents) @client.event async def on_ready(): print(f"Logged in as {client.user}", flush=True) @client.event async def on_message(message): if message.content == "!ping": await message.reply("pong") client.run(os.environ["DISCORD_TOKEN"]) ``` Set the entry file on the Startup tab to `bot.py` if the panel did not pick it already. ## A Telegram bot in Python Long polling needs no port and no domain: requirements.txt: ```text python-telegram-bot==21.6 python-dotenv==1.0.1 ``` main.py: ```python import os from dotenv import load_dotenv from telegram import Update from telegram.ext import Application, CommandHandler, ContextTypes load_dotenv() async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text("Hello from Renode") app = Application.builder().token(os.environ["TELEGRAM_TOKEN"]).build() app.add_handler(CommandHandler("start", start)) app.run_polling() ``` ## Checklist - The bot starts from a `start` script (Node.js) or the entry file on the Startup tab (Python). - The token is read from the environment, is not committed and is never printed. - `.env` is in `.gitignore`, and uploaded through the Files tab. - State that must survive a restart is in files under `/home/container` or in a database. - The bot reconnects on its own after a disconnect. ## Common errors **The console shows the bot logging in, then nothing happens.** The process is alive. Check the provider's side before the code: on Discord, the privileged intents (Message Content, Server Members) must be switched on in the Developer Portal for the bot to receive those events. **`DISCORD_TOKEN is not set`, or an invalid token error.** The `.env` was not uploaded, is not in `/home/container`, or has a stray space or quote around the value. **The bot replies twice.** Two copies are running - for example the same token on your own computer and on the server. Stop one. **It forgets everything after a restart.** State was kept in memory. Write it to a file or a database. --- # Connect your app to a database on Renode Source: https://www.renode.space/docs/databases There are two kinds of database on RE:NODE, and they are reached differently. | | Database slot | Database hosting server | | --- | --- | --- | | What it is | A MySQL-compatible database the panel creates for an app or web server | A whole server running PostgreSQL, MySQL, MongoDB, Microsoft SQL Server or Valkey | | Where it is created | The **Databases** tab of your app or web server | Bought as its own server | | Connection details | Host, port, database and username on the Databases tab | The server's address and port, shown in the panel | | Password | On the Databases tab | A Startup tab variable on the database server | ## Database slots App and web plans include database slots. On the server's **Databases** tab, create a database and the panel generates its host, username and password - nothing is shared with anybody else's server. The tab shows the host, port, database name and username; the password is revealed there too (you need the permission to view it if you are a subuser). **Open in phpMyAdmin** signs you straight into that database. ## Database hosting servers A database server runs its engine, started by its image. There is no application code to deploy to it. Connect from your app using the database server's address and port as shown in the panel. Its superuser or root password is generated when the server is created and lives in a variable on that server's **Startup** tab - copy it from there. The engine's major version is fixed by the server's image and cannot be downgraded: a newer major changes the on-disk format, and an older one refuses to open it. ## Keep the credentials out of the code Connection settings belong in the app's environment, never in its repository. Put them in a `.env` uploaded through the Files tab of the **app** server, or in Startup tab variables: .env (uploaded through the Files tab, never committed): ```text DB_HOST=the-host-from-the-panel DB_PORT=3306 DB_NAME=s12_app DB_USER=u12_abc123 DB_PASSWORD=copied-from-the-databases-tab ``` The values above are placeholders - use the ones the panel shows for your database. Commit a `.env.example` with the same keys and no values. ## Connecting from Node.js With `mysql2` for a database slot or a MySQL server: db.js: ```js require("dotenv").config(); const mysql = require("mysql2/promise"); const pool = mysql.createPool({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, }); module.exports = pool; ``` With `pg` for a PostgreSQL server, the same values as one connection string: ```js const { Pool } = require("pg"); // DATABASE_URL=postgres://user:password@host:port/database const pool = new Pool({ connectionString: process.env.DATABASE_URL }); ``` ## Connecting from Python db.py: ```python import os import pymysql from dotenv import load_dotenv load_dotenv() connection = pymysql.connect( host=os.environ["DB_HOST"], port=int(os.environ["DB_PORT"]), database=os.environ["DB_NAME"], user=os.environ["DB_USER"], password=os.environ["DB_PASSWORD"], ) ``` For PostgreSQL use `psycopg` with a `DATABASE_URL`; for MongoDB, `pymongo` with a `mongodb://` URI built from the same environment values. ## Notes per engine - **Microsoft SQL Server** is the Express edition. In SSMS and most drivers the server name is `host,port` - with a comma. Express limits each database to 10 GB. - **Valkey** is the open-source fork of Redis and speaks the Redis protocol, so Redis clients connect to it. It is password-protected. - **MySQL** database servers come with a root password, an application database and an application user, all generated. ## Checklist - No password, host or connection string is written into the code or committed. - The app reads its connection settings from the environment and fails clearly when one is missing. - The password was copied from the Databases tab (slots) or the Startup tab (database servers). - A linked repository on a database server holds migrations or seed files, not something to execute. ## Common errors **Access denied.** The password was mistyped or has a stray quote in `.env`. Copy it again from the panel. **Connection refused or timed out.** The host or port is wrong - use exactly what the panel shows. `localhost` is never right for a database on another server. **`database files are incompatible with server`.** A database server's data directory was opened by a different major version. Majors cannot be switched on an existing server, and there is no downgrade - open a support ticket rather than changing the image again. --- # Use your own domain on Renode Source: https://www.renode.space/docs/domains App and web plans include a reverse-proxy slot. It accepts requests for your domain on the public address, terminates HTTPS there, and forwards plain HTTP to the one port your app listens on. Certificates are issued and renewed for you. RE:NODE does not sell domains or run DNS, so the records are created wherever your domain's DNS lives. ## Adding a domain 1. On your server's Proxy tab, add the hostname - for example `app.example.com`. 2. At your DNS provider, create an **A record** for that name pointing at the address the proxy slot shows. 3. Wait for the name to resolve. Once it does, the certificate is issued automatically. Renewal runs automatically inside the last 21 days of the certificate's life, so there is nothing to do each year. | Name | Type | Value | | --- | --- | --- | | `app` | A | The address shown on the proxy slot | | `@` (the apex) | A | The address shown on the proxy slot | | `www` | CNAME | `example.com`, or a second A record | Add **both** names to the proxy slot if both should work - a certificate covers only the names it was issued for. The apex cannot be a CNAME. ## Cloudflare If your DNS is on Cloudflare, set the record to **DNS only** (the grey cloud), not proxied. Cloudflare's proxy answers the certificate challenge itself, so the certificate for your server can never be issued, and it only carries HTTP and HTTPS on a fixed list of ports. ## What your app sees behind the proxy - **Plain HTTP, on its own port.** TLS ends at the proxy. An app that redirects every non-HTTPS request to HTTPS will loop for ever - trust the proxy instead. In Express that is `app.set("trust proxy", true)`; in Django, `SECURE_PROXY_SSL_HEADER`. - **The visitor's address in `X-Forwarded-For`**, not in the connection itself. - **The domain in `Host`.** Frameworks with an allowed-hosts list (Django's `ALLOWED_HOSTS`, for one) must include it. Your app still listens on `0.0.0.0` and `SERVER_PORT` exactly as before - the proxy finds it there. ## Game servers and databases The proxy slot carries web traffic. A game server is not reached through it: point an A record straight at the server's address and players connect to the game port. Database hosting servers have no proxy slot; apps connect to their address and port directly. ## Why a certificate is not issued In the order they actually occur: 1. **The name does not resolve here yet.** Check with `dig +short app.example.com A @1.1.1.1` (or `nslookup app.example.com 1.1.1.1` on Windows), not your browser. If the record is right and the answer is old, wait out the record's TTL. 2. **It resolves somewhere else.** An old A record from a previous host is still there, or there are two. Delete the old one rather than adding a second. 3. **A stale AAAA record.** If the server has no IPv6 address, an AAAA record left from a previous host is tried first and fails. Delete it. 4. **The record is proxied by a CDN.** See Cloudflare above. 5. **A CAA record names another certificate authority.** Check with `dig +short example.com CAA`. 6. **Port 80 does not reach the proxy.** Validation happens over HTTP on port 80 for that hostname. Fix the cause, then try once - certificate authorities rate-limit repeated failures. --- # Troubleshooting a Renode deploy Source: https://www.renode.space/docs/troubleshooting Start with the console: it shows what your process printed, and the last lines before it stopped are usually the whole answer. Then run the **pre-flight check** on the GitHub tab, which reads your repository and names what will break. The sections below are the failures that come up again and again, with the fix for each. ## The server will not start **It starts and immediately exits (Node.js).** There is no `start` script in `package.json`, and none of the entry files the server falls back to exists. Add `"start": "node "`. See [Node.js](https://www.renode.space/docs/nodejs). **`can't open file` (Python).** The entry file on the Startup tab is not in the branch. Point it at the script that starts your app. See [Python](https://www.renode.space/docs/python). **`Cannot find module` or `ModuleNotFoundError`.** A dependency is missing from `package.json` or `requirements.txt`. On Python, dependencies declared only in `pyproject.toml` are not installed. **The console stops mid-sentence with no error.** The process reached the memory limit and was killed. There is never a message for this. The usual culprit is a build step on a small plan - build locally and commit the output, or use a larger plan. **It stops because a setting is missing.** Your app expects a `.env` that is not there. The fetch never creates one: upload it through the Files tab into the code folder, or set the values on the Startup tab. **The server restarts again and again.** Read the console for the error that ends each run. Three restarts by itself in an hour put a warning on the server's page and open a support ticket; six suspend it. Restarts you asked for are not counted. ## The port is not reachable The console says the app is listening, but the address does not answer. - **It is bound to `localhost` or `127.0.0.1`.** That is reachable from inside the container only. Bind `0.0.0.0`. - **The port is hardcoded** - 3000, 5000, 8000, 8080. Only the server's own port is reachable. Read `SERVER_PORT` (or `PORT`). - **It runs a development server** - `next dev`, `vite`, `flask run`, `app.run()`. Those usually bind `localhost` and are not meant for production. Use the production server. - **You need a second port.** A port that is not allocated to the server is not reachable. Add an allocation on the Network tab. - **You are using a domain.** Check the domain resolves to the address on the proxy slot - see [Custom domains](https://www.renode.space/docs/domains). ## The build is missing **`next start` cannot find `.next`, or `node dist/index.js` cannot find the file.** Nothing built the project. Put the build in the start script (`"start": "next build && next start -H 0.0.0.0 -p $SERVER_PORT"`, `"start": "tsc && node dist/index.js"`), or build locally and commit the output. **A web hosting site shows a placeholder or a directory listing.** The served folder has no `index.html` at its root. Commit the built `dist/` folder and point the path on the GitHub tab at it. Web hosting never runs a build. See [Static sites and web hosting](https://www.renode.space/docs/web-hosting). ## The wrong Node version runs The Node.js version is the server's runtime image, not something your repository chooses. Pick a version under **Runtime version** on the GitHub tab, or the Docker image on the Startup tab, that matches `engines.node` in `package.json` or your `.nvmrc`. The pre-flight check warns when they differ. Native modules built for one Node major can fail to install or crash on another, and a committed `node_modules` built on Windows or macOS will not run at all - do not commit it. ## A push did not show up - **Restart on push is off.** Restart the server yourself, or tick "Restart the server when someone pushes to this branch" on the GitHub tab. - **Pulling on every start is off** and so is restart on push. Only the first start fetched the branch. Turn either switch on. - **It is a web hosting server.** A restart fetches nothing there. Save the GitHub tab, or turn restart on push on. - **You pushed to a different branch** from the one the GitHub tab names. ## A deleted file is still there On images without `git`, and on web hosting, the branch is unpacked over the folder, so a file removed from the branch is not removed from the server. Delete it through the Files tab. ## Still stuck Open a ticket from the panel - it reaches all staff, and attachments stay private. You can also ask on [Discord](https://discord.com/invite/sfvbC9jkwr) or write to renodespace@gmail.com. On the Console tab, the [AI](https://www.renode.space/docs/ai-assistants) purpose "Server won't start" copies a prompt with the latest console lines, which is the fastest way to hand the problem to an assistant. --- # Deploy to Renode with an AI assistant Source: https://www.renode.space/docs/ai-assistants The panel already knows the facts an assistant would otherwise guess - the runtime, the port, the code path, the boot order, the branch, the databases and the limits of one specific server. It hands them over and leaves every secret out. The full tour is on the [AI setup](https://www.renode.space/ai-setup) page; this is the short version. ## Connect AI on the Console tab On a server's Console tab there is one bar, **Work on this server with AI**. 1. Pick a purpose: - **General** - the server described in full. - **Set up a new project** - start from an empty repository, built the way this server boots. - **Fix my repository** - make a project you already have run here. - **Server won't start** - copies a prompt with the latest console lines instead of a link, so the assistant reads the real error. - **Connect a database** - wire the code to the databases attached to the server, named, with passwords left out. - **Add a domain** - point a domain at the server and serve the project on it. - **Manage this server** - lets the assistant act on the server; see below. 2. Press **Connect AI**. The panel creates a private link and copies it. 3. Paste the link into Claude, ChatGPT or any assistant that can open links. The same link followed by `/mcp` also works as an MCP server URL. There is no duration to choose. A link keeps working while it is used and stops by itself after 7 days unused (24 hours for **Manage this server**), with a hard limit of 90 days (30 for Manage). Making a new link for the same purpose replaces the old one. ## Manage this server A **Manage this server** link can also: - start, stop and restart the server; - send console commands; - read and edit files. Anything else - deleting the server, reinstalling it, databases, backups, users and settings - is never possible through a link. Every action is logged on the server's Activity tab. ## Connect your whole account A link covers one server. To connect Claude, or any MCP client, to your whole account: - **Claude.ai** - add `https://panel.renode.space/api/client/mcp` as a custom connector. - **Claude Code** - run the command below. ```bash claude mcp add --transport http renode https://panel.renode.space/api/client/mcp ``` A browser opens on the panel to approve which servers the assistant may reach, and whether it may manage files only or also start and stop them. Access refreshes itself; disconnect it at any time under **Account → Connected AI**. ## The pre-flight check Once a repository is linked, the GitHub tab reads it and reports what will break: the start script, the port, the build step, the Node version and a missing `.env`. There is no AI in it - the panel reads the repository itself. ## Context written into the repository A switch on the GitHub tab opens a pull request that adds `.renode/server.md` and a block in `CLAUDE.md` or `AGENTS.md`, so coding assistants working in the repository read the server's rules automatically. ## No secrets are shared No password or token is ever shown to the assistant - not through a link, not through a connected account. Database passwords, API keys and deploy tokens are described as existing and where they live, never what they are. Never paste a password or a token into an assistant's chat; upload `.env` yourself through the Files tab. ## For an assistant without the panel If you are an AI assistant helping somebody deploy to RE:NODE and you do not have the server's own link, these rules hold for every app server: - Only `/home/container` persists, and the process runs unprivileged: no root, `sudo`, `apt`, `systemd` or Docker. - Exactly one port is reachable: the one in `SERVER_PORT` (also `PORT` when the panel starts a linked repository). Bind `0.0.0.0`. - Node.js: with a `start` script the server runs `npm install` then `npm run start`; without one it falls back to an entry file such as `index.js`. Python: it installs `requirements.txt` and runs the entry file on the Startup tab. - Web hosting serves the folder as it is: commit the built output, with `index.html` at its root, and start nothing. - `.env` is never in the repository; the user uploads it through the Files tab. - Memory is a hard ceiling; a process that reaches it is killed with no message. - Do not suggest changing the server's startup command or image to work around a code problem - fix the repository. The whole of these docs is available as plain markdown at [renode.space/llms-full.txt](https://www.renode.space/llms-full.txt), with an index at [renode.space/llms.txt](https://www.renode.space/llms.txt).