> ## Documentation Index
> Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Self-hosted services from public Docker images

> Run self-hosted services by using Docker images from public registries.

export const DynamicCodeBlock = ({language = 'yaml', filename, icon, lines, wrap, expandable, highlight, focus, children}) => {
  const STORAGE_KEY = 'upsun_versions_cache';
  const COMPOSABLE_STORAGE_KEY = 'upsun_composable_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = 'https://meta.upsun.com/images';
  const COMPOSABLE_API_URL = 'https://meta.upsun.com/composable';
  const DEBUG_PREFIX = '[DynamicCodeBlock cache]';
  const [versionData, setVersionData] = useState(null);
  const [versionError, setVersionError] = useState(false);
  const [composableData, setComposableData] = useState(null);
  const [composableError, setComposableError] = useState(false);
  useEffect(() => {
    const fetchData = async () => {
      let cachedData = null;
      let cachedEtag = null;
      if (typeof localStorage !== 'undefined') {
        try {
          const cached = localStorage.getItem(STORAGE_KEY);
          if (cached) {
            const parsed = JSON.parse(cached);
            cachedData = parsed?.data || null;
            cachedEtag = parsed?.etag || null;
            if (cachedData && Date.now() - parsed.timestamp < CACHE_TTL) {
              return cachedData;
            }
          }
        } catch (err) {
          console.error('Failed to load from cache:', err);
        }
      }
      const requestHeaders = cachedEtag ? {
        'If-None-Match': cachedEtag
      } : {};
      console.debug(`${DEBUG_PREFIX} revalidating`, {
        storageKey: STORAGE_KEY,
        hasCachedData: Boolean(cachedData),
        hasCachedEtag: Boolean(cachedEtag)
      });
      const response = await fetch(API_URL, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        console.debug(`${DEBUG_PREFIX} revalidated (304)`, {
          storageKey: STORAGE_KEY
        });
        if (typeof localStorage !== 'undefined') {
          try {
            const etag = response.headers.get('etag') || cachedEtag;
            localStorage.setItem(STORAGE_KEY, JSON.stringify({
              data: cachedData,
              etag,
              timestamp: Date.now()
            }));
          } catch (err) {
            console.error('Failed to refresh cache metadata:', err);
          }
        }
        return cachedData;
      }
      if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
      const data = await response.json();
      const etag = response.headers.get('etag');
      console.debug(`${DEBUG_PREFIX} refreshed (200)`, {
        storageKey: STORAGE_KEY,
        etag
      });
      if (typeof localStorage !== 'undefined') {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (err) {
          console.error('Failed to cache data:', err);
        }
      }
      return data;
    };
    fetchData().then(data => setVersionData(data)).catch(err => console.error('Failed to fetch version data:', err));
  }, []);
  const findHighestVersion = versionsMap => {
    if (!versionsMap || Object.keys(versionsMap).length === 0) return null;
    const entries = Object.entries(versionsMap);
    const active = entries.filter(([, v]) => v.upsun && v.upsun.status === 'supported' || v.upsun && v.upsun.status === 'deprecated');
    const candidates = active.length > 0 ? active : entries;
    let [highestName] = candidates[0];
    for (let i = 1; i < candidates.length; i++) {
      const [currentName] = candidates[i];
      const cp = currentName.split('.').map(Number);
      const hp = highestName.split('.').map(Number);
      for (let j = 0; j < Math.max(cp.length, hp.length); j++) {
        if ((cp[j] || 0) > (hp[j] || 0)) {
          highestName = currentName;
          break;
        } else if ((cp[j] || 0) < (hp[j] || 0)) {
          break;
        }
      }
    }
    return highestName;
  };
  const getVersion = (lang, requestedVersion = 'latest') => {
    if (lang === 'composable') {
      if (!composableData || !composableData.versions || Object.keys(composableData.versions).length === 0) return null;
      if (requestedVersion && requestedVersion !== 'latest') {
        return (requestedVersion in composableData.versions) ? requestedVersion : null;
      }
      return findHighestVersion(composableData.versions);
    }
    if (!versionData) return null;
    const imageData = versionData[lang];
    if (!imageData || !imageData.versions || Object.keys(imageData.versions).length === 0) {
      return null;
    }
    if (requestedVersion && requestedVersion !== 'latest') {
      return (requestedVersion in imageData.versions) ? requestedVersion : null;
    }
    return findHighestVersion(imageData.versions);
  };
  let code = typeof children === 'string' ? children : String(children || '');
  const codeLines = code.split('\n');
  while (codeLines.length > 0 && codeLines[0].trim() === '') codeLines.shift();
  while (codeLines.length > 0 && codeLines[codeLines.length - 1].trim() === '') codeLines.pop();
  if (codeLines.length > 0) {
    const indents = codeLines.filter(line => line.trim().length > 0).map(line => line.match(/^[ \t]*/)[0].length);
    const minIndent = Math.min(...indents);
    code = codeLines.map(line => line.slice(minIndent)).join('\n');
  }
  code = code.replace(/\{\{version:(.*?)\}\}/g, (match, params) => {
    const parts = params.split(':');
    const lang = parts[0];
    const ver = parts[1] || 'latest';
    const isComposable = lang === 'composable';
    const hasError = isComposable ? composableError : versionError;
    const dataReady = isComposable ? composableData !== null : versionData !== null;
    if (hasError) return '(unavailable)';
    if (dataReady) {
      const resolvedVersion = getVersion(lang, ver);
      return resolvedVersion || match;
    }
    return '...';
  });
  const codeBlockProps = {
    language,
    ...filename && ({
      filename
    }),
    ...icon && ({
      icon
    }),
    ...lines !== undefined && ({
      lines
    }),
    ...wrap !== undefined && ({
      wrap
    }),
    ...expandable !== undefined && ({
      expandable
    }),
    ...highlight && ({
      highlight
    }),
    ...focus && ({
      focus
    })
  };
  return <CodeBlock {...codeBlockProps}>{code}</CodeBlock>;
};

