Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 86 additions & 10 deletions plugins/upsun/skills/upsun/references/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ Full config reference: https://developer.upsun.com/docs/get-started/here/configu

## Hook phases

Hooks are shell scripts (Dash) that run in order:
Hooks are Dash shell scripts that run in order:

1. **`build`** -- runs in an isolated container with internet access but **no access to services** (databases, caches). The app filesystem is writable here. Start with `set -ex` to fail fast.
1. **`build`** -- runs in an isolated container with internet access but **no access to services** (databases, caches). The app filesystem is writable here. Start hooks with `set -ex` to fail fast on errors.
2. **`deploy`** -- runs after the app is built and connected to services, but before it accepts internet traffic. Use for database migrations, cache warming. The app filesystem is **read-only** at this point.
3. **`post_deploy`** -- runs after the app is open to traffic. Use only when tasks must happen after public access (reduces deployment downtime for long-running tasks, but risks inconsistency).

## `.environment` file

A shell script at the app root, sourced at runtime. Use it to construct derived env vars from the auto-generated service variables:
A shell script at the app root, sourced at runtime. Use it to construct derived env vars from the auto-generated relationship variables:

```bash
# Example: build a DATABASE_URL from the auto-generated relationship vars.
# Example: build a DATABASE_URL from auto-generated relationship vars.
# If the relationship is named "database", Upsun exposes $DATABASE_HOST, $DATABASE_PORT, etc.
export DATABASE_URL="${DATABASE_SCHEME}://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_PATH}"
```
Expand All @@ -26,12 +26,14 @@ Write to `.environment` in the build hook if the content needs shell logic.

## Minimal app (no services)

A minimal working config for a single Node.js app. Adjust the runtime version to a supported image from the registry.

```yaml
applications:
myapp:
type: 'nodejs:24' # check https://meta.upsun.com/images for supported versions
type: 'nodejs:24' # pick a supported runtime version from the registry
build:
flavor: none # manage dependencies explicitly in the build hook
flavor: none # prefer explicit dependency management in build hook
hooks:
build: |
set -ex
Expand Down Expand Up @@ -71,11 +73,13 @@ services:
type: 'redis:7.2'
```

Relationship names become the env var prefix (uppercased). The vars are runtime-only.

---

## Monorepo (multiple apps)

Use `source.root` to point each app at its subdirectory:
Point each app at its subdirectory using `source.root` so multiple apps can coexist in one repository.

