How to Package Applications for Hop3¶
This guide provides step-by-step instructions for packaging your applications so they can be deployed and managed by Hop3. Whether you're deploying a Python web app, a Node.js service, a static site, or any other type of application, this guide will show you exactly what files to create and how to structure your project.
Table of Contents¶
- Understanding Hop3 Application Structure
- Quick Start: Minimal Configuration
- Configuration Options
- Complete Examples by Technology
- Advanced Patterns
- Testing Your Package
- Troubleshooting
Understanding Hop3 Application Structure¶
Hop3 applications require minimal configuration to deploy. At its core, you need:
- Your application code - The actual source files
- Dependency declaration - How to install required packages
- Process definition - How to run your application
Hop3 supports two configuration approaches:
- Procfile (convention) - Simple, Heroku-compatible, works out of the box
- hop3.toml (configuration) - Advanced, full-featured, optional
You can use either one or both together. When both exist, hop3.toml settings override Procfile settings.
Quick Start: Minimal Configuration¶
Option 1: Using Procfile Only (Simplest)¶
For most applications, a simple Procfile is all you need.
Project structure:
my-app/
├── app.py # Your application code
├── requirements.txt # Python dependencies
└── Procfile # Process definition
Procfile:
That's it! Hop3 will:
- Detect it's a Python application (from requirements.txt)
- Install dependencies automatically
- Run the command specified in Procfile
Option 2: Using hop3.toml Only (Full Control)¶
For more control, use hop3.toml:
Project structure:
hop3.toml:
Option 3: Hybrid Approach (Best of Both)¶
Use Procfile for basics and hop3.toml for advanced features:
Procfile:
hop3.toml:
[metadata]
id = "my-app"
[build]
before-build = "pip install -r requirements-dev.txt"
[[provider]]
name = "postgres"
Result:
- web and worker processes from Procfile
- Build configuration and database from hop3.toml
Configuration Options¶
Procfile Format¶
The Procfile declares process types using the format:
Common process types:
| Process Type | Purpose | Example |
|---|---|---|
web |
HTTP server (public-facing) | web: gunicorn app:app |
worker |
Background job processor | worker: celery worker |
scheduler |
Scheduled tasks | scheduler: celery beat |
static |
Static file directory | static: public |
wsgi |
WSGI application | wsgi: app:application |
Special commands:
| Command | When It Runs | Purpose |
|---|---|---|
prebuild |
Before building | Install build tools, compile assets |
build |
During build phase | Build your application |
prerun |
Before starting app | Database migrations, setup tasks |
Example comprehensive Procfile:
prebuild: npm ci
build: npm run build
prerun: python manage.py migrate
web: gunicorn myapp.wsgi:application --workers 4
worker: celery worker --app=myapp
scheduler: celery beat --app=myapp
static: public
hop3.toml Sections¶
[metadata] - Application Identity¶
[metadata]
id = "my-app" # Required: Unique identifier
version = "1.0.0" # Semantic version
title = "My Application" # Human-readable name
author = "Your Name <you@example.com>"
description = "A brief description"
[build] - Build Process¶
[build]
# Single command
before-build = "npm install"
# Multiple commands (executed with &&)
before-build = ["pip install build-tools", "make setup"]
# Build commands
build = "npm run build"
# System packages needed for build
packages = ["nodejs", "gcc", "make"]
# Python packages for build
pip-install = ["setuptools", "wheel"]
# Run tests after build
test = "npm test"
[run] - Runtime Configuration¶
[run]
# Main application command (maps to Procfile "web:")
start = "gunicorn app:app --workers 4"
# Commands before starting (maps to Procfile "prerun:")
before-run = ["python manage.py migrate", "python manage.py collectstatic"]
# System packages needed at runtime
packages = ["postgresql-client", "redis"]
[env] - Environment Variables¶
[env]
DEBUG = "false"
DATABASE_URL = "postgresql://localhost/mydb"
LOG_LEVEL = "info"
ALLOWED_HOSTS = "myapp.example.com"
⚠️ Security Note: Don't hardcode secrets in hop3.toml. There are two cases:
- App-internal random secrets (
SECRET_KEY,SECRET_KEY_BASE,APP_KEY, …) — values the app only needs to be random and unguessable. Declare them and Hop3 generates one on the first deploy, persists it, and never rotates or commits it:
See Generated secrets for the available generators.
- Externally-supplied secrets (API tokens, third-party passwords) — specific values you provide. Set these with the CLI, never in
hop3.toml:
[port] - Port Configuration¶
[port]
web = 8000 # Main web service port
api = 8080 # API service port
metrics = 9090 # Metrics endpoint
[healthcheck] - Health Monitoring¶
[healthcheck]
path = "/health/" # Health check endpoint
timeout = 30 # Request timeout (seconds)
interval = 60 # Check frequency (seconds)
[[provider]] - Service Dependencies¶
Use double brackets [[provider]] for arrays in TOML.
# PostgreSQL database
[[provider]]
name = "postgres"
plan = "standard"
version = "15"
# Redis cache
[[provider]]
name = "redis"
plan = "basic"
version = "7"
Complete Examples by Technology¶
Python: Flask with pip¶
Files needed:
app.py:
from flask import Flask
import os
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello from Flask!'
if __name__ == '__main__':
port = int(os.environ.get('PORT', 5000))
app.run(host='0.0.0.0', port=port)
requirements.txt:
Procfile:
Deploy:
Python: Django with Poetry¶
Files needed:
django-app/
├── myproject/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
├── pyproject.toml
└── hop3.toml
pyproject.toml:
[tool.poetry]
name = "django-app"
version = "1.0.0"
[tool.poetry.dependencies]
python = "^3.10"
Django = "^4.2"
gunicorn = "^21.0"
psycopg2-binary = "^2.9"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
hop3.toml:
[metadata]
id = "django-app"
version = "1.0.0"
[build]
# Poetry automatically installs dependencies
before-build = "poetry install --no-dev"
[run]
start = "poetry run gunicorn myproject.wsgi:application --workers 4"
before-run = "poetry run python manage.py migrate --noinput"
[env]
DJANGO_SETTINGS_MODULE = "myproject.settings"
SECRET_KEY = "change-this-in-production"
[[provider]]
name = "postgres"
plan = "standard"
Deploy:
cd django-app
git init
git add .
git commit -m "Initial commit"
# SECRET_KEY is declared as { generate = "hex", length = 32 } in hop3.toml [env],
# so it's created automatically on the first deploy — no manual step.
hop3 deploy
Node.js: Express Application¶
Files needed:
package.json:
{
"name": "express-app",
"version": "1.0.0",
"main": "app.js",
"dependencies": {
"express": "^4.18.0"
},
"scripts": {
"start": "node app.js"
}
}
app.js:
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
const host = process.env.BIND_ADDRESS || '0.0.0.0';
app.get('/', (req, res) => {
res.send('Hello from Express!');
});
app.listen(port, host, () => {
console.log(`Server running on ${host}:${port}`);
});
Procfile:
Deploy:
cd express-app
npm install # Generate package-lock.json
git init
git add .
git commit -m "Initial commit"
hop3 deploy
Node.js: TypeScript Application¶
Files needed:
package.json:
{
"name": "typescript-app",
"version": "1.0.0",
"scripts": {
"build": "tsc",
"start": "node dist/server.js"
},
"dependencies": {
"express": "^4.18.0"
},
"devDependencies": {
"@types/express": "^4.17.0",
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
}
}
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true
}
}
hop3.toml:
[metadata]
id = "typescript-app"
[build]
before-build = "npm ci"
build = "npm run build"
test = "npm test"
[run]
start = "npm start"
Static Website¶
Files needed:
public/index.html:
<!DOCTYPE html>
<html>
<head>
<title>My Static Site</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello from Hop3!</h1>
<script src="script.js"></script>
</body>
</html>
Procfile:
Deploy:
Go Application¶
Files needed:
go.mod:
server.go:
package main
import (
"fmt"
"os"
"github.com/gin-gonic/gin"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.String(200, "Hello from Go!")
})
r.Run(":" + port)
}
Procfile:
hop3.toml:
Advanced Patterns¶
Multi-Process Applications¶
Procfile:
# Web server
web: gunicorn app:app --workers 4
# Background worker
worker: celery worker --app=myapp --loglevel=info
# Scheduler
scheduler: celery beat --app=myapp --loglevel=info
# Real-time service
realtime: python websocket_server.py
Each process type runs independently and scales separately.
Build-Time vs Runtime Packages¶
Some packages are only needed during build, others at runtime:
hop3.toml:
[build]
# Build-time packages (compilers, build tools)
packages = ["gcc", "g++", "make", "nodejs"]
pip-install = ["setuptools", "wheel", "cython"]
# Build step
build = "python setup.py build_ext"
[run]
# Runtime packages (minimal set for production)
packages = ["postgresql-client"]
# No build tools needed at runtime
start = "gunicorn app:app"
Benefits: - Smaller runtime container - Faster deployments - Better security (fewer packages = smaller attack surface)
Environment-Specific Configuration¶
Local .env file (not committed):
Production (via CLI):
hop3 config set --app myapp DEBUG=false
hop3 config set --app myapp DATABASE_URL="postgresql://prod-server/prod_db"
# SECRET_KEY: declare `{ generate = "hex", length = 32 }` in hop3.toml [env]
# instead — generated once and persisted, never typed or committed.
hop3.toml (defaults only):
Database Migrations¶
Option 1: Before Run Hook (Recommended)
hop3.toml:
Migrations run automatically before each deployment.
Option 2: Separate Command
Procfile:
Deploy with migrations:
Asset Compilation¶
hop3.toml:
[build]
before-build = "npm ci"
build = ["npm run build", "python manage.py collectstatic --noinput"]
[run]
start = "gunicorn app:app"
What happens:
1. Install npm dependencies (npm ci)
2. Build frontend assets (npm run build)
3. Collect Django static files (collectstatic)
4. Start application
Multi-Stage Builds¶
For Node.js applications:
hop3.toml:
[build]
# Install all dependencies (including dev)
before-build = "npm ci"
# Build production assets
build = "npm run build"
# Prune dev dependencies before deployment
build = ["npm run build", "npm prune --production"]
[run]
start = "node dist/server.js"
Custom Build Scripts¶
package.json:
{
"scripts": {
"build": "tsc && webpack --mode production",
"postbuild": "node scripts/optimize.js"
}
}
hop3.toml:
[build]
before-build = "npm ci"
build = "npm run build"
# npm automatically runs postbuild after build
Testing Your Package¶
1. Test Locally First¶
Create local .env file:
Run your start command:
# Python example
source .env
gunicorn app:app --bind 0.0.0.0:$PORT
# Node.js example
source .env
node app.js
Test in browser:
2. Validate Configuration Files¶
Check Procfile syntax:
# Each line should be: <process-type>: <command>
cat Procfile
# Good:
web: gunicorn app:app
# Bad (missing colon):
web gunicorn app:app
Validate hop3.toml:
3. Test Deployment in Staging¶
Create a test deployment:
# Deploy to staging first
hop3 deploy --app myapp-staging
# Test the deployed app
curl https://myapp-staging.hop3.example.com
# Check logs
hop3 app logs --app myapp-staging
# Check status
hop3 app status --app myapp-staging
4. Common Issues to Check¶
Environment variables:
# Verify all required env vars are set
hop3 config show --app myapp
# Add missing vars
hop3 config set --app myapp DATABASE_URL="..."
Port binding:
# Your app MUST use the PORT environment variable
# Hop3 sets this automatically
# Good (Python):
port = int(os.environ.get('PORT', 5000))
# Good (Node.js):
const port = process.env.PORT || 3000
# Bad (hardcoded):
app.listen(8000) # Won't work!
File paths:
# Use relative paths, not absolute
# Good:
./manage.py migrate
# Bad:
/home/user/myapp/manage.py migrate
Troubleshooting¶
Application Won't Start¶
Check logs:
Common causes:
-
Wrong start command
-
Missing dependencies
-
Port binding issues
Build Failures¶
Check build logs:
Common causes:
-
Missing build dependencies
-
Build timeout
Database Connection Errors¶
Check DATABASE_URL:
Verify database service:
# List attached services
hop3 addon list --app myapp
# Attach database if missing
hop3 addon create postgres myapp-db
hop3 addon attach myapp-db --app myapp
Static Files Not Serving¶
Check static configuration:
Verify directory exists:
Permission Errors¶
Don't use sudo in commands:
Environment Variable Not Available¶
Set via CLI:
# Set the variable
hop3 config set --app myapp MY_VAR="value"
# Restart to apply
hop3 app restart --app myapp
# Verify
hop3 config show --app myapp
Best Practices Checklist¶
- Use
$PORTenvironment variable for port binding - Never hardcode secrets in
hop3.tomlorProcfile - Include all dependencies in
requirements.txt/package.json - Test locally before deploying
- Use
before-runfor database migrations - Set up health check endpoints
- Configure automated backups for production
- Use environment variables for configuration
- Keep build artifacts out of git (use
.gitignore) - Document required environment variables in README
Quick Reference¶
Minimal Flask App:
Minimal Node.js App:
Static Site:
With Database:
With Build Step:
Next Steps¶
- Quickstart Guide - Deploy your first app
- hop3.toml Reference - Complete configuration reference
- Migration Guide - Migrate from other platforms
- CLI Reference - All available commands
For help, run: