Back to KB
Difficulty
Intermediate
Read Time
8 min

Ship an app on Ghost + Fly.io for $2/month

By Codcompass Team··8 min read

Current Situation Analysis

Managed database providers traditionally enforce minimum monthly fees, typically hovering around $25, regardless of actual query volume or traffic patterns. This pricing model creates a structural barrier for hobby projects, internal prototypes, and sparse-traffic utilities that generate minimal database load but require persistent relational storage. Developers are forced to either absorb unnecessary overhead or compromise on data integrity by switching to file-based or in-memory alternatives.

The problem is frequently misunderstood because deployment guides still assume manual infrastructure management. Traditional workflows require developers to provision databases through web consoles, configure connection strings manually, and maintain Dockerfiles or platform-specific configuration files. This manual overhead obscures a critical shift in modern development: AI coding agents can now autonomously handle CLI execution, containerization, schema provisioning, and deployment orchestration. When infrastructure tasks are delegated to an agent, the cost model can be decoupled from traffic volume.

Ghost addresses this by offering a Postgres-compatible service metered strictly by active compute hours. The free tier provides 100 active compute hours monthly and 1TB of storage. Compute is billed in 15-minute increments only when queries execute; idle databases consume zero compute. Fly.io complements this with an edge deployment platform that supports automatic machine suspension. When paired with an AI agent capable of executing shell commands and interacting with MCP (Model Context Protocol) servers, developers can provision a fully functional, publicly accessible Postgres-backed application for approximately $2 per month under low-traffic conditions.

WOW Moment: Key Findings

The following comparison illustrates the operational and financial divergence between traditional managed hosting and an agent-native, metered infrastructure stack.

ApproachMinimum Monthly CostIdle State BillingSetup ComplexityAgent Compatibility
Traditional Managed PaaS~$25.00Billed for reserved computeHigh (GUI/Manual CLI)Low (Requires manual adaptation)
Ghost + Fly.io + AI Agent~$2.00Billed only for storage/bandwidthLow (Agent-driven provisioning)High (Native MCP/Shell integration)

This finding matters because it shifts infrastructure economics from capacity-based pricing to usage-based pricing. Developers can maintain persistent relational data without paying for idle compute cycles. The agent-native workflow eliminates boilerplate configuration, reduces human error in deployment pipelines, and enables rapid iteration. For sparse-traffic applications, this architecture transforms database hosting from a fixed operational expense into a variable, near-zero cost.

Core Solution

The deployment pipeline relies on three coordinated components: an AI agent with shell execution capabilities, Ghost for metered Postgres provisioning, and Fly.io for edge compute with automatic suspension. The following implementation demonstrates a service heartbeat tracker (infra-pinger) that logs service status checks into a relational table.

1. Bootstrap the Toolchain

Install the Ghost CLI and Fly.io client. Authentication requires browser-based OAuth flows. Both platforms require a payment method on file, though charges only accrue when resources are actively consumed.

# Install Ghost CLI
curl -fsSL https://install.ghost.build | sh

# Install Fly.io client
curl -L https://fly.io/install.sh | sh

# Authenticate
ghost login
flyctl auth login

Configure the Ghost MCP server within your AI agent environment. This enables the agent to execute database provisioning commands programmatically.

ghost mcp install <your-agent-name>

Restart the agent to load the new MCP configuration. Verify installations:

ghost --version
flyctl version

2. Scaffold Application & Provision Database

Delegate scaffolding to the agent. The agent will generate the application structure, initialize a Ghost database, and execute schema creation.

Agent Prompt:

Create a directory named `infra-pinger`. Inside, build a Node.js application using Express and the `pg` package. The app should expose two routes:
- GET / : Renders a simple HTML form to submit a service heartbeat.
- POST /heartbeat : Accepts form data (service_name, status), inserts a record into a Postgres table, and redirects to /.

Generate the following files:
- package.json (dependencies: express, pg, dotenv)
- server.js (Express setup, route handlers, DB connection logic)
- Dockerfile (Node 20 Alpine base, multi-stage build)
- fly.toml (app config, auto-stop enabled, internal port 3000)
- .dockerignore (exclude node_modules, .env, .git)

