# Native Linux operation No service was installed on the development laptop. `contentd.service` is a deployment example for a dedicated host with systemd and an existing PostgreSQL server. The HTTP listener defaults to loopback. Put a TLS-terminating reverse proxy in front; restrict network access there. Authentication remains out of scope, so do not mistake operational hardening for permission to expose writes publicly. ## Release procedure 1. Build with `bin/build` on the target architecture. Copy `go-service/contentd` into a versioned directory under `/opt/contentd/releases/`. Keep the prior binary. 2. Create an unprivileged OS account named `contentd`. Create a PostgreSQL database and a separate migration-owner role. Back up existing data before schema changes. 3. Run the binary's explicit `migrate` command with the owner's `DATABASE_URL`. It uses a transaction, advisory lock, migration version and SHA-256 checksum. Repeating it is safe. It refuses existing untracked blog tables; this is not an automatic live-Django cutover procedure. Serving never runs migrations. 4. Grant the runtime role CONNECT, schema USAGE, SELECT/INSERT/UPDATE on the blog tables, sequence USAGE/SELECT, and SELECT on `content_schema_migrations`. It needs no CREATE, DROP, DELETE, or superuser rights. Configure local peer authentication or a protected password file. For a remote database use verified TLS. 5. Install the example unit and a root-managed `/etc/contentd/environment`, mode 0600, populated from `environment.example`. Point `current` to the new release. Adjust hosts and paths deliberately. Start the service and check `/readyz`. The harness's `assignment_harness_*` databases and ownership marker are only for tests. Do not use the harness to initialize, seed, or benchmark a real database. The production migrator can initialize a normal database without that marker. Set `DATABASE_URL` explicitly for both migration and serving on a production host; when unset or empty, both commands use `dbname=assignment_harness_native`. ## Profile differences | Setting | Compatibility | Production, default | | --- | --- | --- | | Legacy framework errors | Captured plaintext Python errors for observed cases | Generic JSON 500, no traceback | | Required configuration | None; database defaults to assignment_harness_native | Exact ALLOWED_HOSTS; same database default | | Schema check at startup | Existing compatible tables | Version 1 and matching checksum required | | Health endpoints | Absent | GET /healthz and /readyz | | Request body limit | Reference behavior | 1 MiB, 413 on overflow including chunked input | | Request admission | Unbounded handler admission | 64 active application handlers, excess gets 503 | | SQL connections | 8 by default | 8 by default, configurable 1–128 | | HTTP deadlines | Reference comparison profile | Headers 5s, read/query context 15s, write 30s, idle 60s | | Header size | Go default | 32 KiB nominal limit plus Go parser allowance | | Logs | Startup/shutdown, optional SQL diagnostics | JSON request ID, method, status and elapsed time | | Proxy headers | Unused | Untrusted and ignored; Host checked directly | Both profiles preserve partial writes, draft visibility, the non-atomic counter, response shape and ordering rules. Production errors and operational limits are explicit differences, not a claim that the production profile passes unchanged debug-output golden files. Request deadlines can cancel database operations but do not undo already committed writes. The saved Python errors in compatibility mode are emulation assets, not genuine Go stack traces. `DB_MAX_CONNS` and `MAX_INFLIGHT` have validated bounds. Invalid profiles, absent required settings, unreachable databases and incompatible schema fail startup. `SQL_TRACE` is rejected in production. Logs omit bodies, SQL bind values and query strings. Incoming request IDs are replaced rather than trusted. ## Readiness, shutdown and rollback Liveness checks the HTTP process. Readiness pings PostgreSQL with a one-second deadline; it is not a continuous schema-drift check or dependency-wide health audit. Use an allowed Host for probes. The app stops accepting connections on SIGTERM and waits up to ten seconds for handlers before forcing closure. systemd permits fifteen seconds. Black-box tests hold a row lock, send a real request and SIGTERM, release the lock, then verify a successful response and zero-exit shutdown. For this first schema version, rollback means stop, repoint `current` at the prior compatible binary, start, and verify readiness. Future schema changes need explicit backward-compatibility and restore procedures. No automatic destructive down migration or database deletion is provided. The unit limits privileges, writable paths, process count, descriptors and memory. Its 1 GiB cap is an example, not a measured production capacity recommendation. Unpaginated results can still consume substantial memory. Verify backup restoration, TLS/proxy settings, logging retention, capacity and monitoring on the actual host before exposing the application.