Skip to content

Deployment Reference

How it deploys

Every push to master triggers the Gitea Actions pipeline (.gitea/workflows/deploy.yml):

  1. Builds the Docker image (Dockerfile — multi-stage: Node 24 frontend build + .NET 10 backend build + runtime)
  2. Pushes to gitea.apimeld.com/meld/apimeld-scheduler:latest
  3. Deploys via Portainer API using .gitea/portainer-stack.yml as the stack definition
  4. Cleans up stale SHA-tagged images from the Gitea registry

The running stack definition lives in .gitea/portainer-stack.yml. Edit that file and push to change any runtime environment variable.


Environment variables

Database (required)

VariableDescription
SETUP_DB_HOSTPostgreSQL hostname or IP
SETUP_DB_PORTPostgreSQL port (default: 5432)
SETUP_DB_NAMEDatabase name
SETUP_DB_USERNAMEDatabase user
SETUP_DB_PASSWORDDatabase password

First-run admin account (required for automated setup)

VariableDescription
SETUP_ADMIN_EMAILAdmin account email
SETUP_ADMIN_PASSWORDAdmin account password
SETUP_ADMIN_DISPLAY_NAMEDisplay name (defaults to email if omitted)

If these are present on first startup, the setup wizard is skipped and the app configures itself automatically.

Application

VariableExampleDescription
ASPNETCORE_ENVIRONMENTProductionSet to Development to enable Swagger UI and detailed errors
App__BaseUrlhttps://poc.apimeld.comPublic-facing URL — used in email links and CORS
CORS__AdditionalOriginshttp://localhost:6274Comma-separated extra origins allowed by CORS (e.g. MCP Inspector)

Networking — choose one mode

Mode 1: Reverse proxy (NginxPM, Traefik, Cloudflare Tunnel, etc.) — current production setup

The container serves plain HTTP on port 8080. The proxy handles TLS termination and forwards X-Forwarded-For / X-Forwarded-Proto.

VariableValueDescription
PROXY_ENABLEDtrueTrust X-Forwarded-* headers from the upstream proxy

NginxPM config: point proxy host to http://<docker-host-ip>:8080. Enable "Force SSL" and "HTTP/2 Support" in NPM. Set HSTS on the NPM proxy host (not on the container).

yaml
# portainer-stack.yml snippet
ports:
  - "8080:8080"
environment:
  - PROXY_ENABLED=true

Mode 2: Direct TLS (cert mounted into container — no proxy)

The container terminates TLS itself on port 8443. Mount a directory containing your certificate.

VariableExampleDescription
TLS_ENABLEDtrueEnable Kestrel HTTPS
TLS_CERT_PATH/certs/cert.pemPath to certificate file inside the container
TLS_KEY_PATH/certs/key.pemPath to private key (PEM only — omit for PFX)
TLS_CERT_PASSWORDsecretPFX password (optional, omit if unprotected)
TLS_PORT8443HTTPS port (default: 8443)

Do not set PROXY_ENABLED=true at the same time — these modes are mutually exclusive. HSTS is set automatically by the app in this mode.

PEM example:

yaml
ports:
  - "8443:8443"
volumes:
  - /host/path/to/certs:/certs
environment:
  - TLS_ENABLED=true
  - TLS_CERT_PATH=/certs/cert.pem
  - TLS_KEY_PATH=/certs/key.pem

PFX example:

yaml
ports:
  - "8443:8443"
volumes:
  - /host/path/to/certs:/certs
environment:
  - TLS_ENABLED=true
  - TLS_CERT_PATH=/certs/cert.pfx
  - TLS_CERT_PASSWORD=your-pfx-password

Let's Encrypt with Certbot (PEM): Certbot writes to /etc/letsencrypt/live/<domain>/. Mount that directory:

-v /etc/letsencrypt/live/yourdomain.com:/certs:ro
-e TLS_CERT_PATH=/certs/fullchain.pem
-e TLS_KEY_PATH=/certs/privkey.pem

Note: Certbot rotates certs — set up a renewal hook that restarts the container after renewal.


Security headers

The following headers are set on every response by the application:

HeaderValuePurpose
X-Content-Type-OptionsnosniffPrevent MIME-type sniffing
X-Frame-OptionsDENYBlock clickjacking via iframes
X-XSS-Protection0Disable legacy browser XSS auditor (CSP replaces it)
Referrer-Policystrict-origin-when-cross-originLimit referrer leakage
Permissions-Policycamera=(), microphone=(), geolocation=(), payment=()Opt out of unused browser APIs
Strict-Transport-Securitymax-age=31536000; includeSubDomainsDirect TLS mode only

HSTS in proxy mode: Enable HSTS on the NginxPM proxy host instead (Advanced tab → Custom Nginx config, or the HSTS toggle). Do not set it on the container — Kestrel serves plain HTTP on the internal leg.

CSP (Content-Security-Policy): Not yet configured. Monaco Editor requires unsafe-eval for its web workers; this needs per-route tuning before enforcement.


Volumes

Mount pathPurpose
/app/configRuntime config overrides (appsettings.Production.json written by setup wizard). Persist this across container restarts.
/certs(Direct TLS mode only) Certificate directory. Read-only mount recommended.

Ports

PortProtocolWhen active
8080HTTPAlways
8443HTTPSOnly when TLS_ENABLED=true