Skip to content

Git Deployments

Free: 3 projects. Pro: unlimited. Point DockPod at a repo, and it clones, builds, and deploys on every push.

Creating a Deploy App

Go to Git Deployments → New Deployment and provide:

  • Name — used to derive the compose project name
  • Repository URL — HTTPS or SSH (git@host:org/repo.git)
  • Branch — defaults to master
  • Deploy modeCompose, Dockerfile, Nixpacks, or Railpack (the last two need zero config — see Zero-config Builds below)
  • Environment variables — injected at deploy time via a temp .env file, never committed to your repo

A dedicated Ed25519 SSH deploy key is generated per app the moment it's created. Add it as a read-only Deploy Key on GitHub/GitLab if your repo is private — you don't need to grant write access.

Compose-first, Dockerfile Fallback

DockPod's compose-first design means:

  1. In compose mode, every deploy re-resolves which compose file to actually use by checking the repo root against a fixed priority order — a prod-named file wins whenever it's present, not just when the default is missing: docker-compose.prod.ymldocker-compose.prod.yamldocker-compose.ymldocker-compose.yamlcompose.ymlcompose.yaml. Whichever one is found builds with docker compose up -d --build, and the app's stored config is updated to match if it changed. This is deliberate: a repo commonly carries a dev-oriented docker-compose.yml (local ports, bind mounts, dev env) alongside a prod-oriented variant, and a DockPod deploy is inherently the hosted/prod side — it should never pick up the dev one by accident just because it happens to be the first name in the list.
  2. If none of those exist at all but a Dockerfile does, it silently falls back to Dockerfile mode: docker build → tag app:sha → stop the old container → start the new one
  3. If neither exists, the build fails immediately with a clear error instead of a confusing "no such file" from deep inside docker compose

There's no UI to manually pick a compose file today — this priority order is the only way to target a non-default filename, and it's re-evaluated on every deploy, not just once at creation. Adding a docker-compose.prod.yml to a repo that previously only had docker-compose.yml switches the next deploy over to it automatically.

Compose Best Practices

DockPod does not rewrite or strip anything from your docker-compose.yml — whatever ports:/volumes: you declare are exactly what runs. Two things are worth understanding before pushing a compose file: how services should reach each other, and what makes a service safe to horizontally scale (see Optimize — currently Dockerfile-mode only, but this is the same groundwork either way, including if you scale a compose service yourself with docker compose up --scale).

Skip ports: almost everywhere. A published host port only earns its keep for a non-HTTP protocol Traefik can't route — nothing else needs it, including debugging:

  • The web-facing service doesn't need a host port even to be reachable from the internet — add a Custom Domain instead. Traefik routes to it over the shared internal network using the container's port (its traefik.http.services.*.loadbalancer.server.port label), never the host's.
  • Internal services (a database, Redis, a queue) are reached by every other container in the same compose project via service-name DNSpostgres:5432, redis:6379 — automatically, with zero ports: entries. This is also what avoids the host-port clashes covered in Database Provisioning and the section above: two unrelated projects can both run Postgres on container-port 5432 forever without ever touching each other, as long as neither publishes it to the host.
  • Wanting to poke at a service directly for debugging (psql, redis-cli, a shell) doesn't need a host port either — every container's own Console tab drops you straight into it over the Docker API (docker exec, not a network connection), whether or not anything is published to the host.

What makes a service safe to scale to N replicas:

  1. No host port. Two replicas of the same service can't both bind the same host port — the second one fails outright. Not an issue if you followed the advice above.
  2. No exclusive-lock volume. Postgres/MySQL-style data directories take a lock on startup — a second replica pointed at the same volume won't just risk corruption, it will flatly refuse to start. True horizontal scaling for a stateful database needs actual replication (e.g. Patroni), not a higher replica count.
  3. Session state isn't local. If your app keeps user sessions in memory or in a local file, a request load-balanced to a different replica won't recognize an existing session — users get randomly logged out. Externalize sessions to Redis or your database, or go stateless (JWT with no server-side session store).
  4. A shared volume for independent files (e.g. uploads) is fine, with a caveat. Docker named volumes support concurrent mounts from multiple containers just fine — unlike a database's data directory, there's no built-in exclusive lock. A replica that writes an upload under a unique filename (a UUID, typically) and never touches it again is safe: every other replica reading that same volume sees the file immediately. It stops being safe the moment multiple replicas might write the same filename concurrently (a shared config.json, a counter file) — that's a plain race condition, and whether your app avoids it isn't something DockPod can see from the compose file alone. This is exactly why volume presence alone isn't a reliable "unsafe to scale" signal — it depends on how the volume is actually used.