Using the Ghost MCP:
1. Provision a database named `pinger-db`.
2. Execute a raw SQL statement to create a `heartbeats` table with columns: id (serial PK), service_name (varchar(100) not null), status (varchar(20) not null), recorded_at (timestamptz default now()).
3. Output the connection string.

The agent will generate the files, provision the database, and return a connection string resembling: postgres://tsdbadmin:***@***.tsdb.cloud.timescale.com:5432/tsdb?sslmode=require

3. Local Validation & SSL Configuration

The pg library interprets sslmode=require in the connection string as verify-full, which rejects Timescale/Ghost's certificate chain

during local testing. Strip the query parameter and configure the pool explicitly.

Agent Prompt:

1. Create a .env file containing DATABASE_URL=<connection_string>. Add .env to .gitignore.
2. Update server.js to load dotenv. Configure the pg Pool by parsing the DATABASE_URL, removing the sslmode query parameter, and passing ssl: { rejectUnauthorized: false } to the pool constructor.
3. Run npm install, start the server on port 3000, and execute curl commands to test both routes.
4. Verify the record appears in the database using the Ghost MCP.
5. Terminate the local process.

The agent will handle dependency installation, apply the SSL workaround, execute integration tests, and confirm data persistence.

4. Fly.io Deployment & Machine Scaling

Deploy the containerized application. Skip flyctl launch to prevent automatic provisioning of unwanted managed databases.

Agent Prompt:

1. Generate a unique app name: infra-pinger-<random_suffix>.
2. Update fly.toml with the new app name.
3. Execute: flyctl apps create <app_name>
4. Inject the connection string as a secret: flyctl secrets set DATABASE_URL="<connection_string>" --app <app_name>
5. Deploy with high availability disabled: flyctl deploy --ha=false
6. Force a single machine instance: flyctl scale count 1 --app <app_name> --yes
7. Output the public URL.

Fly.io will build the Docker image, push it to the registry, and route traffic to the deployed machine. The --ha=false flag prevents multi-region replication, and scale count 1 ensures the auto-stop billing model remains accurate.

5. Production Verification

Validate end-to-end functionality against the live endpoint.

Agent Prompt:

1. curl the public URL to verify the HTML form renders.
2. POST a heartbeat record with service_name="api-gateway" and status="healthy".
3. curl the URL again to confirm the redirect.
4. Query the heartbeats table via Ghost MCP to verify the row exists.

The application is now live, accepting HTTPS traffic, and persisting data to a metered Postgres instance.

Pitfall Guide

1. The pg SSL Verification Mismatch

Explanation: Modern versions of the pg driver treat sslmode=require as verify-full, enforcing strict certificate validation. Ghost/Timescale uses intermediate certificates that the local Node.js runtime may not recognize, causing connection failures. Fix: Parse the connection string to remove sslmode=require, then pass ssl: { rejectUnauthorized: false } to the Pool constructor. This maintains encryption while bypassing strict chain validation in non-production environments.

2. Fly.io High Availability Defaults

Explanation: Fly.io defaults to deploying two machines for redundancy. This breaks the auto-stop cost model, as idle machines still consume storage and network resources. Fix: Always deploy with --ha=false and immediately run flyctl scale count 1 --app <name> --yes to enforce a single-machine topology.

3. Idle Compute Metering Misunderstanding

Explanation: Ghost bills compute in 15-minute increments. A single query that takes 2 seconds still consumes a full 15-minute billing chunk. Developers often assume per-second billing. Fix: Batch queries where possible, use connection pooling to reduce connection overhead, and monitor active hours via the Ghost dashboard to stay within the 100-hour free tier.

4. Agent Shell Permission Overreach

Explanation: AI agents with shell access can execute destructive commands (rm -rf, flyctl destroy, ghost delete). Without explicit boundaries, agents may misinterpret prompts and delete infrastructure. Fix: Configure agent sandboxing, review shell commands before approval, and use Fly.io/Ghost project-level permissions to restrict destructive operations.

