How to Build Your Own Docker Image (A Dockerfile for Your Script or App)

Build your own Docker image: Your script becomes a container like everything else.

Everything so far has used images other people built. Sooner or later you have a Python script that polls an API, a little Flask app, or a Node bot, and you want it to run like the rest of the server: in a container, restarted on failure, updated with compose up. A Dockerfile is a short recipe — start from a base image, copy the code in, install what it needs, say how to start it — and docker compose build turns it into an image.

Quick reference

TaskWhat
RecipeDockerfile next to your code
Builddocker build -t myapp . or docker compose build
Rundocker run --rm myapp or build: in compose + up -d
Keep it smallpython:3.12-slim, node:22-alpine, .dockerignore
Don't run as rootUSER app
Rebuild after a code changedocker compose up -d --build

1. A Python script

Say ~/code/weather-bot/bot.py with a requirements.txt.

nano ~/code/weather-bot/Dockerfile

FROM python:3.12-slim

WORKDIR /app

RUN useradd --create-home app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
USER app

CMD ["python", "bot.py"]

Line by line: start from a small official Python; work in /app; make a non-root user; copy only the requirements first and install them (so this slow step is cached while you edit code); copy the rest; drop to the user; and the command that runs when the container starts.

nano ~/code/weather-bot/.dockerignore

.git
__pycache__
*.pyc
.env
data/

Without this, COPY . . drags your git history and secrets into the image.

2. Build and run

cd ~/code/weather-bot
docker build -t weather-bot .
docker run --rm -e API_KEY=xxx weather-bot

-t tags it with a name. The first build downloads the base image; later builds reuse cached layers and take seconds unless requirements.txt changed.

3. Run it with compose like everything else

nano ~/code/weather-bot/compose.yaml

services:
weather-bot:
build: .
container_name: weather-bot
env_file: .env
volumes:
- ./data:/app/data
restart: unless-stopped

docker compose up -d --build
docker compose logs -f

build: . tells compose to build from the Dockerfile in this folder instead of pulling. After editing the code: docker compose up -d --build again.

4. A Node app and a static site

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]

A static site is even shorter — build it, serve it with Nginx:

FROM nginx:alpine
COPY public/ /usr/share/nginx/html/

5. Multi-stage: build tools out, result in

A site generated by Hugo or a compiled Go program needs tools to build but not to run:

FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o app

FROM gcr.io/distroless/static
COPY --from=build /src/app /app
USER nonroot
CMD ["/app"]

The final image is a few megabytes and contains nothing but your program.

6. Good habits

Share it with the Pi

Push to Gitea's container registry (tutorial 45: docker tag weather-bot git.example.com/john/weather-bot && docker push ...), or just copy the folder and compose up -d --build there. For a Pi, build on the Pi (ARM) or use docker buildx build --platform linux/arm64.

Troubleshooting

"COPY failed: file not found". The path is relative to the build context (the . in docker build). Files in .dockerignore are invisible too.

Image is 1 GB. You used python:3.12 instead of -slim, or installed build tools and left them. Slim base, --no-cache-dir, multi-stage.

Permission denied writing to the volume. The container runs as app (UID 1000 inside) but the host folder is owned by someone else. Match the UIDs or chown the folder.

Changes to the code don't appear. You ran up -d without --build. Compose reuses the old image unless told to rebuild.

Wrapping up

A ten-line Dockerfile, a .dockerignore, build: . in compose, up -d --build. Your own script becomes a first-class citizen of the server — restarted, logged, limited and backed up like everything else — and you never again wonder which Python version it needs.

Call us