SpipCP
Sites

Docker site type

A site type that runs a containerized app from a compose file inside an instance โ€” each app with its own directory, port, and route โ€” served and managed like any other site.

The docker (compose) site type runs a containerized app from a docker-compose.yml inside an instance, served through the same domain โ†’ route โ†’ port โ†’ SSL chain as every other site. It's a peer of WordPress / PHP / static / Node sites, and it's per-app: an instance hosts many Docker apps, each with its own compose directory, published port, compose project, and route.

Many Docker apps per instance

A Docker app is a site. An instance hosts many of them, each with its own compose directory (/srv/docker/<app>), its own published port (not a shared 8080), its own compose project, and its own route. Two Docker apps in one instance sit side by side and never collide โ€” just like two Node apps. Once running, see and control them all from the Docker management plane.

What it is

The Docker type works like every other site type โ€” same wizard, same validation, same launch flow. The only difference is its runtime: a container, rather than software installed on the host. A Docker app is created from the new-site wizard like any other type, and it gets a served port, a domain, and SSL the same way.

How it works

Launching a Docker site runs these steps inside the instance:

  1. Install docker โ€” only if it's not already there, then start it. Docker isn't a catalog service; it's installed as part of the launch (the same way the Node type installs its runtime).
  2. Write the per-app compose โ€” a docker-compose.yml plus a starter webroot under the app's own directory /srv/docker/<app> (where <app> is the site's slug). Never a shared directory.
  3. Bring the stack up โ€” run docker compose up -d under the app's own compose project, so two Docker apps in one instance never share state and each container is clearly attributed to its site. The site's environment plus its served port fill in the compose file's ${VAR} values.
  4. Wait and smoke-test โ€” poll the published port until it answers, then check it. A failed check leaves the site failed with a remedy, never green โ€” the same rule as every type.

How the port reaches the web

The compose service publishes its container port on the instance at the site's served port โ€” allocated the same way a Node/PHP/static site gets one. Once a domain is attached, the node's reverse proxy (Caddy) routes the hostname to that port, exactly as it does for a Node app. Two Docker apps get two ports and two routes; nothing about routing is docker-specific.

Editing the compose file (the Compose tab)

A Docker site gets a Compose tab (right after Environment) that edits its docker-compose.yml in place โ€” the same way the Environment editor works: the raw file is the source of truth, a table flips on top, and a two-step Save โ†’ Apply stages then relaunches.

  • Raw is the source of truth. The whole docker-compose.yml โ€” its # comments, blank lines, and ordering โ€” is the editable text, and it's saved and kept (it comes back on reload). Paste a whole file in the YAML view; nothing is rewritten or reordered behind the scenes.
  • The table flips on top. The Ports view shows a per-service table (service, image, container port, published port, restart) built from that text. Editing a service's published port in the table changes only that one line โ€” every other line, comment, and the container port stay put. Switching between YAML and Ports never loses a comment.
  • Two-step Save โ†’ Apply. Save stages the file. Apply changes relaunches the app so the edited compose lands in the instance. Just like the env editor: nothing changes until Apply runs.
  • Port-collision guard. Each port on an instance must be unique, so Save refuses a published port another site on the same instance already uses, with a clear port N is taken by site "X" message โ€” before a relaunch could fail. Pick a free port and Save again.

Read-only without manage permission

Viewing the Compose tab needs the sites view permission; editing (Save / Apply) needs the sites manage permission (admin-only) โ€” the same grants as the environment editor. The tab shows only for docker-type sites.

The Compose tab on a Docker site
๐Ÿ“ทA Docker site โ†’ Compose: the raw docker-compose.yml is canonical; the Ports view flips on top, and Save โ†’ Apply stages then relaunches the app.img/docker-compose-tab.avif
A Docker site โ†’ Compose: the raw docker-compose.yml is canonical; the Ports view flips on top, and Save โ†’ Apply stages then relaunches the app.

The app's directory, project, and port

The Compose tab names all three at the top of the editor, each one copyable:

FactWhat it is
App directory/srv/docker/<app> โ€” where the compose file and any bind-mounted data live.
Compose projectThe -p name namespacing this app's containers, so many Docker sites share one instance.
Served portThe host port to publish on. The attached domain's route proxies to exactly this port.

The panel owns those paths, as it does for every site type โ€” with one consequence specific to Docker:

The editor is the file โ€” never hand-edit on disk

The panel writes /srv/docker/<app>/docker-compose.yml from the staged text on every Apply, so a hand edit made over SSH or in the site terminal is overwritten on the next relaunch. The Compose tab is the file.

Publish on the served port