export const VariableBlock = ({name}) => {
  return <var spellCheck={false} title={`Replace '${name}' with your own data`}>{name}</var>;
};

Deploy a self-hosted service straight from its public Docker image, without writing a Dockerfile or maintaining a build process yourself. Use this approach when the service you need already ships an official, production-ready image.

Unlike Upsun's other app types, which build your repository code into a runtime image, a Docker image app skips that build step: you provide an already-built image, and Upsun deploys it exactly as-is.

If you already configure environment variables, mounts, or service relationships for other apps in your project, you can do the same for a Docker image app, with the adjustments noted in [Mounts](#mounts) and [Environment variables](#environment-variables).

## Verified examples

The following services are verified to run on Upsun using the ready-to-use configuration provided in the corresponding topic.

* [Meilisearch](/docs/add-services/docker-images/docker-meilisearch)

## Before you begin

1. [Install the Upsun CLI](/cli/install).
2. Run the following commands to log in and connect the current Git repository to the correct Upsun project. Replace `<PROJECT_ID>` with your project ID (run `upsun projects` to list them).

   ```bash theme={null}
   upsun login
   upsun auth:info
   upsun set-remote <PROJECT_ID>
   upsun project:info
   upsun environment:info
   ```

## Configure an app

Define each Docker image as an application in `.upsun/config.yaml`. At minimum:

* Set `type` to `docker:1`.
* Set `image.name` to the image tag to run. Images without a registry hostname are pulled from [Docker Hub](https://hub.docker.com/search?badges=official). For images from other registries, include the full registry hostname in `image.name`.

Add a custom start command, persistent storage, or environment variables to the same app definition as needed — see [Startup commands](#startup-commands), [Mounts](#mounts), and [Environment variables](#environment-variables).

```yaml theme={null}
# Minimal configuration. Add a custom start command, mounts, or
# environment variables to this app definition as needed.
applications:
  myapp:
    type: docker:1
    image:
      name: "<registry-or-namespace>/<image>:<tag>"

    web:
      upstream:
        socket_family: tcp
      locations:
        "/":
          passthru: true

routes:
  "https://myapp.{default}/":
    type: upstream
    upstream: "myapp:http"
```

## Deploy an app

Commit the changes to `.upsun/config.yaml` and push them to Upsun:

```bash theme={null}
git add .upsun/config.yaml
git commit -m "Add Docker image app"
upsun push
```

List routes after deployment:

```bash theme={null}
upsun route:list
```

## Startup commands

By default, Upsun runs the startup command defined by the image. `$PORT` typically matches the port exposed by the image.

Set `web.commands.start` in `.upsun/config.yaml` only when the image's default command is not suitable. If you set `web.commands.start`:

* `$PORT` changes to a platform-assigned value. Make sure the command indicates to listen on `$PORT`; otherwise, the app may listen on the wrong port and return `502 Bad Gateway` errors.
* Environment variables in `web.commands.start` are passed literally unless the command runs through a shell. If the image includes `sh`, use `sh -c "..."` when you need environment variable expansion.

```yaml theme={null}
applications:
  myapp:
    type: docker:1
    image:
      name: "example/myapp:1.0.0"
    web:
      commands:
        start: 'sh -c "./myapp --listen=:$PORT --db-host=$DATABASE_HOST"'
```

## Mounts

Any path configured under `mounts` must already exist as a directory in the image. Upsun does not create missing directories inside the image at runtime.

```yaml theme={null}
applications:
  myapp:
    type: docker:1
    image:
      name: "example/myapp:1.0.0"
    mounts:
      "/data":
        source: storage
        source_path: data
```

## Environment variables

Variables under `variables.env` are exposed to the running container.

```yaml theme={null}
applications:
  myapp:
    type: docker:1
    image:
      name: "example/myapp:1.0.0"
    variables:
      env:
        APP_ENV: "production"
```

If the app connects to Upsun services, use direct service variables such as `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_USERNAME`, and `DATABASE_PASSWORD`.

Avoid relying on `PLATFORM_RELATIONSHIPS` inside minimal Docker images. It requires base64 and JSON parsing tools that may not exist in the image. Use direct service variables instead.

## Updating the image version

Change the image tag in `image.name`, then rebuild and redeploy the environment.

* For fixed version tags, commit the tag change and run `upsun push`.
* For `latest` tags, trigger a redeploy so Upsun pulls the current image for that tag.

To redeploy from the Console: select your project, choose the environment, click **More**, and click **Redeploy**.

To redeploy from the CLI: manually trigger a build by updating an environment variable and pushing the change by running `upsun push`.

## Debug an app over SSH

If a container fails to start, a health check fails, or the image has no shell of its own, connect over SSH to debug it:

```bash theme={null}
upsun ssh --app myapp
```

This connects to the LXC container, the layer Upsun controls around the image, not the image's own container (consistent with other Upsun app images). From the LXC container, you can:

* View logs, such as `/var/log/app.log`, to debug start command failures.

* Inspect the environment around the image's container, for example mounts, networking, and process state, without needing a shell inside the image. The process list shows whether the app's start command is running, still starting, or crash-looping, which is the main signal available when the health check fails and the image has no shell to exec into:

  ```bash theme={null}
  pwd
  env
  ps -ef
  ls -la /
  ```

* Enter the image's own container, if it includes a shell. Upsun cannot inject a shell into an image it does not control.

The Docker CLI is not available inside the shell. Use standard shell commands to inspect the running app.

## Limitations

* Docker image apps use pre-built images. Upsun does not build a Dockerfile from the repository for this app type.
* Build hooks are not supported for Docker image apps.
* Meilisearch is the only image currently verified to work on Upsun. Other public images may work but have not been tested.
* You are responsible for updating images when image publishers release security fixes.
* Docker images include their own filesystem and can use more project storage than Upsun's built-in runtime images.
* Keep images under 4 GB. Larger images require different storage handling. To use an image larger than 4 GB, create a Support ticket and provide the full image name and registry path.

  To keep image size down:

  * Use a minimal base image such as `alpine`, `distroless`, or `busybox`.
  * Avoid bundling build tools or caches into your production image.
  * Use multi-stage builds to separate build and runtime layers.

  Minimal images such as `distroless` and `busybox` may not include shell tools such as `sh`, `base64`, or `jq`. This affects startup command expansion and any logic that depends on decoding `PLATFORM_RELATIONSHIPS`.
