Deploying
The container, migrations, and checking config before a deploy.
The binary is static and the image is distroless, so gostore runs anywhere a
container does — a managed platform or a VM behind a reverse proxy. It reads
PORT from the environment, takes a single DATABASE_URL, logs JSON to stdout,
and answers GET /healthz.
Migrations
Migrations run on boot, before the server accepts traffic, behind a Postgres advisory lock so several instances starting at once cannot race.
To migrate as a deploy step of its own, run the same image with -migrate
first. It needs only DATABASE_URL: a schema change has no reason to be trusted
with the live merchant key.
gostore -check-config && gostore -migrate && exec gostore-check-config validates the whole environment, touches nothing and exits, so a
missing payment key fails the deploy while the database is still untouched.
-migrate-status prints which migrations have been applied.
Migrations only go forward in production. Rolling a schema back over live orders loses money, not just columns.
Before it takes real money
- Set
BASE_URLto the store'shttps://origin. - Set
PAYFAST_SANDBOXexplicitly, and use your own merchant credentials. See Payments. - Set
CLIENT_IP_SOURCEto match whatever is in front of the server. See Configuration. - Use object storage for images if more than one instance runs.
- Leave
THEME_RELOADoff.
Terraform
infra/terraform has a production stack on a Vultr
instance with Docker Compose, and a staging VM on Proxmox, built on a shared
cloud-init module.
Images
Images are built on a workstation and pushed to GHCR by hand with
make publish, usually from a tagged release. It refuses to publish from a
dirty tree or an untagged commit.
Logs
JSON to stdout and nothing else — which docker compose logs, make logs and
Cloud Logging all read with no setup. Every response carries a request id,
echoed as X-Request-Id, printed on error pages and attached to every log line
for that request. LOG_FORMAT=gcp renames the fields so Google Cloud's severity
filters work.