# Hostney - Build & Deploy Guide for AI agents > How to structure an application so it deploys and runs on Hostney on the first try. > Hostney runs your app in an isolated container (or serves static builds from the edge), > deployed from Git. This file is the authoritative contract. Human version: https://www.hostney.com/build-guide Last updated: 2026-07-18 ## Universal rules (true for every runtime) ### Bind to 0.0.0.0 on $PORT A server app MUST listen on 0.0.0.0 (all interfaces), on the port given by the PORT environment variable. Binding to localhost / 127.0.0.1, or hardcoding a port, is the single most common reason a build succeeds but the app never becomes reachable. Default internal ports: Node 3000, Python 8000, Ruby 3000 - but always read PORT rather than assuming. ### Framework is auto-detected from your files On connect, the platform inspects your repo and picks the runtime and framework: package.json (Node), manage.py / requirements.txt / pyproject.toml (Python), bin/rails / Gemfile / config.ru (Ruby). It sets the install command, build/start command, and output directory for you. You can override any of them in the app’s build settings. ### Commit your lockfile Commit the lockfile for your ecosystem: package-lock.json / pnpm-lock.yaml / yarn.lock / bun.lockb for Node, and Gemfile.lock for Ruby (required - the build fails clearly without it). Ruby lockfiles must include the Linux platform; if you locked on Windows or macOS, run: bundle lock --add-platform x86_64-linux. ### Environment variables & secrets Set build-time and runtime variables in the panel. Mark any variable as a secret and its value is sealed - write-only, never shown in the UI or API again, though your app still receives it. Rotate a secret by entering a new value. Anything compiled into a static bundle is visible to browsers, so never put secrets there. ### Databases reach mysql-host over TCP MySQL is reachable from your container at the host mysql-host on port 3306 over TCP. The host is injected for you. Create the database and user in the panel. An external MySQL user must allow the 10.88.0.0/16 range (the container bridge). Configure your app via a DATABASE_URL or discrete DB_HOST/DB_USERNAME/DB_PASSWORD/DB_NAME variables. ### Three deploy modes Auto deploy: every push to the tracked branch builds and goes live. Build only: every push builds but stays on the current version until you promote it. Paused: pushes are ignored. Choose per app in the deployment settings. ### Promote & rollback are artifact swaps Every successful build is stored as an artifact. Promoting a previous build swaps it back to live in seconds - no rebuild, no Git revert. Static builds are kept indefinitely; container (SSR) builds keep the five most recent successful artifacts per app, and the currently-live one is always protected. ### Health check gates every deploy A new container must answer on its port within 30 seconds before it takes over traffic (blue/green). If it never comes up, the previous build keeps serving and the deploy is marked failed - a bad build cannot take your app down. ## Git providers Supported: GitHub, GitLab, Bitbucket. Connect any of the three over OAuth or a personal access token. All three support automatic deploys: a push to the tracked branch triggers a build via webhook. If a webhook can’t be registered (missing scope), the connection still saves and you can trigger builds manually from the panel. ## Build settings you can configure - **Install command** - Runs before the build. Defaulted from your framework; override for custom dependency steps. - **Build command** - For Node/static this builds your output. For Python/Ruby this is effectively the container start command. - **Output directory** - Where the built static assets live (e.g. dist, build, .next). Used for static/SPA deploys. - **Root directory** - Subfolder to build from, for monorepos. Defaults to the repo root. - **Runtime version** - Node 24/22/20, Python 3.13/3.12/3.11, or Ruby 4.0/3.4/3.3. Or pin it in your repo (see each runtime below). - **Internal port** - Override the port the platform expects your server on. Defaults: Node 3000, Python 8000, Ruby 3000. - **Skip tests** - Node only. When off, the build runs your test script in CI mode and fails the deploy if tests fail. ## Runtimes ### Node.js & Next.js (marketing page: https://www.hostney.com/nodejs) Next.js and Nuxt run as container SSR; Express, Fastify, NestJS, Hono, and any Node server run under the Node preset. - Detection: package.json (+ your lockfile picks npm / pnpm / yarn / bun). - Runtime versions: Node 24, 22 (recommended), 20 - Pin the version: Set via .nvmrc, .node-version, or the "engines" field in package.json. - Internal port: 3000 - Install: npm install (or the detected package manager equivalent) - Start: Framework default (e.g. next start), or your "start" script / a custom command. - Env vars: None required for a plain server. Add your own as needed. NODE_OPTIONS is set to cap memory unless you override it. - Notes: - Next.js: output: "export" is served as static from the edge; standard/standalone builds run as a live container (SSR, API routes, and ISR all work). - Any framework is fine as long as it listens on 0.0.0.0:$PORT. Example - Express (server.js): ```javascript const express = require('express'); const app = express(); app.get('/', (req, res) => { res.json({ framework: 'express', message: 'hello from hostney' }); }); app.get('/health', (req, res) => res.json({ ok: true })); // Bind to 0.0.0.0 and the port Hostney provides. const port = process.env.PORT || 3000; app.listen(port, '0.0.0.0', () => console.log(`listening on ${port}`)); ``` Example - Next.js (next.config.js): ```javascript /** @type {import('next').NextConfig} */ module.exports = { // Omit "output" for SSR (runs as a container - API routes, ISR, etc.). // Set output: 'export' to ship a static site to the edge instead. // Next binds to 0.0.0.0:$PORT via `next start` - nothing else to configure. }; ``` ### Nuxt (marketing page: https://www.hostney.com/nuxt) Nuxt 3 with the Nitro engine. Server-rendered as a container, or statically generated to the edge. - Detection: Nuxt in package.json (+ your lockfile). - Runtime versions: Node 24, 22 (recommended), 20 - Pin the version: Set via .nvmrc, .node-version, or "engines" in package.json. - Internal port: 3000 - Install: npm install (or detected package manager) - Start: SSR: nuxt build then the Nitro server. Static: nuxt generate (served from the edge). - Env vars: None required. Nitro reads PORT automatically, so SSR binding just works. - Notes: - Run nuxt generate to ship a static site to the edge (no container, no app slot used). - A standard build runs as a live container: SSR, Nitro server routes, and server API all work. Example - Nitro server route (server/api/health.get.ts): ```javascript // Nuxt binds to 0.0.0.0:$PORT automatically via Nitro - nothing to configure. // A server route looks like this: export default defineEventHandler(() => { return { ok: true }; }); ``` Example - Static generate (nuxt.config.ts): ```javascript export default defineNuxtConfig({ // Leave as SSR (a live container), or pre-render everything to the edge: nitro: { prerender: { crawlLinks: true, routes: ['/'] } }, }); // Build with `nuxt generate` to ship static output (no app slot used). ``` ### Python (marketing page: https://www.hostney.com/python) Django, Flask, and FastAPI, built on a slim Python image (glibc - compiled wheels install cleanly). - Detection: Django from manage.py; Flask and FastAPI from your dependencies (requirements.txt / Pipfile). - Runtime versions: Python 3.13, 3.12 (recommended), 3.11 - Pin the version: Set via .python-version, requires-python in pyproject.toml, or runtime.txt. - Internal port: 8000 - Install: pip install --no-cache-dir -r requirements.txt (Django also runs collectstatic and migrate). - Start: Django: gunicorn .wsgi:application --bind 0.0.0.0:$PORT. Flask: gunicorn app:app --bind 0.0.0.0:$PORT. FastAPI: uvicorn main:app --host 0.0.0.0 --port $PORT. - Env vars: Django: DJANGO_SECRET_KEY, DJANGO_ALLOWED_HOSTS (* for testing, your fqdn in prod). Flask: SECRET_KEY. FastAPI: none required. - Notes: - gunicorn/uvicorn take the bind address on the command line, which the platform sets for you. - Expose a module-level WSGI/ASGI callable named "app" (Flask/FastAPI) or "application" (Django wsgi.py). Example - FastAPI (main.py): ```python from fastapi import FastAPI app = FastAPI() @app.get("/") async def hello(): return {"framework": "fastapi", "message": "hello from hostney"} @app.get("/health") async def health(): return {"ok": True} # Start (set by Hostney): uvicorn main:app --host 0.0.0.0 --port $PORT ``` Example - Flask (app.py): ```python import os from flask import Flask, jsonify app = Flask(__name__) app.secret_key = os.environ.get("SECRET_KEY", "dev-key") @app.route("/") def hello(): return jsonify(framework="flask", message="hello from hostney") @app.route("/health") def health(): return jsonify(ok=True) # Start (set by Hostney): gunicorn app:app --bind 0.0.0.0:$PORT --workers 2 ``` Example - Django (settings.py (env-driven) + wsgi.py): ```python # settings.py - read config from the environment import os SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "*").split(",") # wsgi.py exposes a module-level "application": # application = get_wsgi_application() # Start (set by Hostney): # gunicorn .wsgi:application --bind 0.0.0.0:$PORT --workers 2 ``` ### Ruby (marketing page: https://www.hostney.com/ruby) Ruby on Rails, Sinatra, and any Rack app (Roda, Grape, Hanami, Padrino), all served by Puma. - Detection: Rails from bin/rails + gem "rails"; Sinatra from gem "sinatra" + config.ru; otherwise generic Rack from config.ru. - Runtime versions: Ruby 4.0, 3.4 (recommended), 3.3 - Pin the version: Set via .ruby-version, the ruby "x.y.z" directive in your Gemfile, or Gemfile.lock. - Internal port: 3000 - Install: bundle install (from your committed Gemfile.lock). - Start: Rails: bundle exec puma -C config/puma.rb. Sinatra: bundle exec rackup -s puma -o 0.0.0.0 -p $PORT. Rack: bundle exec puma config.ru -b tcp://0.0.0.0:$PORT. - Env vars: Rails: RAILS_MASTER_KEY (contents of config/master.key) or SECRET_KEY_BASE. DATABASE_URL optional (falls back to DB_* against mysql-host). RAILS_ENV/RACK_ENV/PORT are set for you. - Notes: - Gemfile.lock is required. Lock for Linux: bundle lock --add-platform x86_64-linux. - Rails builds multi-stage and precompiles assets; Node + Yarn are auto-installed if you use jsbundling/cssbundling. - Rails apps run bin/rails db:prepare at boot (creates the DB if missing, runs pending migrations - idempotent). - Values are read straight from ENV[...] at process start (no .env file baked in). Example - Rails (Puma) (config/puma.rb): ```ruby # Puma binds to 0.0.0.0 on the port Hostney injects. port ENV.fetch("PORT", 3000) bind "tcp://0.0.0.0:#{ENV.fetch('PORT', 3000)}" environment ENV.fetch("RAILS_ENV", "production") workers ENV.fetch("WEB_CONCURRENCY", 2).to_i threads_count = ENV.fetch("RAILS_MAX_THREADS", 5).to_i threads threads_count, threads_count preload_app! ``` Example - Sinatra (app.rb): ```ruby require "sinatra/base" require "json" class App < Sinatra::Base set :bind, "0.0.0.0" set :port, ENV.fetch("PORT", "3000").to_i set :environment, :production get("/") { { framework: "sinatra" }.to_json } get("/health") { { ok: true }.to_json } end # Start (set by Hostney): bundle exec rackup -s puma -o 0.0.0.0 -p $PORT ``` Example - Rack (any) (config.ru + app.rb): ```ruby # app.rb - a bare Rack app class App def call(env) body = { framework: "rack" }.to_json [200, { "content-type" => "application/json" }, [body]] end end # config.ru require "./app" run App.new # Start (set by Hostney): bundle exec puma config.ru -b tcp://0.0.0.0:$PORT ``` ### Static & SPA (marketing page: https://www.hostney.com/spa) React, Vue, Svelte, Angular, Astro, SvelteKit, Vite, Gatsby, and more - built and served from the edge. - Detection: Framework from package.json; the build command and output directory are set for you. - Runtime versions: Built with Node 24, 22 (recommended), or 20. - Pin the version: Set via .nvmrc, .node-version, or "engines" in package.json. - Internal port: n/a - no running server. Output is served from the edge cache. - Install: npm install (or detected package manager) - Start: None. The build output (e.g. dist) is published to the edge; client-side routes get an SPA fallback. - Env vars: Build-time only: VITE_, NEXT_PUBLIC_, REACT_APP_ prefixes are baked into the bundle at build time. Never put secrets in a client bundle. - Notes: - No container and no app slot used - ship as many static apps as you like. - Deep links and refreshes on client routes resolve to your app via the SPA fallback. - Custom domains get automatic HTTPS. Example - Vite (React / Vue / Svelte) (package.json): ```json { "scripts": { "build": "vite build" } } // Output directory: dist (detected automatically) // No start command - dist/ is served from the edge with an SPA fallback. ``` Example - Astro (astro.config.mjs): ```javascript import { defineConfig } from 'astro/config'; // Default static output builds to ./dist and serves from the edge. export default defineConfig({}); // Build: astro build → output directory: dist ``` ## Pre-deploy checklist - [ ] Server listens on 0.0.0.0 using process.env.PORT / ENV["PORT"] (not localhost, not a hardcoded port). - [ ] A /health route returns 200 quickly (used to confirm the app is up). - [ ] Lockfile is committed (Gemfile.lock for Ruby - and add the x86_64-linux platform). - [ ] Required env vars are set in the panel before the first deploy (see your framework above). - [ ] Framework-required secrets (Rails master key, Django secret key, Flask secret) are marked as secrets. - [ ] Database access, if any, targets mysql-host:3306 over TCP with panel-created credentials. ## Not supported as Applications Go, Rust, Java, PHP frameworks (Laravel, Symfony), Hugo, Jekyll, Remix. These are not deployable as Applications today. Go is on the roadmap. ### WordPress and Drupal: one-click install Both are installed from the Marketplace, not from Git. Pick the subdomain and we provision the site, its MySQL database and user, the PHP version, and the web server config for you. Drupal 10 and 11 are built with Composer and ship with a project-local Drush; its docroot is the web/ subfolder and its trusted host pattern is pinned to the FQDN at install time. Admin and database credentials are shown once, at install. ### Custom PHP and plain static HTML: subdomain website type Create the subdomain and choose the website type (Apache, or Nginx with PHP). Upload over SFTP, Git-clone over SSH, or use the file manager. Each site gets its own containerised PHP-FPM pool and its own PHP version. A subdomain hosts exactly one thing: a Git application, a WordPress install, or a Marketplace app. The platform refuses the second one rather than overwriting the first, so pick the path before you pick the subdomain.