```yaml
applications:
Expand Down Expand Up @@ -122,13 +126,48 @@ routes:

---

## Composable images (Nix-based)

Use composable images when the standard registry runtimes do not provide required packages. A composable application declares `type: composable:stable` and a `stack` of runtimes and extra Nix packages.

Minimal composable example:

```yaml
applications:
myapp:
type: composable:stable
stack:
runtimes:
- python@3.13
packages:
- ffmpeg
- python313Packages.yq
hooks:
build: |
set -ex
pip install -r requirements.txt
web:
commands:
start: gunicorn app:app
```

Notes:
- Always verify Nix package names with the package search tool before adding them.
- Composable images are for applications only (not services) and cannot be mixed with Registry runtimes in the same application.

---

## Available runtimes, services and versions

Canonical source (always up to date): https://meta.upsun.com/images

Each image entry has a `versions` map with a `status` of `supported`, `deprecated`, `retired`, or `decommissioned`.
Use only `supported` (or `deprecated` if a specific older version is required).
Services have `"service": true`; runtimes have `"service": false`.
Each image entry has a `versions` map with a `status` of `supported`, `deprecated`, `retired`, or `decommissioned`. Use only `supported` (or `deprecated` if a specific older version is required).

---

## Routes

The top-level `routes` section defines public HTTP endpoints. A default route mapping 'https://{default}/' is commonly used when there is a single application.

---

Expand All @@ -140,3 +179,40 @@ Services have `"service": true`; runtimes have `"service": false`.
- For PHP apps, set `build.flavor: none` and use `composer install --no-dev --no-interaction --optimize-autoloader` in the build hook.
- Relationship names become the env var prefix (uppercased). The vars are runtime-only.
- Run `upsun init` to auto-detect your stack and generate a starter config.

---

Per-language and per-framework details

For framework- and language-specific configuration patterns and examples, see the companion reference files below. Each link is a relative path to a file with focused guidance and minimal examples:

- [config/directus.md](config/directus.md)
- [config/django.md](config/django.md)
- [config/drupal.md](config/drupal.md)
- [config/echo.md](config/echo.md)
- [config/express.md](config/express.md)
- [config/flask.md](config/flask.md)
- [config/gatsby.md](config/gatsby.md)
- [config/gin.md](config/gin.md)
- [config/go.md](config/go.md)
- [config/hugo.md](config/hugo.md)
- [config/jekyll.md](config/jekyll.md)
- [config/js.md](config/js.md)
- [config/laravel.md](config/laravel.md)
- [config/nextjs.md](config/nextjs.md)
- [config/nuxt.md](config/nuxt.md)
- [config/php.md](config/php.md)
- [config/python.md](config/python.md)
- [config/rails.md](config/rails.md)
- [config/reactjs.md](config/reactjs.md)
- [config/ruby.md](config/ruby.md)
- [config/sinatra.md](config/sinatra.md)
- [config/static.md](config/static.md)
- [config/strapi.md](config/strapi.md)
- [config/sylius.md](config/sylius.md)
- [config/symfony.md](config/symfony.md)
- [config/vite.md](config/vite.md)
- [config/vuejs.md](config/vuejs.md)
- [config/wordpress.md](config/wordpress.md)

Use the per-framework pages for concrete build hooks, common relationships, recommended routes, and any runtime-specific caveats.
126 changes: 126 additions & 0 deletions plugins/upsun/skills/upsun/references/config/directus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
## Directus Application Configuration

Reference configuration for Directus applications on Upsun.

**Framework Priority**: Use Directus-specific configuration instead of generic Node.js guidance when both apply.

**Minimum Requirements**: Node.js 18+ required for Directus compatibility

**Template Usage**: Adapt database service and authentication configuration to project requirements. Exclude explanatory comments from production configurations.

```yaml
applications:
directus:
type: nodejs:22

build:
flavor: none

hooks:
build: |
set -ex
npm install
npm run argon2-rebuild

# Create .environment file with dynamic variables
cat > .environment <<EOF
CACHE_ENABLED=true
CACHE_STORE=redis
REDIS_HOST=$REDIS_HOST
REDIS_PORT=$REDIS_PORT
RATE_LIMITER_ENABLED=true
RATE_LIMITER_STORE=redis
RATE_LIMITER_REDIS_HOST=$REDIS_HOST
RATE_LIMITER_REDIS_PORT=$REDIS_PORT
KEY=$PLATFORM_PROJECT_ENTROPY
SECRET=$PLATFORM_PROJECT_ENTROPY
Comment on lines +26 to +36
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The build hook writes .environment using an unquoted heredoc, so $REDIS_HOST/$REDIS_PORT will be expanded during the build phase (when relationship variables are not available) and end up empty at runtime. Write these lines in a way that defers expansion to runtime (e.g., quote the heredoc delimiter or escape $), and ensure variables meant for the app process are exported in .environment.

Suggested change
cat > .environment <<EOF
CACHE_ENABLED=true
CACHE_STORE=redis
REDIS_HOST=$REDIS_HOST
REDIS_PORT=$REDIS_PORT
RATE_LIMITER_ENABLED=true
RATE_LIMITER_STORE=redis
RATE_LIMITER_REDIS_HOST=$REDIS_HOST
RATE_LIMITER_REDIS_PORT=$REDIS_PORT
KEY=$PLATFORM_PROJECT_ENTROPY
SECRET=$PLATFORM_PROJECT_ENTROPY
cat > .environment <<'EOF'
export CACHE_ENABLED=true
export CACHE_STORE=redis
export REDIS_HOST=$REDIS_HOST
export REDIS_PORT=$REDIS_PORT
export RATE_LIMITER_ENABLED=true
export RATE_LIMITER_STORE=redis
export RATE_LIMITER_REDIS_HOST=$REDIS_HOST
export RATE_LIMITER_REDIS_PORT=$REDIS_PORT
export KEY=$PLATFORM_PROJECT_ENTROPY
export SECRET=$PLATFORM_PROJECT_ENTROPY

