Managed databases have always published their port on 127.0.0.1 only.
public_port opts into binding on all interfaces so the database is
reachable off-host. Toggling via PUT recreates the container — the
named data volume survives.
- create accepts public_port; PUT /databases/{id} toggles it
- external rows reject the flag (no container exists)
- CLI: databases create --public, databases update --public/--public=false
- MCP: databases_update tool; UI: create checkbox + per-row toggle
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Databases hosted outside Containr can now be registered alongside
managed ones. The connection is probed before storing (protocol-level
for postgres/redis-family via real drivers, TCP dial for the rest),
credentials are encrypted at rest and never returned, and external rows
skip managed lifecycle actions, backups, and Docker metrics.
- POST /databases/register-external — probe + create
- POST /databases/test-connection — ad-hoc probe (ok/latency_ms/error)
- POST /databases/{id}/test-connection — re-probe stored credentials
- provider/external fields on database services; privileged reads get a
composed connection URL with loopback hosts rewritten to the caller's
- CLI: databases register + test-connection; MCP: 3 new tools
- UI: managed/external toggle in the create modal, external badge and
host display, per-row test button, backups section hidden
- Migration widens the plan CHECK to admit 'external'
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Service lifecycle: POST /services/:id/clone duplicates config, volumes,
domains, access gates, builder settings, and variables (secret ciphertext
copied verbatim); POST /services/:id/move re-parents to another owned
project. Both enforce ownership, reject name conflicts, and audit.
Registries: CRUD at /registries — host-matched pull credentials for
prebuilt-image deploys, encrypted at rest via secrets pkg, never returned
(has_password flag). Empty password on update keeps the stored credential;
host normalized (scheme/case/trailing slash). Credentials thread through
BuildConfig → BuildRequest → pull auth config, used only for the pull.
Surfaces: CLI services clone|move + registries group, 7 MCP tools,
OpenAPI paths/schemas, frontend Lifecycle card (clone/move/delete) and
Settings registries section, llms.txt + CLI.md.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
services.builder picks the build strategy per service: auto detects from
repo contents (Dockerfile → railpack → nixpacks), explicit overrides force
a builder, and static builds assets in a node stage then serves them from
an nginx image (static_build_cmd/static_dir, port 80 default).
- cpu_reserve/memory_reserve → Docker CPU shares + MemoryReservation;
cpu/memory stay hard caps (NanoCPUs/Memory). Empty string clears.
- app_settings.default_cpu/default_memory override service-create
fallbacks; capacity_policy=block fails deploys whose requested memory
exceeds node capacity (default warn logs via slog).
- Resource specs are validated — "512mb" fails instead of silently
dropping the limit.
- Fixes: deployments.go hardcoded nixpacks (auto-detect was dead code);
DockerfileBuilder passed the Dockerfile *path* as build-context content;
BuildImage never drained the daemon stream so builds could abort and
errors went unseen — stream is now consumed and errorDetail surfaced.
- CLI: --builder/--cpu-reserve/--memory-reserve/--static-cmd/--static-dir
on create+update; MCP args on services create/update; UI Build &
Resources card; OpenAPI + llms.txt + CLI.md.
Verified live: static build of a real repo → nginx image → deployed;
reserves reach HostConfig (shares=256, MemoryReservation=128Mi);
validation rejects bad specs; builder persists + clears.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
service_domains table holds the full hostname set; services.domain stays
the derived default and is synced on every domain write. Runtime spec
carries all domains into a multi-Host Traefik rule plus access gates.
- GET|POST /services/:id/domains, DELETE + POST /:domain_id/default
- GET /services/:id/domains/check — DNS preflight vs public_ip/base_url
(ok | wrong-target | pending), stamps last_checked_at
- maintenance_mode → Traefik redirectregex to /api/v1/maintenance
(base_url/BASE_URL setting; falls back to noop@internal offline)
- basic_auth [{username,password}] → bcrypt htpasswd on the label;
responses return usernames only
- First domain auto-defaults; domain="" via PUT clears all domains
- CLI: services domains list|add|remove|default|check, update
--maintenance/--basic-auth; MCP: 5 domain tools + update args
- Settings UI: Domains card (CRUD + DNS check badges) + Access card
- Fixes latent C1 bug: PUT without volumes 500'd — nil []byte sent as
bytea, not NULL; COALESCE needs untyped nil + ::jsonb cast
Verified live: domain CRUD + default sync, pending/wrong-target checks,
bcrypt htpasswd and both middlewares on the deployed container's labels.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
services.volumes JSONB [{type: volume|bind, source, target, read_only}]
mapped into every replica's container config at deploy and reconcile
time, hashed into the runtime spec so mount changes trigger
reconciliation. Mounted via existing docker.HostConfig.Mounts.
- RuntimeSpec.Volumes + specHash + mount.Mount mapping in createReplica
- Create/update service accepts volumes; PUT replaces the whole list
- Replica-count reconcile now goes through the deploy queue — the last
raw `go` reconcile left that could race a build
- GET /admin/volumes inventories node volumes with in_use flags;
DELETE /admin/volumes/:name removes unused ones (409 on in-use)
- CLI: services update --volume/--bind/--clear-volumes, volumes
list|delete; MCP: services_update + volumes_list/delete
- Settings UI: Volumes section with mount editor
- Migration normalizes the nullable baseline column to NOT NULL [].
Verified live: deploy applies mounts to the container, docker
auto-creates named volumes, in-use deletion is refused.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- internal/deployqueue: serialized FIFO per service — concurrent deploys
of one service no longer race (the container-name-conflict failure
mode); different services still run in parallel.
- Deploys and rollbacks enqueue through the queue; a busy service
returns status 'queued' and promotes when the active job finishes.
- POST /deployments/:id/cancel — queued jobs drop immediately
(200 cancelled); active jobs get ctx cancelled so build/reconcile
abort (202 cancelling, settles 'cancelled'); finished deployments
return 409 NOT_CANCELLABLE.
- Redeploy and variables save now route through the same queue so they
can't overlap a build; PUT /services/:id/variables accepts
redeploy:true to apply env changes to running containers.
- no_cache on deployment create plumbs BuildRequest -> BuildOptions for
nixpacks, dockerfile and railpack builders.
- CLI: deployments cancel, deploy --no-cache, variables set/unset
--redeploy. MCP: containr_deployments_cancel; deploy/variables_set
document no_cache/redeploy. UI: Cancel button + queued/cancelled
badges on the deployments table. OpenAPI + types regenerated.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
After tests + image publish, a release job bumps the tag via
conventional commits (github-tag-action, default_bump=false so
docs/chore merges skip releasing), retags :main images in GHCR as
semver+latest (tag pushes via GITHUB_TOKEN do not retrigger CI),
cross-compiles the CLI for 5 platforms, and cuts a GitHub Release
with generated notes and checksums. Manual v* tag pushes get the
same release treatment. CLI --version now reports the tag via the
previously dead -X ldflags.
Authored By: TDvorak <[email protected]>
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>