5. Hardcoded Connection Strings in Version Control

Explanation: Committing .env files or embedding connection strings directly in source code exposes credentials and breaks deployment portability. Fix: Always add .env to .gitignore. Inject secrets via platform-specific secret management (flyctl secrets set) and load them at runtime using dotenv or environment variable access.

6. Skipping Post-Deploy Scaling

Explanation: Fly.io's deployment pipeline occasionally spawns a second machine during the rolling update process, even with --ha=false. This doubles idle storage costs. Fix: Make flyctl scale count 1 a mandatory post-deploy step in your automation scripts.

7. Migration Framework Overhead

Explanation: Using Prisma, Knex, or Sequelize for a single-table hobby app introduces unnecessary build steps, schema sync commands, and dependency bloat. Fix: For sparse-traffic applications, execute raw SQL via the agent or Ghost MCP. Reserve migration frameworks for multi-table, version-controlled production schemas.

Production Bundle

Action Checklist

  • Install and authenticate Ghost CLI and Fly.io client
  • Configure Ghost MCP server in your AI agent environment
  • Scaffold application structure with Dockerfile and fly.toml
  • Provision Ghost database and execute schema creation via MCP
  • Apply SSL workaround for pg driver and validate locally
  • Deploy to Fly.io with --ha=false and enforce single-machine scaling
  • Inject DATABASE_URL as a platform secret
  • Verify end-to-end data persistence and HTTPS routing

Decision Matrix

ScenarioRecommended ApproachWhyCost Impact
Hobby/Prototype (<100 req/day)Ghost + Fly.io + AgentMetered compute aligns with sparse traffic; auto-stop eliminates idle costs~$2/mo
Internal Tooling (Team Use)Ghost + Fly.io + AgentFast provisioning, no GUI overhead, easy teardown~$5-10/mo
Production SaaS (>10k req/day)Managed Postgres + Kubernetes/VMPredictable scaling, SLA guarantees, advanced monitoring$50-200+/mo
Read-Heavy AnalyticsGhost + Read ReplicasGhost supports fork/share workflows; pair with caching layerVariable

Configuration Template

fly.toml

app = "infra-pinger-<suffix>"
primary_region = "iad"

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true
  min_machines_running = 0

package.json

{
  "name": "infra-pinger",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "pg": "^8.11.3",
    "dotenv": "^16.3.1"
  }
}

server.js

require('dotenv').config();
const express = require('express');
const { Pool } = require('pg');
const url = require('url');

const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

const dbUrl = new URL(process.env.DATABASE_URL);
dbUrl.searchParams.delete('sslmode');

const pool = new Pool({
  connectionString: dbUrl.toString(),
  ssl: { rejectUnauthorized: false }
});

app.get('/', (req, res) => {
  res.send(`
    <form method="POST" action="/heartbeat">
      <input name="service_name" placeholder="Service Name" required />
      <select name="status">
        <option value="healthy">Healthy</option>
        <option value="degraded">Degraded</option>
        <option value="down">Down</option>
      </select>
      <button type="submit">Log Heartbeat</button>
    </form>
  `);
});

app.post('/heartbeat', async (req, res) => {
  const { service_name, status } = req.body;
  try {
    await pool.query(
      'INSERT INTO heartbeats (service_name, status) VALUES ($1, $2)',
      [service_name, status]
    );
    res.redirect('/');
  } catch (err) {
    console.error('DB Error:', err);
    res.status(500).send('Failed to log heartbeat');
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Quick Start Guide

  1. Install CLIs and authenticate: curl -fsSL https://install.ghost.build | sh && curl -L https://fly.io/install.sh | sh
  2. Configure MCP in your agent: ghost mcp install <agent-name>
  3. Prompt your agent to scaffold the app, provision the database, and apply the SSL workaround
  4. Deploy with flyctl deploy --ha=false and enforce scaling: flyctl scale count 1 --app <name> --yes
  5. Verify via HTTPS endpoint and query the database through Ghost MCP