Copilot uses AI. Check for mistakes.
EOF

deploy: |
set -ex
if [ ! -f var/upsun.installed ]; then
echo 'Bootstrapping Directus on first deploy...'

export PROJECT_NAME='Directus on Upsun'
export ADMIN_EMAIL='admin@example.com'
export ADMIN_PASSWORD='password'
Comment on lines +45 to +46
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example bootstraps Directus with hard-coded admin credentials (admin@example.com / password). Even as guidance, this is likely to be copy-pasted into production and creates an immediate account-takeover risk. Prefer reading admin email/password from Upsun variables/secrets (or omit the bootstrap credential values and instruct the user to set them explicitly).

Suggested change
export ADMIN_EMAIL='admin@example.com'
export ADMIN_PASSWORD='password'
: "${ADMIN_EMAIL:?Set ADMIN_EMAIL as an Upsun environment variable or secret before first deploy}"
: "${ADMIN_PASSWORD:?Set ADMIN_PASSWORD as an Upsun environment variable or secret before first deploy}"
export ADMIN_EMAIL ADMIN_PASSWORD

Copilot uses AI. Check for mistakes.

npx directus bootstrap

touch var/upsun.installed
else
npx directus database migrate:latest
fi

web:
commands:
start: npx directus start

variables:
env:
NODE_ENV: production
DB_CLIENT: pg
EXTENSIONS_PATH: ./extensions
UPLOADS_LOCATION: local
UPLOADS_LOCAL_ROOT: ./uploads

mounts:
var:
source: storage
uploads:
source: storage

relationships:
db: {}
redis: {}

services:
db:
type: postgresql:16

redis:
type: redis:7.4

routes:
https://{default}/:
type: upstream
upstream: directus:http
cache:
enabled: true
default_ttl: 0
cookies: ['*']
headers: ['Accept', 'Accept-Language']

https://www.{default}/:
type: redirect
to: https://{default}/
```

## Configuration Details

- Database Service: PostgreSQL 16 service. Service named "db" exposes standard DB_ environment variables.
- Caching: Single Redis service for both application caching and rate limiting. Service named "redis" exposes standard REDIS_ environment variables.
- Environment File: Dynamic variables written to `.environment` file during build hook for proper substitution.
- Authentication: Uses platform project entropy for key/secret generation ensuring unique values per environment.
- File Storage: Local upload storage with persistent mount for file retention.
- First Deploy: Automated bootstrap process creates admin user on initial deployment.
- Database Migration: Automatic schema updates on subsequent deployments.
- Argon2 Rebuild: Required dependency rebuild for password hashing compatibility.
- Upload Directory: Persistent storage mount for file uploads with local storage driver.
- Extensions: Support for custom Directus extensions in dedicated directory.

## Security Notes

- Change default admin credentials immediately after first login.
- Project entropy provides cryptographically secure keys unique to each environment.
- Database credentials managed automatically via service relationships.
- File uploads restricted to designated mount for security isolation.

## Build Process

1. Install Node.js dependencies including Directus core
2. Rebuild Argon2 native bindings for platform compatibility
3. Create `.environment` file with dynamic Redis and authentication variables
4. Bootstrap database and admin user on first deployment
5. Run schema migrations on subsequent deployments
6. Start Directus server with production configuration
93 changes: 93 additions & 0 deletions plugins/upsun/skills/upsun/references/config/django.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
## Django Application Configuration

Reference configuration for Django web applications on Upsun.

Framework Variants: For Django derivatives (nanodjango, microdjango), adapt management commands accordingly.

Template usage: Customize this configuration for project-specific requirements. Do not include explanatory comments in production configurations.

```yaml
applications:
site:
type: python:3.13

dependencies:
python3:
uv: '*'

hooks:
build: |
set -ex
uv sync --frozen --no-dev --no-managed-python

