Containerising an application and building our own images
Docker - Overview and how to use it
6 min read
Published Jul 13 2025
Guide Sections
Guide Comments
Containerisation is the process of packaging an application along with all its dependencies, libraries, and configuration files into a single unit — an image — which can run reliably as a container in any environment.
Understand Your App's Structure
Before writing a Dockerfile, it's essential to understand your app:
- What language and framework does it use? (e.g., Node.js, Python, Go)
- What are its runtime dependencies?
- How is it started? (e.g.,
npm start
,python app.py
) - Are there configuration files (e.g.,
.env
,config.json
)?
Example Node.js project structure:
Create a Dockerfile
The Dockerfile is a script that tells Docker how to build your image. Here's an example for a simple Node.js app:
Explanation:
FROM
: Specifies the base image (e.g.,node
,python
,ubuntu
).WORKDIR
: Sets the directory where all commands will be run.COPY
: Adds files from your machine into the image.RUN
: Executes commands during image build (e.g., install dependencies).CMD
: The default command run when a container starts.EXPOSE
: Indicates the port the container will listen on.
Each command in a Dockerfile results in a new image layer:
Each layer stores only the changes made compared to the previous one. This has benefits:
- Caching: If nothing has changed in an earlier step, Docker can reuse cached layers.
- Efficiency: Smaller rebuilds and faster development.
- Portability: Layers can be downloaded individually and reused across images.
Docker ignore file
Similar to .gitignore
, you should create a .dockerignore
file to avoid copying unnecessary files:
This speeds up builds and keeps your image clean and secure.
Multi-Stage Builds
Multi-stage builds allow you to create complex Docker images that are small, clean, and production-ready, while still using intermediate steps for building, testing, or compiling.
Key idea:
- Use one or more build stages to compile code or perform setup.
- Then copy the result into a final minimal image, leaving behind the build tools and dependencies.
Example of a multi-stage Dockerfile:
Benefits:
- Final image is much smaller (no dev dependencies or build tools).
- Keeps the image cleaner and more secure.
- Multiple build stages can be used — for example:
builder
,tester
,production
,docs
.
Best Practices with Multi-Stage Builds:
- Name your stages with
AS
for clarity. - Use
COPY --from=<stage>
to move only what's needed. - Use minimal base images for the final stage (like
alpine
,slim
, ordistroless
). - Always cache intelligently: keep frequently-changing steps lower in the Dockerfile.
Build the Docker Image
Once your Dockerfile is ready, you can build the image:
Options:
-t my-app
: Tags the image with a name (my-app
)..
: Tells Docker to look for the Dockerfile in the current directory.
You can add version and latest tags when building too:
Adds tags:
my-app:1.0.0
is a versioned tag.my-app:latest
is a convenience tag, usually pointing to the current stable build.
After building, you can confirm it exists with:
You can pass build time arguments when building a Dockerfile.
ARG
defines build-time variables, which are used only while the image is being built. They do not persist in the final image unless explicitly passed into ENV
.
In the Dockerfile:
You pass values during build with --build-arg
:
If not passed, it defaults to the value in the Dockerfile (default_value
in this case).
Good For:
- Configuring optional tools or behavior in builds.
- Passing tokens, Git SHA versions, or feature flags.
- Avoiding leaking secrets into final images.
Scope:
- Available only after declared in the Dockerfile.
- Not accessible at runtime unless passed to
ENV
. ARG
is only available in the stage where it’s defined - On mulit-stage builds, they can be used to setENV
values needed for building, that you don't want appearing in the final image. If you want to use the sameARG
in multiple stages, you must redeclare it in each stage.
You can use an ARG
to define a default value and pass it into ENV
:
This lets you inject values during build and keep them available at runtime if needed.
Security Consideration
ARG
values are not included in the final image unless used inENV
, but they can still be seen in image history.ENV
values are visible withdocker inspect
, so avoid storing secrets or passwords there.
Multi-Architecture Builds
By default, Docker builds for the host architecture (e.g., ARM64 on M1 Macs, x86_64 on Intel). To build for multiple platforms, use Docker Buildx, which extends Docker's build
capabilities.
Explanation:
--platform
: Targets architectures (e.g.,linux/amd64
,linux/arm64
)-t
: Adds one or more tags.
: Build context--push
: Pushes the multi-arch manifest and actual images to a registry (required to make the multi-arch image useful)
Without --push
, Docker can only build for your local architecture unless you're using an emulation backend.
You can check which platforms are supported by an existing image:
Common list of platforms:
OS | Architecture | Platform string | Notes |
Linux | amd64 |
| Most common for servers and desktops (Intel/AMD 64-bit) |
Linux | arm64 |
| Common for Apple M1/M2, Raspberry Pi 4+ |
Linux | arm/v7 |
| 32-bit ARM (Raspberry Pi 2/3, older SBCs) |
Linux | arm/v6 |
| Very old ARM devices |
Linux | 386 |
| 32-bit x86 (legacy PCs) |
Linux | s390x |
| IBM mainframe systems |
Linux | ppc64le |
| IBM PowerPC Little Endian |
Windows | amd64 |
| Windows containers (requires Windows host or LCOW support) |
Windows | arm64 |
| ARM based Windows containers, quite rare |
Note: Windows containers require Windows hosts — Linux-based Docker engines (like Docker Desktop on Mac) cannot run Windows containers natively.
To see which platforms your builder supports:
To see a full platform list:
Example of a more detailed production multi-stage Dockerfile
This is an example NextJS application Dockerfile that produces a small production ready image.
Build steps:
- Installs the node packages required for building the application, based on the preferred package manager.
- Builds the application and runs database migrations, utilising build arguments to know the database connection string but this value isn't included in the final built image.
- Copies over just the final built files on top of a fresh base layer - ie. no files only needed at build time are included in the final production image, reducing the size.