---
title: "Docker"
description: "Run the CloudPDF server as a self-contained Docker image from GitHub Container Registry."
source: "https://www.cloudpdf.com/docs/server/deployment/docker"
---

# Run with Docker

The server ships as a single, self-contained image. A consumer only needs
`docker pull` and `docker run` — no repository, Node, or PDF toolchain. The Node
22 runtime, native PDFium, image processing, SQLite, and fonts are all baked in.

Images are published to GitHub Container Registry:

```
ghcr.io/embedpdf/cloudpdf-server
```

## Run it

The zero-config profile uses SQLite, the local filesystem, and a local cache,
all under the `/data` volume:

```sh
docker run --rm --init -p 3000:3000 \
  -v cloudpdf-data:/data \
  -e CLOUDPDF_LICENSE_KEY="key/..." \
  ghcr.io/embedpdf/cloudpdf-server:latest
```

The server is fail-closed on licensing: `CLOUDPDF_LICENSE_KEY` is **required**
(a [development key](https://www.cloudpdf.com/docs/server/configuration/licensing) keeps everything
else zero-config for local runs — even the JWT secret falls back to a dev
default, with a warning).

- `--init` gives the container correct signal and zombie handling. The server
  already handles `SIGTERM`/`SIGINT`, so it stops cleanly.
- `-v cloudpdf-data:/data` persists your data across restarts. **Everything
  mutable lives under `/data`** — the SQLite file, stored objects, and the cache.

Confirm it's up:

```sh
curl localhost:3000/healthz   # {"status":"ok"}
curl localhost:3000/readyz    # {"status":"ok"}
```

## The built-in defaults

The image sets production-friendly defaults so the zero-config run just works:

| Variable                   | Default in the image  |
| -------------------------- | --------------------- |
| `NODE_ENV`                 | `production`          |
| `PORT` / `HOST`            | `3000` / `0.0.0.0`    |
| `CLOUDPDF_DB_SQLITE_PATH`  | `/data/cloudpdf.db`   |
| `CLOUDPDF_STORAGE_FS_ROOT` | `/data/objects`       |
| `CLOUDPDF_CACHE_ROOT`      | `/data/cache`         |
| `CLOUDPDF_AUTO_MIGRATE`    | `1` (migrate on boot) |

Override any of them with `-e`. The full list is in the
[configuration reference](https://www.cloudpdf.com/docs/server/configuration).

## The entrypoint is the CLI

The image's entrypoint is the `cloudpdf-server` binary and the default command is
`serve`, so you can run any subcommand by appending it:

```sh
docker run --rm -v cloudpdf-data:/data \
  ghcr.io/embedpdf/cloudpdf-server:latest migrate status

docker run --rm -v cloudpdf-data:/data \
  ghcr.io/embedpdf/cloudpdf-server:latest db doctor
```

See the [CLI reference](https://www.cloudpdf.com/docs/server/operations/cli) for all commands.

## Production: external database and storage

For production, point the container at Postgres and a cloud bucket, take
migrations out of the boot path, and let the pool use your cores:

Generate the server secrets once and keep them in your secret manager — they
must stay **stable across restarts** (see
[Encrypted-PDF secrets](https://www.cloudpdf.com/docs/server/configuration/authentication#encrypted-pdf-secrets)
for what the last two protect):

```sh
export CLOUDPDF_LICENSE_KEY="key/..."
export CLOUDPDF_JWT_SECRET="$(openssl rand -hex 32)"
export CLOUDPDF_PASSWORD_VERIFICATION_HMAC_SECRET="$(openssl rand -hex 32)"
export CLOUDPDF_PASSWORD_SESSION_SERVER_SECRET="$(openssl rand -hex 32)"
```

```sh
docker run -d --init -p 3000:3000 \
  -e CLOUDPDF_LICENSE_KEY \
  -e CLOUDPDF_JWT_SECRET \
  -e CLOUDPDF_PASSWORD_VERIFICATION_HMAC_SECRET \
  -e CLOUDPDF_PASSWORD_SESSION_SERVER_SECRET \
  -e CLOUDPDF_DB_DRIVER=postgres \
  -e CLOUDPDF_DB_URL="postgres://user:pass@db:5432/cloudpdf" \
  -e CLOUDPDF_STORAGE_KIND=s3 \
  -e CLOUDPDF_STORAGE_S3_BUCKET=my-cloudpdf-bucket \
  -e CLOUDPDF_STORAGE_S3_REGION=eu-west-1 \
  -e CLOUDPDF_AUTO_MIGRATE=0 \
  -e CLOUDPDF_FAIL_ON_PENDING=1 \
  -e CLOUDPDF_WORKER_POOL_SIZE=max \
  ghcr.io/embedpdf/cloudpdf-server:1.0.0
```

Run migrations as a separate, one-shot step before rolling out new containers —
so replicas never race each other to migrate:

```sh
docker run --rm \
  -e CLOUDPDF_DB_DRIVER=postgres \
  -e CLOUDPDF_DB_URL="postgres://user:pass@db:5432/cloudpdf" \
  ghcr.io/embedpdf/cloudpdf-server:1.0.0 migrate up
```

> Managing more than the server itself — Postgres, object storage, a migration
> step — is exactly what [Docker
> Compose](https://www.cloudpdf.com/docs/server/deployment/docker-compose) wires up for you.

## Health checks

The image includes a built-in `HEALTHCHECK` that polls `/healthz`, so
`docker ps` and your orchestrator can see container health without extra
configuration.

## Image tags & versioning

| Tag                      | Meaning                                          |
| ------------------------ | ------------------------------------------------ |
| `:1.2.3`                 | An exact, immutable release.                     |
| `:1.2`                   | The latest patch in a minor line.                |
| `:latest`                | The newest release. Convenient, but moves.       |
| `:1.2.3-next.N`, `:next` | Pre-release candidates from the preview channel. |

> In production, pin a **specific version** — ideally by digest
> (`@sha256:…`) — so every replica runs identical bytes. Never run
> `:latest` in production.

Pin by digest:

```sh
docker pull ghcr.io/embedpdf/cloudpdf-server:1.0.0
docker run ... ghcr.io/embedpdf/cloudpdf-server@sha256:<digest>
```

## Next steps

- [Docker Compose](https://www.cloudpdf.com/docs/server/deployment/docker-compose) — Add Postgres and object storage in one file.
- [Configuration reference](https://www.cloudpdf.com/docs/server/configuration) — Every CLOUDPDF\_\* setting.