# Auto-detect WSGI module name
export WSGI_NAME=$(basename "$(dirname "$(find . -maxdepth 4 -path './.*' -prune -o -name wsgi.py -print | head -n1)")")
if [ -z "$WSGI_NAME" ]; then
echo >&2 'Failed to find WSGI module name'
exit 1
fi

# Configure Django settings for Upsun
export settings_dir=$(basename "$(dirname "$(find . -Maxdepth 4 -path './.*' -prune -o -name settings.py -print | head -n1)")")
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

find uses -Maxdepth here, but the POSIX/GNU option is -maxdepth (lowercase). As written, settings_dir will be empty and the settings auto-configuration block won’t run.

Suggested change
export settings_dir=$(basename "$(dirname "$(find . -Maxdepth 4 -path './.*' -prune -o -name settings.py -print | head -n1)")")
export settings_dir=$(basename "$(dirname "$(find . -maxdepth 4 -path './.*' -prune -o -name settings.py -print | head -n1)")")

Copilot uses AI. Check for mistakes.
if [ -n "$settings_dir" ]; then
echo >> "$settings_dir"/settings.py "\n# Upsun configuration"
echo >> "$settings_dir"/settings.py 'ALLOWED_HOSTS = ["*"]'
Comment on lines +33 to +34
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The build hook appends ALLOWED_HOSTS = ["*"] to settings.py, which disables host header validation and is unsafe for production. Prefer deriving the allowed host(s) from the primary route at runtime, or setting a narrow default and instructing users to configure it explicitly.

Suggested change
echo >> "$settings_dir"/settings.py "\n# Upsun configuration"
echo >> "$settings_dir"/settings.py 'ALLOWED_HOSTS = ["*"]'
cat >> "$settings_dir"/settings.py <<'EOF'
# Upsun configuration
import base64
import json
import os
def _upsun_allowed_hosts():
routes = os.environ.get("PLATFORM_ROUTES")
if not routes:
return ["localhost", "127.0.0.1", "[::1]"]
try:
decoded = base64.b64decode(routes).decode("utf-8")
route_map = json.loads(decoded)
except Exception:
return ["localhost", "127.0.0.1", "[::1]"]
hosts = []
for url in route_map.keys():
try:
host = url.split("://", 1)[1].split("/", 1)[0]
if host:
hosts.append(host)
except Exception:
continue
return hosts or ["localhost", "127.0.0.1", "[::1]"]
ALLOWED_HOSTS = _upsun_allowed_hosts()
EOF

Copilot uses AI. Check for mistakes.
if ! grep -q STATIC_ROOT "$settings_dir"/settings.py; then
echo >> "$settings_dir"/settings.py 'STATIC_ROOT = BASE_DIR / "static"'
echo >> "$settings_dir"/settings.py 'STATIC_URL = "/static/"'
fi
fi

uv run python manage.py collectstatic --noinput
echo >> .environment "export WSGI_NAME=$WSGI_NAME"
echo >> .environment 'export DATABASE_URL="$DB_SCHEME://$DB_USERNAME:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_PATH"'

deploy: |
set -ex
uv run python manage.py migrate --noinput

web:
commands:
start: uv run gunicorn "$WSGI_NAME".wsgi:application --bind 0.0.0.0:$PORT

locations:
/:
passthru: true
/static:
allow: true
expires: 1h
root: static
/media:
expires: 1h
root: media
scripts: false

variables:
env:
UV_LINK_MODE: copy

mounts:
media:
source: storage
logs:
source: storage

relationships:
db: {}

services:
db:
type: postgresql:16
```

Configuration Details:

- Python Version: Match project requirements from pyproject.toml or requirements.txt; example uses python:3.13
- Package Manager: Adapt for pip, poetry, or pipenv based on project setup
- WSGI Detection: Automatically finds Django project structure
- Static Configuration: Auto-configures STATIC_ROOT and STATIC_URL if missing
- Database Integration: Sets up DATABASE_URL for django-environ or dj-database-url
- File Handling:
- /static: Cached static assets
- /media: User uploads with storage mount
- uv Optimization: UV_LINK_MODE=copy prevents symlink issues in containers
Loading
Loading