Docker Compose: build vs image for VPS, wetin e mean
image go pull published tag, build go build from Dockerfile. See why compose up ignores your Dockerfile change for VPS, plus the correct fix.
Docker Compose build versus image: di short answer
For Docker Compose file, image: dey name image wey Compose go pull from registry, while build: tell Compose to build image for this machine from Dockerfile. If you set only image:, Compose go pull that tag and run am. If you set only build:, Compose go build image here, then give am name wey come from project name and service name. If you set both, Compose go build am locally, then tag the result with the name for image:. Na so you fit build image and push am with name wey you choose.
Na the full difference be that. Everything wey follow explain wetin e mean when you dey operate am for server. This assume say Docker Engine and Compose plugin don already install; how to run Docker for VPS cover that part.
Di three forms complete
Pull one published tag and run am. Dockerfile no dey involved at any point.
services:
web:
image: nginx:1.27
restart: unless-stopped
ports:
- "80:80"Build from Dockerfile wey dey inside current directory. Nothing dey get pulled except the base image wey dem name for FROM.
services:
web:
build: .
restart: unless-stopped
ports:
- "80:80"Build am locally and tag the result. docker compose push fit then send that exact tag go registry.
services:
web:
build:
context: .
dockerfile: Dockerfile
image: registry.example.com/acme/web:1.4.2
restart: unless-stopped
ports:
- "80:80"context na the directory wey dem send go builder. dockerfile dey resolve relative to that context, so context: . with dockerfile: docker/prod.Dockerfile normal and correct. Run docker compose images to see the image name and image ID behind each service container. Na the fastest way to confirm which of these three forms you actually write.
Why docker compose up no build again after I change the Dockerfile?
Because up dey check whether the image dey exist, not whether e current.
When Compose start a service wey get a build: section, e dey look for the image inside the local image store. If image with that name don already dey there, Compose go use am. E no dey read your Dockerfile, compare your source files, or check any timestamp. The Compose specification define this rule with the pull_policy attribute, and the default behaviour dey build image only when e no dey available. If image dey present, Compose treat am as good enough.
So you edit app.py, run docker compose up -d, see Compose report say the container dey run, but the old code still dey serve. Nothing fail, so nothing warn you. This na the most common "my change no take effect" report with Compose. The clue na the status word wey Compose print beside the container name: a container wey Compose replace go report as recreated or started, while a container wey Compose decide to leave alone go report as running.
Two checks go settle the matter. docker compose images go print the image ID wey each container dey use, so write am down before the deploy and compare am after. docker image ls get a CREATED column, and any image wey dem create before your last commit na stale image, no matter wetin the deploy script print.
Which flags force rebuild
docker compose up -d --buildgo build first, then e go recreate any container wey image don change. Na this flag most people dey look for.docker compose build webgo build one service and e no start anything. Follow am withdocker compose up --no-deps -d webto replace only that container and leave the rest of the stack running.docker compose build --no-cache webgo discard every cached layer and rebuild from the first instruction.docker compose build --pullgo try pull newer version of the base image forFROM, so moving tag likenode:22go pick the current contents instead of the copy wey you download for March.docker compose up -d --force-recreatego recreate containers from the image wey dem already dey use. E never build. If you use am when you mean--build, na common dead end.
You fit also put this decision inside the file. As Compose specification talk am, pull_policy: build mean say Compose go build the image, and rebuild am if e already dey present. Every up go pay for a build, and na wetin you want for laptop but rarely wetin you want for server.
services:
web:
build: .
image: registry.example.com/acme/web:dev
pull_policy: buildOne more interaction dey important to know. docker compose pull go try pull images for services wey get build section too, and if that pull fail, e go tell you say you must build the image instead. Pass --ignore-buildable to quietly skip those services.
How build cache dey decide your deploy time
Each instruction for Dockerfile dey produce one layer, and builder go reuse cached layer when that instruction and its inputs never change. For COPY, na the contents of the files wey dey copied be the inputs. Once one layer miss cache, every layer after am go rebuild, because each layer dey build on the filesystem wey the previous one produce.
Na this one rule dey decide whether your deploy go take seconds or minutes. Arrange Dockerfile from things wey rarely change reach things wey change for every commit.
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]npm ci dey above COPY . ., so when you edit source file, install layer remain cached and build continue from copy step. If you swap those two lines, one-character change go reinstall every dependency, because COPY . . dey invalidate the layer wey npm ci build on. The same pattern apply to pip install -r requirements.txt and go mod download.
--no-cache na the correct tool when you suspect say stale layer dey hide your fix. But e no good as default, because e dey throw away the reuse wey Dockerfile ordering suppose give you.
One thing wey image set and Compose fit override: Dockerfile's CMD na wetin image dey run by default, and command: key for the service go replace am. How command and entrypoint dey work together important here, because Compose override fit make freshly built image behave exactly like the old one.
Build context and .dockerignore
context: . mean say Compose package that directory and send am go the builder before the first instruction run. Everything wey dey inside go enter, including .git and any data directory wey you keep near your source. If build dey pause for the transferring-context step even though the project no change, e mean say the context too large.
A .dockerignore file for the context root fit exclude paths from that transfer. The syntax near .gitignore.
.git
node_modules
*.log
data/
.envYou get two benefits. The transfer go smaller, so every build go start faster. Also, COPY . . no fit copy .env enter the image again, where anybody wey pull that image fit read am.
The slow-build problem wey dey grow over time na bind mount. Named volume dey outside your project directory, but bind mount like ./data:/var/lib/postgresql/data dey inside the build context. So your builds go dey slower every week as the database dey grow. One line for .dockerignore go fix am. Bind mounts against named volumes explain the wider trade-off.
Build arguments get smaller version of the same risk. Values wey you pass through args: dey visible for the image history to anybody wey get the image. So put version number there, and never put token. Env files and secrets in Compose explain where credentials suppose dey instead.
You wan build on the VPS, or build for another place then pull?
Building for the same box wey dey serve your traffic na the default because na the shortest path: git pull, then docker compose up -d --build. This one dey okay for small server wey nobody depend on yet. E stop to dey okay for two reasons wey you fit measure, plus one wey only go show for bad day.
Memory. Build dey run compilers and bundlers beside your live application, and dem na the part wey dey use plenty memory for most stacks. For 1 GB VPS, JavaScript bundler or Rust compile dey often be the biggest process for the box. When kernel run out of memory, e kill the biggest process: either build stop with Killed and exit status 137, or e kill your database instead and site go down during deploy. dmesg -T | grep -i oom dey print the kill line with process name, so you fit know which one happen instead of guessing.
Disk. Every build dey leave layers behind, and builder dey keep its own cache separately from your images. docker system df dey show both, and build cache row just dey grow. Reclaim space with docker image prune for dangling images and docker builder prune for cached layers. Full disk dey stop more than build. Database go stop writing too, and that failure dey cost far more than slow deploy.
Reproducibility. Image wey you build on server dey exist only for that server. To roll back, you need checkout the old commit and build again. But that build no guarantee say e go produce the same thing, because base tag don move and package mirrors don move with am. If you build elsewhere and push a tag, rollback become just one edit: point image: to the previous tag and run docker compose up -d.
The arrangement wey dey hold up na simple. Your continuous integration go run the build and push registry.example.com/acme/web:<git-sha>, while the Compose file for VPS go carry image: without any build: key. Deployment then become two commands wey need almost no memory.
docker compose pull
docker compose up -dRun docker login registry.example.com once for the server, and Compose fit pull private tags from that time onward.
Keep the build section for development instead of deleting am, for file wey you name yourself.
# compose.dev.yaml
services:
web:
build:
context: .
pull_policy: builddocker compose -f compose.yaml -f compose.dev.yaml up -d --buildName that file compose.dev.yaml, no be compose.override.yaml. Compose dey load override file automatically whenever e dey present, so stray override wey somebody copy go server fit quietly make am start building there again. Layering several Compose files explain how the merge dey resolve each key.
The architecture trap when you build elsewhere
Image get the CPU architecture wey e use build. If you build am for Apple Silicon laptop, push am, then pull that tag go x86_64 VPS, Docker go warn say the requested image platform no match the host platform wey e detect. The process go then die with exec format error. The error dey look like corrupt binary, but e no be so. Build am specifically for the target:
docker buildx build --platform linux/amd64 \
-t registry.example.com/acme/web:1.4.2 --push .The same mismatch fit happen the other way if your laptop na x86 and you run ARM VPS instead of x86 one. If you make CI build for the same architecture wey you deploy to, you no go need worry about this.
Wetin to check after deploy
docker compose imagesdey show the image and tag wey dey behind every container wey dey run. If image ID change, na proof say the new build dey in service.docker compose configdey show the merged file after variable substitution, so you fit read the final image name wey Compose go use before you run anything.docker compose logs -f webfor the first half minute after the swap. Container wey start and exit go dey restart for loop instead of staying up, and the loop quiet unless you check am.docker image lsdey show a CREATED column. If image old pass your last commit, dem never rebuild am.
If you still dey assemble the file wey these checks go run against, the basics of a Compose file on a VPS dey explain the related keys, while the Compose command cheat sheet list the remaining subcommands.
FAQ
I fit use build and image for the same service?
Yes, and na the normal setup for project wey you build by yourself. Compose dey build from the build: section and tag the result with the value of image:. Na that tag docker compose push dey send go registry, and another machine dey pull. If image: key no dey, Compose still dey build, but e name the image after the project and service, and e warn say the missing attribute stop the image from being pushed.
Why docker compose up no dey pick my Dockerfile change?
Because up only dey check whether image with that name dey exist. Once one dey available, Compose start am and e no compare am with your Dockerfile or source files. Run docker compose up -d --build, or run docker compose build web followed by docker compose up --no-deps -d web to replace one service. If you set pull_policy: build for the service, every up go rebuild, and this fit well for development machine.
Wetin be the difference between --build and --force-recreate?
--build build the image again, then e recreate containers wey their image change. --force-recreate recreate containers from the image wey dem already get, so e no fit ever pick code change. If your change dey for source or Dockerfile, --build na the flag wey you need. --force-recreate na for resetting the container itself, for example, to clear its writable layer while e keep the same image.
I suppose build my Docker images for VPS or for somewhere else?
Build am somewhere else and pull a tag once the box dey serve traffic too. Build dey compete with your application for memory, and small VPS fit handle this competition by making the kernel kill the biggest process. That process fit be the build or your database. Builds also leave cache for disk wey nothing dey reclaim for you. Building for the server still dey okay for small project wey no get users, and moving later no cost much if you keep the build: section for development-only Compose file.
How I fit stop Docker build cache from filling my disk?
Run docker system df to see how much space your images and build cache dey use separately. docker builder prune remove cached layers, while docker image prune remove dangling images wey earlier builds leave behind. Adding -a to either one dey more aggressive and e force your next build to start cold. No schedule docker system prune -af --volumes for server, because --volumes delete any volume wey no container dey use currently, and stack wey you stop for maintenance fit hold your database inside exactly that kind volume.