In ports: - "8080:5000", the left number is the host (published) port and must be the site's served port โ€” that's what the domain's route points at. The right number is whatever the image itself listens on (5000 for LibreTranslate, 80 for nginx, 3000 for many Node images โ€” check the image's documentation). Getting the left number wrong means the domain serves nothing; getting the right number wrong means the container is up but nothing answers.

Walkthrough: running a real image

Everything a Docker app needs is set from the panel โ€” the terminal is never the install path. To run LibreTranslate, for example:

  1. Create the site with type docker (any image; the starter nginx launches immediately so the domain, route, and certificate can be settled first).

  2. Open the Compose tab and replace the starter with the app's own compose, publishing on the served port the strip shows:

    services:
      app:
        image: libretranslate/libretranslate:latest
        restart: unless-stopped
        ports:
          - "8080:5000"          # left = the site's served port ยท right = what the image listens on
        environment:
          LT_API_KEYS: "true"    # the app's own settings โ€” plain compose, nothing SpipCP-specific
          LT_LOAD_ONLY: en,de,fr,es
        volumes:
          - lt_models:/home/libretranslate/.local
    volumes:
      lt_models:
  3. Save, then Apply changes โ€” the relaunch writes the file and runs docker compose up -d.

  4. Watch it come up on the Docker management plane: status, health, and live logs. An image that downloads models or migrates a database on first boot can take a while to bind its port โ€” the logs are where that's visible.

First boot can outlast the launch check

The launch waits a bounded time for the published port to answer. An image with a long first-boot (large model downloads, a database migration) can exceed it, leaving the site failed while the container is still working โ€” the logs show the truth. Once it's listening, Apply again (or relaunch): with the volumes already populated it comes straight up and the site goes live.

Secrets and settings

Anything the image reads as an environment variable goes in the compose environment: block. For values that shouldn't sit in the compose text, set them on the site's Environment tab and reference them in the compose as ${VAR} โ€” the launch resolves them, exactly as it resolves the starter's ${COMPOSE_IMAGE}.

Anything the image manages itself (API keys held in the app's own database, an admin account created on first run) is done through the app, typically with a command inside the running container โ€” docker compose -p <project> exec app <the app's own CLI> from the instance's terminal, using the compose project the strip shows. That's the app's own tooling, not a SpipCP step.

Connecting to a database

Ticking "create a database" for a Docker app makes SpipCP install the database engine on the instance host and create a database and user, just like a Node or WordPress site. Because a container has its own network, 127.0.0.1 inside the container is the container โ€” not the host โ€” so the starter compose wires the connection automatically:

  • extra_hosts: host.docker.internal:host-gateway lets the container reach the database on the instance host.
  • The credentials are passed into the container as DB_HOST (defaults to host.docker.internal), DB_NAME, DB_USER, and DB_PASSWORD โ€” the app reads them like any container environment variable.

Point the image at the right vars

The image must read DB_HOST / DB_NAME / DB_USER / DB_PASSWORD (or map them to whatever names the image expects in the compose environment: block). The DB runs on the host, reached at host.docker.internal from inside the container โ€” not localhost.

Managing & monitoring

Once a Docker app is running, the Docker management plane is where it can be seen and controlled โ€” fleet-wide and per-app:

  • Status, health, CPU, memory, size โ€” refreshed regularly and shown honestly (a crash-looping or unhealthy container reads as degraded / failed, never green).
  • Lifecycle โ€” start, stop, restart, recreate, and pull (the update path) per app.
  • Live logs โ€” container logs streamed into the browser.
  • Alerts โ€” down, restart-loop, unhealthy, CPU, memory, and disk rules that fire through the configured email and webhook notification channels.

Backups โ€” full coverage

A Docker app backs up like any other site (through restic), and a Docker app gets full coverage โ€” all three places a containerized app keeps state ride the same backup:

  • The compose directory (/srv/docker/<app>) โ€” the compose file plus any bind-mounted data directories under it.
  • A containerized database โ€” if the app runs its own DB as a compose service (e.g. a postgres or mysql container), the backup dumps it from inside the container and the restore reloads it.
  • Named docker volumes โ€” volumes declared in the compose file are captured into the backup and re-hydrated on restore, even though they live outside the app directory.

Declare named volumes

Named volumes are covered when they're declared in the app's compose file, so SpipCP knows which ones belong to the app. The containerized DB's own credentials stay inside the container โ€” the panel never has to hold them to dump or reload it.

Caveats

  • Not a catalog service โ€” docker is installed as part of the launch, not as a catalog service, so it doesn't appear on the instance's Services tab.
  • One compose service in the starter; edit the compose file for more, but the served port and smoke check track the single published port.
  • Instance-local โ€” the container runs inside one instance; there's no orchestration across instances. Each app is its own site, managed fleet-wide by the management plane.

On this page