Adapting an Existing Prod Compose File

A compose file written for hand-rolled deployment (its own docker compose -f docker-compose.prod.yml --env-file .env.prod up -d on a bare VPS) commonly hits three specific DockPod incompatibilities, all confirmed live against DockPod's actual generated hardening override — not just theoretical:

  • deploy.resources.limits conflicts with DockPod's own resource limits. DockPod's compose override always sets the legacy mem_limit/cpus/pids_limit keys on every service. Compose V2 hard-rejects a merge that has both styles present on the same service, even if you make the values match: services.api: can't set distinct values on 'mem_limit' and 'deploy.resources.limits.memory': invalid compose project. Drop deploy.resources.limits entirely — every service in a DockPod compose project shares one memory/CPU/pids ceiling, set via Optimize's Resources, not a distinct value per service.
  • An env_file: pointing at a gitignored path always fails. A pattern like env_file: apps/api/.env (deliberately never committed, filled in by hand on the deploy host) can never exist in DockPod's checkout — every deploy is a fresh git clone --depth 1, and a missing env_file path is an immediate hard error, not a skip. Delete the env_file: entry and move those variables into the app's own Environment Variables box (create/edit dialog) instead — DockPod injects that into every service the same way (see Compose Best Practices above), and any ${VAR} interpolation elsewhere in the compose file resolves from the same source.
  • A service publishing 80/443 collides with DockPod's own Traefik, which already binds those ports on the host for every app's Custom Domain routing (see Skip ports: almost everywhere above). This is common when a compose file terminates its own TLS (e.g. a Caddy edge container doing automatic HTTPS) — drop the ports: publish, add a DockPod Custom Domain pointing at that service's container-internal HTTP port instead, and change the edge container's own config to plain HTTP (Caddy: an explicit :80 { ... } site address instead of {$DOMAIN} { ... }'s automatic-HTTPS form) — Traefik becomes the actual public TLS terminator, and everything else the edge container does (reverse-proxying to other services, serving static files, security headers) is unaffected.

Zero-config Builds (Nixpacks / Railpack)

Nixpacks and Railpack mode build straight from your repo with no Dockerfile or compose file required — both tools auto-detect the language/framework (Node, Python, Go, Ruby, etc.) and generate the build for you. They show as not installed and stay disabled in the deploy-mode picker until the nixpacks/railpack CLI is present on the host — Installation installs both automatically on recognized distros, or install manually from nixpacks.com/railpack.com.

DockPod injects PORT=3000 into the app's env vars if you haven't set one yourself — both tools follow the Railway convention of listening on $PORT, and 3000 matches what their own generated builds default to internally (confirmed via a real generated Caddyfile: :{$PORT:3000}). If you add a custom domain, its port field also defaults to 3000 for these two deploy types so both sides line up without you having to know this detail. Setting PORT explicitly in the app's env vars always overrides the default.

A docker-compose.yml in the repo is ignored under Nixpacks/Railpack — the CLI only builds the app itself from source, the same way it would with no compose file present at all. If your repo's compose file also defines a database or Redis service (a common local-dev setup), those services are simply never deployed — only the app. Point the app at a provisioned database instead: see Database Provisioning and Linking a Database below, then update the app's own connection config/env vars to match (a hardcoded hostname like database or redis from your dev compose file won't resolve here — nothing by that name exists in this app's network).

Switching the deploy mode to Nixpacks or Railpack also pre-fills a NIXPACKS_NODE_VERSION=22 / RAILPACK_NODE_VERSION=22 line into the (editable) env vars box — a visible default rather than a silent one, since it's meant as a starting point you're expected to change to whatever version your app actually needs. Switching between the two builders swaps which var is set instead of leaving both piled up; switching to Compose/Dockerfile removes it entirely.

Webhooks

Every deploy app gets a webhook URL (/api/webhooks/<app_id>) and a per-app secret shown on its detail page. Configure it on your provider:

  • GitHub — Settings → Webhooks → Add webhook. Content type application/json, secret = the value shown, select "Just the push event." DockPod verifies the X-Hub-Signature-256 HMAC-SHA256 header.
  • GitLab — Settings → Webhooks. GitLab uses a plain token comparison (X-Gitlab-Token header) rather than HMAC.

Live Build Logs & History

Every build streams its output live over WebSocket to the deploy app's detail page, and the button disables itself with a spinner while a build is in progress — no more wondering if your click registered. Full logs are stored per build; Free tier keeps the last 5 builds per app, Pro keeps unlimited history.

Rollback (Pro)

Roll back to the previous successful build — for compose deploys, this restores the previous compose file + env snapshot and re-applies it; for Dockerfile deploys, it swaps back to the previous tagged image (DockPod keeps the last 3 tagged images per app).

Optimize (Pro)

The Optimize dropdown on an app's detail page holds two resource-tuning actions:

  • Scale — set the replica count for image-based (Dockerfile) apps. Not available for compose apps, since compose already defines its own service topology.
  • Resources — set the app's memory (128–16384MB), CPU (0.1–16 vCPU), and process (64–8192 pids) limits, image-based or compose. If the app is running, the new limits apply instantly via the Docker API to every container in the project — no rebuild, no restart. If it isn't running, the limits are saved and apply on the next deploy. For a compose app, one shared value applies to every service in the project — there's no way to give one service a higher CPU/memory ceiling than another (see Adapting an Existing Prod Compose File above for what this means in practice).

Unlike Container Detail's own Optimize, this one is persisted — it survives redeploys, restarts, and container recreation, since it's stored on the app record itself rather than being a one-off live adjustment. Both cover memory/CPU/pids the same way now; the difference is persistence, not scope.

Custom Domains

Add a domain on an app's detail page (or via the add_domain MCP tool) to point it at a real hostname. DockPod manages a single shared Traefik container (dockpod-traefik) that handles the actual routing and certificate issuance — you never touch Traefik config directly. It runs with a fixed 256MB/1 vCPU/512-pids baseline — generous headroom for typical self-hosted traffic, not a tunable per-app override the way deployed apps get (see Optimize), since it's infrastructure DockPod manages rather than something you deploy.

DockPod picks one of three modes per domain, mostly automatically:

  • Real domain (anything else) — Traefik requests a certificate from Let's Encrypt automatically via HTTP-01 challenge. Needs an ACME email set once under Settings → General → Reverse Proxy, and the domain's DNS actually pointing at this host (port 80 reachable) for the challenge to succeed.
  • Local test domain (.test, .local, .localhost, .example, .invalid — auto-detected, no toggle needed) — these are IANA-reserved and can never get a real certificate, so DockPod issues one itself from a local certificate authority it generates on first use (the same technique mkcert uses). No ACME email needed. Point the domain at 127.0.0.1 (or the host's IP) via /etc/hosts and it works immediately — the browser will show an untrusted-certificate warning until you trust DockPod's local CA once (Settings → General → Reverse Proxy → Local Development Domains: "Trust automatically" attempts this for you, or download the certificate to install manually). This only matters if you're browsing from the same machine DockPod runs on — that's the whole point of a local-only domain.
  • Plain HTTP (explicit skip_tls checkbox/param) — no certificate at all, local or otherwise. An escape hatch for the rare case you don't want HTTPS even for a local domain.

Local-CA trust is per-machine: security add-trusted-cert on macOS, update-ca-certificates on Linux (Firefox uses its own certificate store on Linux and needs a separate certutil step — a known limitation shared with mkcert). On a shared/remote server accessed by a team, each person's machine needs to trust the CA individually; for a solo local dev setup (DockPod and browser on the same machine) it's a one-time, zero-friction step.

Every deployed app gets its own dedicated, isolated network — it can't reach any other app's containers directly, only what Traefik routes to it or a database you explicitly link (below).

Linking a Database

Free: 3 linked databases per app. Pro: unlimited. Same numeric limit as Database Provisioning itself — see Pricing.

Database Provisioning gives you a Postgres/MySQL/Redis/MongoDB container, but the connection string shown on the Databases page (127.0.0.1:<port>) only resolves from the DockPod host itself — not from inside another container, which is what your deployed app actually runs in. Linking a database on an app's detail page (or via the link_database_to_app MCP tool) fixes that: DockPod connects the database's container to the app's dedicated network and injects env vars using the database's container name as the host, so it's actually reachable.

For a link with prefix DATABASE (the default — matches the DATABASE_URL convention most app code already expects; only needs changing if you're linking a second database to the same app, since prefixes can't repeat), you get:

  • DATABASE_HOST — the database's container name
  • DATABASE_PORT — its container-internal port (not the host-bound one)
  • DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD
  • DATABASE_URL — a ready-to-use connection string (postgres://..., mysql://..., etc.)

Linking (and unlinking) triggers a redeploy so the running container actually picks up the env var changes — same as adding or removing a custom domain. Only available for Git Deployment apps, not Paste & Deploy.

Released under a commercial-friendly freemium license.