Easiest 3D printing tools for Docker Compose
Which 3D printing tools install in one Compose command, by services and values to edit first, plus what you do by hand. Includes the minimal PrintStash install.
Count the work, not the containers. By that measure the shortest installs in this category are Spoolman if you only want filament inventory, Manyfold’s solo image or PrintStash if you want a model library, and OctoPrint if the printer is plugged into the same machine as Docker. Each of those is one Compose file and one up -d, and the differences show up in what you have to decide before that command and what is still missing after it.
PrintStash starts two containers and needs no environment file at all: every value in the Compose file has a working default, and the app generates its own signing secret on first boot. The one step it adds is a setup token printed to the API log, which you paste into the first-run wizard. Manyfold’s solo image is a single container but wants a SECRET_KEY_BASE you generate yourself. Neither needs Postgres.
Checked against each project’s current documentation and repository on August 17, 2026. PrintStash details were read from the v0.11.4 tag rather than the docs page, so a few of them are more current than the install guide.
What makes a Compose install easy
Container count is a bad proxy. A single image that refuses to boot without a generated secret and a correctly owned bind mount is more work than two images that start with defaults. The things that cost you time are how many services come up on a default file, whether an external database is mandatory, how many values you have to edit before the first start, whether anything gets built locally, and what is left to do by hand once the containers are healthy.
| Tool | Services on a default file | External database | Values to set first | Left to do after up |
|---|---|---|---|---|
| PrintStash 0.11.4 | 2 (frontend, api) | None. SQLite in a named volume | None | Read the setup token from the API log, then create the admin account in the browser |
| Manyfold solo v0.147.2 | 1 (database and Redis bundled in) | None | SECRET_KEY_BASE, plus PUID/PGID to match the owner of your volumes |
Create the administrator account in the browser |
| Manyfold standard | 3 (app, postgres:15, redis:7) |
Postgres and Redis | SECRET_KEY_BASE, DATABASE_*, REDIS_URL |
Create the administrator account in the browser |
| Spoolman | 1 | None. SQLite by default | None (TZ is optional) |
Create the data folder and chown 1000:1000 it before starting |
| OctoPrint | 1 | None | None | Pass the printer’s serial device through, then run OctoPrint’s wizard |
| Mainsail | 1, and it is only the web UI | None | Printer addresses in config.json |
Install Klipper and Moonraker on the printer host, which this method does not do |
Mainsail is the honest edge case. Its Docker page is accurate and short, and it says plainly that Klipper and Moonraker are not installed this way and have to already be running on the printer. So Compose gets you a dashboard, not a printing stack. If someone tells you the Klipper stack is a one-command Compose deploy, they are counting the last container.
The minimal PrintStash install
The whole install is two commands and a grep, with nothing to open in an editor:
mkdir printstash && cd printstashcurl -O https://raw.githubusercontent.com/xiao-villamor/PrintStash/main/docker-compose.ymldocker compose up -dThe file pulls prebuilt images from GHCR for linux/amd64 and linux/arm64, so nothing compiles on your machine. Cloning the repository works too and is what the installation guide shows, but you only need the one file. Both the file and the images it names track the latest release, which is the right default for a first install and the wrong one for a server you upgrade deliberately: for that, set PRINTSTASH_VERSION to the release you want and take the Compose file from the matching tag instead of main.
Then find the first-run token and open the UI:
docker compose logs api | grep "setup token"Open http://localhost:3000, paste the token, and create the first admin account. There is no default username or password, and the wizard rejects a wrong token with a 403.
The project’s own README is more cautious than its code, and every gap makes the install shorter than advertised rather than longer.
.env is optional. Every variable in the Compose file carries a default, so the stack comes up without one. Copying .env.example is for when you want to change something.
You do not have to invent a JWT secret. The placeholder shipped in the Compose file is public, so since 0.8.4 the API refuses to sign anything with it: on first boot it generates a real secret and stores it in its own database, where it survives restarts. Set VAULT_JWT_SECRET yourself if you would rather hold that value, but leaving it alone is not the security hole the placeholder looks like.
The setup token is generated per process when you have not set VAULT_SETUP_TOKEN, so if the API container restarts before you finish the wizard, the old token stops working and you grep the log again. Setting the variable to a value of your own makes it stable, which is worth doing on a host where containers get restarted on a schedule.
What you get from that default install is SQLite and local disk, both in named Docker volumes, with database migrations run by the image entrypoint on every start. About 1 GB of RAM works, 2 GB is comfortable because thumbnailing large meshes is the heavy step, and one or two cores is enough. Since v0.12.0 the full image generates STEP and STP previews on ARM as well as amd64.
Why two containers instead of one
The frontend container is nginx. It serves the built single-page app and proxies /api/v1, WebSockets included, to the api container on the same origin, which is why there is no browser-side API URL to configure. That removes a familiar failure in split-origin deployments, where the page loads fine and every request afterwards goes to the wrong host.
It also means the API port is not published on the host. The api service only exposes 8000 on the internal Compose network, so the liveness endpoint you can reach from the host is http://localhost:3000/api/v1/health, through nginx. With the optional profiles off, port 3000 is the only thing published, so there is one port to think about rather than three.
Advanced setup that stays optional
The optional services live behind Compose profiles, so they stay stopped until you name them. That is the part the deploy comparisons usually miss: PrintStash ships a Postgres service and an S3-compatible service in the same file it hands a first-time user, and neither one starts.
# Postgres instead of SQLitedocker compose --profile postgres up -d
# A local SeaweedFS target for S3-compatible storagedocker compose --profile s3 up -d seaweedfsSeaweedFS 4.41 is pinned by digest and its S3 endpoint lands on port 9000. MinIO left the normal stack in v0.12.0 and existing volumes are migrated through docker-compose.migrate-minio.yml, which copies objects across without deleting the source, since MinIO’s upstream is archived and abandoning object data silently would be worse than keeping a migration path around. Whether you need either is a real decision rather than a default: SQLite or Postgres, local or S3 works through when the added service pays for itself, and for a single-user home library the answer is usually no.
There is a smaller file too, and since v0.12.0 it changes more than its line count. docker-compose.light.yml declares only frontend and api, which is what a default install runs anyway, but the api it names is now a different image: printstash-api-lite rather than printstash-api. Lite is built without Chromium, so browser-assisted imports of pages that block a plain fetch are gone, and without the OpenCASCADE tessellation that generates STEP previews, on every platform rather than only on ARM. It keeps mesh thumbnailing through NumPy, Pillow, and Trimesh, so STL, 3MF, and OBJ previews are untouched. CI refuses the build unless lite comes out at least 700 MiB smaller than full and starts no slower.
The file also drops 101 lines and 24 environment keys, all of them for things it then cannot do: VAULT_STORAGE_BACKEND and the nine VAULT_S3_* variables for S3 or R2 vault storage, the five VAULT_BACKUP_S3_* variables for pushing backups to a bucket, the retention setting, and the credentials for the optional services.
curl -o docker-compose.yml https://raw.githubusercontent.com/xiao-villamor/PrintStash/main/docker-compose.light.ymldocker compose up -dSaving it under the standard name keeps the command short and comes with one trap: any later instruction that re-downloads docker-compose.yml overwrites it with the full file. Take the light file if you are staying on SQLite and local disk and you do not need STEP previews or browser-assisted imports. Skip it if storage might move later, and note that it is the less-exercised path, since the 0.8.2 release notes record it being repaired for the people using it.
For anything reachable from outside the LAN, docker-compose.prod.yml publishes only the frontend and binds it to 127.0.0.1, so nothing is exposed except through the proxy you put in front:
curl -O https://raw.githubusercontent.com/xiao-villamor/PrintStash/main/docker-compose.prod.ymlecho "VAULT_JWT_SECRET=$(openssl rand -hex 32)" >> .envdocker compose -f docker-compose.prod.yml up -dThis is the one place the secret is not optional, and it is deliberate. The production file declares it as ${VAULT_JWT_SECRET:?set a strong VAULT_JWT_SECRET in .env}, so Compose refuses to start without it rather than quietly falling back to a generated value on a host you are about to expose. Leave that line out and you get an error, not a running stack.
Because nginx already proxies the API and WebSockets, your reverse proxy has exactly one upstream. A working Caddyfile is two lines, and reverse proxy and TLS covers the nginx and Traefik equivalents plus the body-size limit you have to raise alongside VAULT_MAX_UPLOAD_MB.
The rest of the advanced surface is opt-in in the same way. PRINTSTASH_VERSION in .env pins the image tag for deliberate upgrades. VAULT_OIDC_ENABLED and the issuer, client, and group-claim variables add Authentik, Authelia, or Keycloak login while local accounts keep working as a fallback. FORWARDED_ALLOW_IPS tells uvicorn which proxy address may set the client IP, and leaving it unset trusts only loopback, which is the right default when the port is published directly.
Two operational details cost more than their size suggests. Credentials stored in the database are encrypted with a key file that PrintStash creates at /data/db/.printstash-secrets-key unless you supply VAULT_SECRETS_KEY, and a database backup cannot decrypt them without it, so that file needs a copy of its own. The other is that 0.11.4 makes the supported topology explicit at one API process per vault: startup claims a lock and fails fast when another API process is already live, so scaling that container is not a knob you have.
Where PrintStash is not the easiest option
Manyfold’s solo image is one container against two, and if the shortest possible stack is what you are optimizing for, it wins on that count. Spoolman is smaller still, because tracking spools is a smaller job. Neither of those is a criticism of a project. It is what a narrower scope buys.
PrintStash is also not a printer dashboard or a slicer, so a Compose file that gives you a library does not replace Mainsail, Fluidd, or OctoPrint, and it does not slice anything. It talks to Moonraker for status, upload, and send-to-print, and runs beside those interfaces. If your actual question was how to get a printer on the network with the least effort, the library is the wrong layer to start at.
The setup token is one step more than “open the page and make an account”, and it exists because the first-run endpoint has to be reachable before any account exists. I would rather grep a log line than leave that endpoint open, but it is a step, and any comparison that calls PrintStash a zero-friction install is skipping it.
Questions that come up
Which self-hosted 3D printing tool is genuinely easiest to deploy with Docker Compose?
It depends on the job, and the useful ranking is by decisions rather than containers. Spoolman is the smallest thing here: one image, SQLite, one bind mount you have to create and chown first, and it only tracks filament. For a model library, Manyfold’s solo image and PrintStash are close, with Manyfold at one container that needs a generated SECRET_KEY_BASE and PrintStash at two containers that need no environment file but do need a setup token from the API log. OctoPrint is one container and the constraint is physical rather than software, since the printer has to be attached to the Docker host. Mainsail installs as a single container that is only the UI, so it is the easiest deploy and the least complete one.
Does PrintStash need Postgres or an S3 bucket?
No. The default install is SQLite plus local disk, both in named Docker volumes, and that is also the best-tested path. Postgres and SeaweedFS ship in the same Compose file but sit behind profiles named postgres and s3, which means Docker never starts them unless you pass the profile on the command line. The confusion is understandable, since the Postgres service is sitting right there in the file you downloaded, but reading it as part of the install gets the footprint wrong by two containers.
Do I have to set a secret before starting it?
Not since 0.8.4. The Compose file defaults VAULT_JWT_SECRET to a placeholder that is published in the repository, and because it is public the API treats it as unusable: on first boot it generates a real secret, stores it in its own configuration table, and keeps using that one across restarts so nobody gets logged out. Setting your own with openssl rand -hex 32 is still reasonable if you want to own that value or move it between installs, but it is a preference rather than a prerequisite. The project’s README and the light Compose file both still tell you to change it first, which overstates what the code requires.
Why does the first launch ask for a setup token, and where is it?
Because the endpoint that creates the very first administrator cannot require an existing login, so something else has to gate it. When VAULT_SETUP_TOKEN is empty the API generates a random one per process and writes it to its log while the vault is unconfigured, and docker compose logs api | grep "setup token" is enough to find it. Submitting the wizard without it returns a 403. The token being per process matters in one case: restart the API container mid-setup and you need the new value from the log, which you can avoid entirely by setting the variable to a value you chose.
Can I run the Klipper web stack with Compose too?
Partly, and it is worth being precise about which part. Mainsail’s documented Docker setup runs the web interface in one container and configures the network addresses of printers it should talk to, and its own docs state that Klipper and Moonraker are not installed by that method and must already be running on the printer. Klipper needs to talk to the printer’s controller board, so it lives on the machine physically wired to it. Compose is a good fit for the dashboard and a poor fit for the firmware layer underneath it.
What is the least work to reach it from outside the LAN?
Switch to docker-compose.prod.yml, which publishes only the frontend and binds it to 127.0.0.1, then point a TLS-terminating proxy at that single port. That file is also the one place VAULT_JWT_SECRET becomes mandatory: it is declared so that Compose fails rather than starting with a generated secret on a host you are about to expose, so put a value from openssl rand -hex 32 in .env first. Because the frontend’s nginx already proxies the API and its WebSockets on the same origin, your proxy has one upstream and no path rules, and a Caddyfile for it is two lines. Raise the proxy’s request body limit to match VAULT_MAX_UPLOAD_MB or large uploads fail before they reach the API. Do not port-forward the containers directly, and treat remote access as your responsibility rather than something the application solves.
Does the simple install lock me out of Postgres or S3 later?
No, but moving is real work rather than a flag flip, so pick deliberately if you already know you want Postgres. Storage backends and database URLs are configuration, and the optional services are in the file you already have, so nothing needs reinstalling. What you do need is a migration of existing data, and the practical route is a full backup first. Starting on SQLite and local disk is the normal recommendation because it is the path most installs use, which is also where problems surface and get fixed first.
Sources
- PrintStash
docker-compose.yml,docker-compose.prod.yml,.env.example,backend/app/core/config.py,backend/app/services/setup_token.py,backend/app/api/v1/setup.py,backend/app/main.py, andfrontend/nginx.confat tagv0.11.4, plus the 0.8.4 and 0.11.4 changelog entries. Read August 17, 2026. - Manyfold Docker installation and administrator setup, version v0.147.2 as shown on manyfold.app. Checked August 17, 2026.
- Spoolman installation wiki, octoprint-docker, and the Mainsail Docker setup. Checked August 17, 2026.
If you have decided on PrintStash, getting started continues from the setup wizard through the first model and a connected printer. For the feature side of the Manyfold decision rather than the deploy side, read PrintStash vs Manyfold.