Deployment on Coolify and Hostinger

The production stack contains web, API, worker, migration job and PostgreSQL. Only web is connected to the public reverse proxy. The existing stack and shared Coolify proxy are preserved.

Configuration

Generate new production values for POSTGRES_PASSWORD, MAGNUM_APP_PASSWORD, ENCRYPTION_KEY (32 random bytes encoded as 64 hex characters), API_KEY_PEPPER and BETTER_AUTH_SECRET. Store them in a private environment file outside release archives. The database owner credentials are passed only to PostgreSQL and the migration job. Application processes receive the restricted runtime connection.

Set APP_URL=https://magnum.lat. Configure Resend with RESEND_API_KEY and an authorized RESEND_FROM, or retain SMTP with SMTP_URL (smtp:// or smtps://) and MAIL_FROM. If RESEND_FROM is empty, Resend uses MAIL_FROM. SMTP in production requires TLS. Keep AUTH_ALLOW_SIGNUP=false until a real authorized delivery is verified, then enable it explicitly. Set operator-approved PUBLIC_CONTACT_EMAIL, LEGAL_OPERATOR_NAME, LEGAL_OPERATOR_DOCUMENT and LEGAL_ADDRESS; do not invent legal identifiers.

Set FOUR_P_API_KEY only in the private API and worker environment, managed by the Magnum operator. This single provider credential is never configured by a business or exposed to the browser. Businesses configure fixed network/asset receiving addresses in authenticated settings. The live-payment gate remains disabled regardless of credentials or saved addresses, pending explicit approval of the commercial model by 4P.

Existing Hostinger proxy

Use both compose.yaml and compose.hostinger.yaml, with the existing external Docker network coolify. The second file binds a diagnostic endpoint to 127.0.0.1:13100 only and adds narrowly scoped Traefik routes for magnum.lat. It does not take over ports 80 or 443 or restart the shared proxy.

Set TRUST_PROXY_HOPS=1 only for this topology: Traefik sanitizes forwarded client headers, the internal web proxy forwards that chain, and the API accepts one trusted hop. Do not expose the web port publicly or make arbitrary forwarded headers trusted.

Keep TRAEFIK_ENABLED=false until the domain resolves to this VPS and the candidate passes local smoke tests. Change only the intended website A/AAAA records; preserve mail MX/TXT entries. Enable the route after DNS confirmation, then verify the Let's Encrypt certificate and HTTPS response. A configured route is not proof of public deployment.

docker compose --env-file /opt/magnum/shared/.env \
  -f compose.yaml -f compose.hostinger.yaml config --quiet
docker compose --env-file /opt/magnum/shared/.env \
  -f compose.yaml -f compose.hostinger.yaml build
docker compose --env-file /opt/magnum/shared/.env \
  -f compose.yaml -f compose.hostinger.yaml up -d

Do not print docker compose config without --quiet: it includes resolved secrets.

Automated deployment from main

.github/workflows/ci.yml verifies pull requests and main pushes with the locked dependency install, local PostgreSQL migrations, typecheck, tests, production build and formatting checks. Only a successful verification on main can reach the deploy job. A manual workflow dispatch on main uses the same gate; pull requests never deploy. Deployment jobs are serialized without cancelling an in-progress release.

Configure these GitHub Actions repository secrets:

SecretPurpose
MAGNUM_DEPLOY_KEYPrivate key authorized only for the server's forced deployment command
MAGNUM_DEPLOY_HOSTAuthorized deployment host
MAGNUM_DEPLOY_KNOWN_HOSTSVerified SSH host-key entry for that host

The workflow uses strict SSH host-key verification and sends only deploy <40-character commit SHA>. The forced command installed at /opt/magnum/shared/deploy-main.sh rejects every other command, acquires a deployment lock, fetches origin/main with a repository-specific read-only key, and verifies that its current head still matches the requested SHA. A superseded or already deployed revision is skipped.

The server creates an immutable source release at /opt/magnum/releases/main-<first-12-SHA-characters>, records REVISION, builds separate api, worker, migrate and web image tags using the full SHA, and writes their exact references into compose.images.yaml. The private database/production configuration remains outside Git at /opt/magnum/shared/.env.

Before migration, the deployment invokes /opt/magnum/shared/backup.sh. It then runs the release's migration image, starts only the application services from pinned images, waits for container health and checks the public HTTPS root. Only after these checks pass does it update /opt/magnum/current and /opt/magnum/shared/deployed-commit.

An application startup or HTTPS health failure triggers restoration of the previous release's pinned api, worker and web images. The rollback does not downgrade the database or automatically restore a dump. Migration failure stops the deployment before replacing the application services. Migrations must therefore remain compatible with the previous application version.

Release layout

Keep immutable source releases under /opt/magnum/releases/<release-id>, private configuration under /opt/magnum/shared, and a current symlink to the selected release. The persistent Compose project name is magnum; changing the source directory must not create a new database volume.

Before updating, create and verify a PostgreSQL custom-format dump, an archive of affected configuration, and a release manifest with checksums. Retain the backup and image references for the previous healthy release, including the shared proxy configuration.

Verification and rollback

Check web routes in both languages, /v1/capabilities, /readyz from the API container, worker process health, restricted database role, privacy headers and the checkout's exact frame policy. Verify a real Resend or SMTP delivery separately with the approved sender before opening signup. Provider acceptance is not proof of inbox delivery. Restart application containers and confirm persisted configuration survives.

Each deploy must retain a compose.images.yaml that pins the web, API, worker and migration service images to the exact release tags or immutable digests. Keep those images locally or in an authorized registry. Selecting an older source directory alone is insufficient: the base Compose file uses build targets and can otherwise reuse the wrong image.

For rollback, enter the verified previous release and use its image manifest with the same private environment file and existing project name/volumes:

test -f compose.images.yaml
docker compose --env-file /opt/magnum/shared/.env \
  -f compose.yaml -f compose.hostinger.yaml -f compose.images.yaml config --quiet
docker compose --project-name magnum --env-file /opt/magnum/shared/.env \
  -f compose.yaml -f compose.hostinger.yaml -f compose.images.yaml \
  up -d --no-build --no-deps --wait api worker web

This manual application rollback deliberately excludes PostgreSQL and the migration service. Check pinned images are available before changing the running service, then repeat route, database and worker checks. Update the current symlink and deployed-revision record only after the restored release passes checks. Use a new verified main revision for a forward fix; the deploy script intentionally rejects an old SHA that is no longer origin/main. If no image manifest exists, rebuild the verified previous source explicitly before restarting; do not assume an old directory identifies an old binary. Check migration compatibility first. Never run a destructive schema rollback without a verified backup and explicit review.

For the initial release, docker compose down using only the Magnum Compose files removes its containers and routes. Do not add --volumes. The shared Coolify proxy and all other sites must remain running.

Build DNS

On hosts with unreachable resolvers in BuildKit, the Dockerfile accepts an optional build_resolv secret mounted only at build time. It is not copied to the image and does not modify host DNS. Use only resolvers approved for the host.

Magnum integration documentationAPI v1

Search documentation