Replacing Yarn with npm in Docker Workshop (#24012)

- replace old Docker desktop screenshot with the new sidebar
- Remove all references to Yarn in the explanation for Part 1
- Show new project directory structure for getting-started-repo
- Remove PWD reference from Part 3
- Remove all occurances of Yarn in Part 7 and 8

<!--Delete sections as needed -->

## Description

<!-- Tell us what you did and why -->

## Related issues or tickets

<!-- Related issues, pull requests, or Jira tickets -->

## Reviews

This PR is heavily dependent upon
https://github.com/docker/getting-started-app/pull/98

<!-- Notes for reviewers here -->
<!-- List applicable reviews (optionally @tag reviewers) -->

- [ ] Technical review
- [ ] Editorial review
- [ ] Product review

---------

Co-authored-by: Craig Osterhout <craig.osterhout@docker.com>
This commit is contained in:
Ajeet Singh Raina, Docker Captain, ARM Innovator
2026-01-28 21:28:31 +05:30
committed by GitHub
parent 0440aae80a
commit ca0e85cf12
9 changed files with 171 additions and 182 deletions

View File

@@ -42,10 +42,10 @@ Before you can run the application, you need to get the application source code
├── getting-started-app/
│ ├── .dockerignore
│ ├── package.json
│ ├── package-lock.json
│ ├── README.md
│ ├── spec/
│ ├── src/
│ └── yarn.lock
```
## Build the app's image
@@ -58,18 +58,21 @@ To build the image, you'll need to use a Dockerfile. A Dockerfile is simply a te
```dockerfile
# syntax=docker/dockerfile:1
FROM node:lts-alpine
FROM node:24-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
RUN npm install --omit=dev
CMD ["node", "src/index.js"]
EXPOSE 3000
```
This Dockerfile does the following:
This Dockerfile starts off with a `node:lts-alpine` base image, a
light-weight Linux image that comes with Node.js and the Yarn package
manager pre-installed. It copies all of the source code into the image,
installs the necessary dependencies, and starts the application.
- Uses `node:24-alpine` as the base image, a lightweight Linux image with Node.js pre-installed
- Sets `/app` as the working directory
- Copies source code into the image
- Installs the necessary dependencies
- Specifies the command to start the application
- Documents that the app listens on port 3000
2. Build the image using the following commands:
@@ -85,9 +88,9 @@ To build the image, you'll need to use a Dockerfile. A Dockerfile is simply a te
$ docker build -t getting-started .
```
The `docker build` command uses the Dockerfile to build a new image. You might have noticed that Docker downloaded a lot of "layers". This is because you instructed the builder that you wanted to start from the `node:lts-alpine` image. But, since you didn't have that on your machine, Docker needed to download the image.
The `docker build` command uses the Dockerfile to build a new image. You might have noticed that Docker downloaded a lot of "layers". This is because you instructed the builder that you wanted to start from the `node:24-alpine` image. But, since you didn't have that on your machine, Docker needed to download the image.
After Docker downloaded the image, the instructions from the Dockerfile copied in your application and used `yarn` to install your application's dependencies. The `CMD` directive specifies the default command to run when starting a container from this image.
After Docker downloaded the image, the instructions from the Dockerfile copied in your application and used `npm` to install your application's dependencies.
Finally, the `-t` flag tags your image. Think of this as a human-readable name for the final image. Since you named the image `getting-started`, you can refer to that image when you run a container.

View File

@@ -81,55 +81,7 @@ Let's try to push the image to Docker Hub.
## Run the image on a new instance
Now that your image has been built and pushed into a registry, try running your app on a brand
new instance that has never seen this container image. To do this, you will use Play with Docker.
> [!NOTE]
>
> Play with Docker uses the amd64 platform. If you are using an ARM based Mac with Apple silicon, you will need to rebuild the image to be compatible with Play with Docker and push the new image to your repository.
>
> To build an image for the amd64 platform, use the `--platform` flag.
> ```console
> $ docker build --platform linux/amd64 -t YOUR-USER-NAME/getting-started .
> ```
>
> Docker buildx also supports building multi-platform images. To learn more, see [Multi-platform images](/manuals/build/building/multi-platform.md).
1. Open your browser to [Play with Docker](https://labs.play-with-docker.com/).
2. Select **Login** and then select **docker** from the drop-down list.
3. Sign in with your Docker Hub account and then select **Start**.
4. Select the **ADD NEW INSTANCE** option on the left side bar. If you don't see it, make your browser a little wider. After a few seconds, a terminal window opens in your browser.
![Play with Docker add new instance](images/pwd-add-new-instance.webp)
5. In the terminal, start your freshly pushed app.
```console
$ docker run -dp 0.0.0.0:3000:3000 YOUR-USER-NAME/getting-started
```
You should see the image get pulled down and eventually start up.
> [!TIP]
>
> You may have noticed that this command binds the port mapping to a
> different IP address. Previous `docker run` commands published ports to
> `127.0.0.1:3000` on the host. This time, you're using `0.0.0.0`.
>
> Binding to `127.0.0.1` only exposes a container's ports to the loopback
> interface. Binding to `0.0.0.0`, however, exposes the container's port
> on all interfaces of the host, making it available to the outside world.
>
> For more information about how port mapping works, see
> [Networking](/manuals/engine/network/_index.md#published-ports).
6. Select the 3000 badge when it appears.
If the 3000 badge doesn't appear, you can select **Open Port** and specify `3000`.
Now that your image has been built and pushed into a registry, you can run your app on any machine that has Docker installed. Try pulling and running your image on another computer or a cloud instance.
## Summary

View File

@@ -65,7 +65,7 @@ filesystem you can share with containers. For details about accessing the settin
{{< tab name="Mac / Linux" >}}
```console
$ docker run -it --mount type=bind,src="$(pwd)",target=/src ubuntu bash
$ docker run -it --mount type=bind,src=.,target=/src ubuntu bash
```
{{< /tab >}}
@@ -79,14 +79,14 @@ filesystem you can share with containers. For details about accessing the settin
{{< tab name="Git Bash" >}}
```console
$ docker run -it --mount type=bind,src="/$(pwd)",target=/src ubuntu bash
$ docker run -it --mount type=bind,src="/.",target=/src ubuntu bash
```
{{< /tab >}}
{{< tab name="PowerShell" >}}
```console
$ docker run -it --mount "type=bind,src=$($pwd),target=/src" ubuntu bash
$ docker run -it --mount "type=bind,src=.,target=/src" ubuntu bash
```
{{< /tab >}}
@@ -116,7 +116,7 @@ filesystem you can share with containers. For details about accessing the settin
```console
root@ac1237fad8db:/# cd src
root@ac1237fad8db:/src# ls
Dockerfile node_modules package.json spec src yarn.lock
Dockerfile node_modules package.json package-lock.json spec src
```
6. Create a new file named `myfile.txt`.
@@ -124,7 +124,7 @@ filesystem you can share with containers. For details about accessing the settin
```console
root@ac1237fad8db:/src# touch myfile.txt
root@ac1237fad8db:/src# ls
Dockerfile myfile.txt node_modules package.json spec src yarn.lock
Dockerfile myfile.txt node_modules package.json package-lock.json spec src
```
7. Open the `getting-started-app` directory on the host and observe that the
@@ -136,9 +136,9 @@ filesystem you can share with containers. For details about accessing the settin
│ ├── myfile.txt
│ ├── node_modules/
│ ├── package.json
│ ├── package-lock.json
│ ├── spec/
── src/
│ └── yarn.lock
── src/
```
8. From the host, delete the `myfile.txt` file.
@@ -146,7 +146,7 @@ filesystem you can share with containers. For details about accessing the settin
```console
root@ac1237fad8db:/src# ls
Dockerfile node_modules package.json spec src yarn.lock
Dockerfile node_modules package.json package-lock.json spec src
```
10. Stop the interactive container session with `Ctrl` + `D`.
@@ -180,9 +180,9 @@ You can use the CLI or Docker Desktop to run your container with a bind mount.
```console
$ docker run -dp 127.0.0.1:3000:3000 \
-w /app --mount type=bind,src="$(pwd)",target=/app \
node:lts-alpine \
sh -c "yarn install && yarn run dev"
-w /app --mount type=bind,src=.,target=/app \
node:24-alpine \
sh -c "npm install && npm run dev"
```
The following is a breakdown of the command:
@@ -190,13 +190,13 @@ You can use the CLI or Docker Desktop to run your container with a bind mount.
create a port mapping
- `-w /app` - sets the "working directory" or the current directory that the
command will run from
- `--mount type=bind,src="$(pwd)",target=/app` - bind mount the current
- `--mount type=bind,src=.,target=/app` - bind mount the current
directory from the host into the `/app` directory in the container
- `node:lts-alpine` - the image to use. Note that this is the base image for
- `node:24-alpine` - the image to use. Note that this is the base image for
your app from the Dockerfile
- `sh -c "yarn install && yarn run dev"` - the command. You're starting a
shell using `sh` (alpine doesn't have `bash`) and running `yarn install` to
install packages and then running `yarn run dev` to start the development
- `sh -c "npm install && npm run dev"` - the command. You're starting a
shell using `sh` (alpine doesn't have `bash`) and running `npm install` to
install packages and then running `npm run dev` to start the development
server. If you look in the `package.json`, you'll see that the `dev` script
starts `nodemon`.
@@ -226,9 +226,9 @@ You can use the CLI or Docker Desktop to run your container with a bind mount.
```powershell
$ docker run -dp 127.0.0.1:3000:3000 `
-w /app --mount "type=bind,src=$pwd,target=/app" `
node:lts-alpine `
sh -c "yarn install && yarn run dev"
-w /app --mount "type=bind,src=.,target=/app" `
node:24-alpine `
sh -c "npm install && npm run dev"
```
The following is a breakdown of the command:
@@ -236,13 +236,13 @@ You can use the CLI or Docker Desktop to run your container with a bind mount.
create a port mapping
- `-w /app` - sets the "working directory" or the current directory that the
command will run from
- `--mount "type=bind,src=$pwd,target=/app"` - bind mount the current
- `--mount "type=bind,src=.,target=/app"` - bind mount the current
directory from the host into the `/app` directory in the container
- `node:lts-alpine` - the image to use. Note that this is the base image for
- `node:24-alpine` - the image to use. Note that this is the base image for
your app from the Dockerfile
- `sh -c "yarn install && yarn run dev"` - the command. You're starting a
shell using `sh` (alpine doesn't have `bash`) and running `yarn install` to
install packages and then running `yarn run dev` to start the development
- `sh -c "npm install && npm run dev"` - the command. You're starting a
shell using `sh` (alpine doesn't have `bash`) and running `npm install` to
install packages and then running `npm run dev` to start the development
server. If you look in the `package.json`, you'll see that the `dev` script
starts `nodemon`.
@@ -273,8 +273,8 @@ You can use the CLI or Docker Desktop to run your container with a bind mount.
```console
$ docker run -dp 127.0.0.1:3000:3000 ^
-w /app --mount "type=bind,src=%cd%,target=/app" ^
node:lts-alpine ^
sh -c "yarn install && yarn run dev"
node:24-alpine ^
sh -c "npm install && npm run dev"
```
The following is a breakdown of the command:
@@ -284,11 +284,11 @@ You can use the CLI or Docker Desktop to run your container with a bind mount.
command will run from
- `--mount "type=bind,src=%cd%,target=/app"` - bind mount the current
directory from the host into the `/app` directory in the container
- `node:lts-alpine` - the image to use. Note that this is the base image for
- `node:24-alpine` - the image to use. Note that this is the base image for
your app from the Dockerfile
- `sh -c "yarn install && yarn run dev"` - the command. You're starting a
shell using `sh` (alpine doesn't have `bash`) and running `yarn install` to
install packages and then running `yarn run dev` to start the development
- `sh -c "npm install && npm run dev"` - the command. You're starting a
shell using `sh` (alpine doesn't have `bash`) and running `npm install` to
install packages and then running `npm run dev` to start the development
server. If you look in the `package.json`, you'll see that the `dev` script
starts `nodemon`.
@@ -318,9 +318,9 @@ You can use the CLI or Docker Desktop to run your container with a bind mount.
```console
$ docker run -dp 127.0.0.1:3000:3000 \
-w //app --mount type=bind,src="/$(pwd)",target=/app \
node:lts-alpine \
sh -c "yarn install && yarn run dev"
-w //app --mount type=bind,src="/.",target=/app \
node:24-alpine \
sh -c "npm install && npm run dev"
```
The following is a breakdown of the command:
@@ -328,13 +328,13 @@ You can use the CLI or Docker Desktop to run your container with a bind mount.
create a port mapping
- `-w //app` - sets the "working directory" or the current directory that the
command will run from
- `--mount type=bind,src="/$(pwd)",target=/app` - bind mount the current
- `--mount type=bind,src="/.",target=/app` - bind mount the current
directory from the host into the `/app` directory in the container
- `node:lts-alpine` - the image to use. Note that this is the base image for
- `node:24-alpine` - the image to use. Note that this is the base image for
your app from the Dockerfile
- `sh -c "yarn install && yarn run dev"` - the command. You're starting a
shell using `sh` (alpine doesn't have `bash`) and running `yarn install` to
install packages and then running `yarn run dev` to start the development
- `sh -c "npm install && npm run dev"` - the command. You're starting a
shell using `sh` (alpine doesn't have `bash`) and running `npm install` to
install packages and then running `npm run dev` to start the development
server. If you look in the `package.json`, you'll see that the `dev` script
starts `nodemon`.

View File

@@ -212,14 +212,14 @@ You can now start your dev-ready container.
```console
$ docker run -dp 127.0.0.1:3000:3000 \
-w /app -v "$(pwd):/app" \
-w /app -v ".:/app" \
--network todo-app \
-e MYSQL_HOST=mysql \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=secret \
-e MYSQL_DB=todos \
node:lts-alpine \
sh -c "yarn install && yarn run dev"
node:24-alpine \
sh -c "npm install && npm run dev"
```
{{< /tab >}}
@@ -228,14 +228,14 @@ You can now start your dev-ready container.
```powershell
$ docker run -dp 127.0.0.1:3000:3000 `
-w /app -v "$(pwd):/app" `
-w /app -v ".:/app" `
--network todo-app `
-e MYSQL_HOST=mysql `
-e MYSQL_USER=root `
-e MYSQL_PASSWORD=secret `
-e MYSQL_DB=todos `
node:lts-alpine `
sh -c "yarn install && yarn run dev"
node:24-alpine `
sh -c "npm install && npm run dev"
```
{{< /tab >}}
@@ -250,8 +250,8 @@ You can now start your dev-ready container.
-e MYSQL_USER=root ^
-e MYSQL_PASSWORD=secret ^
-e MYSQL_DB=todos ^
node:lts-alpine ^
sh -c "yarn install && yarn run dev"
node:24-alpine ^
sh -c "npm install && npm run dev"
```
{{< /tab >}}
@@ -259,14 +259,14 @@ You can now start your dev-ready container.
```console
$ docker run -dp 127.0.0.1:3000:3000 \
-w //app -v "/$(pwd):/app" \
-w //app -v "/.:/app" \
--network todo-app \
-e MYSQL_HOST=mysql \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=secret \
-e MYSQL_DB=todos \
node:lts-alpine \
sh -c "yarn install && yarn run dev"
node:24-alpine \
sh -c "npm install && npm run dev"
```
{{< /tab >}}
@@ -276,11 +276,13 @@ You can now start your dev-ready container.
using the mysql database.
```console
$ nodemon src/index.js
[nodemon] 2.0.20
[nodemon] 3.1.11
[nodemon] to restart at any time, enter `rs`
[nodemon] watching dir(s): *.*
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node src/index.js`
Waiting for mysql:3306.
Connected!
Connected to mysql db at host mysql
Listening on port 3000
```

View File

@@ -29,9 +29,9 @@ In the `getting-started-app` directory, create a file named `compose.yaml`.
│ ├── compose.yaml
│ ├── node_modules/
│ ├── package.json
│ ├── package-lock.json
│ ├── spec/
── src/
│ └── yarn.lock
── src/
```
## Define the app service
@@ -40,14 +40,14 @@ In [part 6](./07_multi_container.md), you used the following command to start th
```console
$ docker run -dp 127.0.0.1:3000:3000 \
-w /app -v "$(pwd):/app" \
-w /app -v ".:/app" \
--network todo-app \
-e MYSQL_HOST=mysql \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=secret \
-e MYSQL_DB=todos \
node:lts-alpine \
sh -c "yarn install && yarn run dev"
node:24-alpine \
sh -c "npm install && npm run dev"
```
You'll now define this service in the `compose.yaml` file.
@@ -58,7 +58,7 @@ You'll now define this service in the `compose.yaml` file.
```yaml
services:
app:
image: node:lts-alpine
image: node:24-alpine
```
2. Typically, you will see `command` close to the `image` definition, although there is no requirement on ordering. Add the `command` to your `compose.yaml` file.
@@ -66,8 +66,8 @@ You'll now define this service in the `compose.yaml` file.
```yaml
services:
app:
image: node:lts-alpine
command: sh -c "yarn install && yarn run dev"
image: node:24-alpine
command: sh -c "npm install && npm run dev"
```
3. Now migrate the `-p 127.0.0.1:3000:3000` part of the command by defining the `ports` for the service.
@@ -75,22 +75,22 @@ You'll now define this service in the `compose.yaml` file.
```yaml
services:
app:
image: node:lts-alpine
command: sh -c "yarn install && yarn run dev"
image: node:24-alpine
command: sh -c "npm install && npm run dev"
ports:
- 127.0.0.1:3000:3000
```
4. Next, migrate both the working directory (`-w /app`) and the volume mapping
(`-v "$(pwd):/app"`) by using the `working_dir` and `volumes` definitions.
(`-v ".:/app"`) by using the `working_dir` and `volumes` definitions.
One advantage of Docker Compose volume definitions is you can use relative paths from the current directory.
```yaml
services:
app:
image: node:lts-alpine
command: sh -c "yarn install && yarn run dev"
image: node:24-alpine
command: sh -c "npm install && npm run dev"
ports:
- 127.0.0.1:3000:3000
working_dir: /app
@@ -103,8 +103,8 @@ You'll now define this service in the `compose.yaml` file.
```yaml
services:
app:
image: node:lts-alpine
command: sh -c "yarn install && yarn run dev"
image: node:24-alpine
command: sh -c "npm install && npm run dev"
ports:
- 127.0.0.1:3000:3000
working_dir: /app
@@ -185,8 +185,8 @@ At this point, your complete `compose.yaml` should look like this:
```yaml
services:
app:
image: node:lts-alpine
command: sh -c "yarn install && yarn run dev"
image: node:24-alpine
command: sh -c "npm install && npm run dev"
ports:
- 127.0.0.1:3000:3000
working_dir: /app

View File

@@ -27,14 +27,13 @@ to create each layer within an image.
```plaintext
IMAGE CREATED CREATED BY SIZE COMMENT
a78a40cbf866 18 seconds ago /bin/sh -c #(nop) CMD ["node" "src/index.j… 0B
f1d1808565d6 19 seconds ago /bin/sh -c yarn install --production 85.4MB
f1d1808565d6 19 seconds ago /bin/sh -c npm install --omit=dev 85.4MB
a2c054d14948 36 seconds ago /bin/sh -c #(nop) COPY dir:5dc710ad87c789593… 198kB
9577ae713121 37 seconds ago /bin/sh -c #(nop) WORKDIR /app 0B
b95baba1cfdb 13 days ago /bin/sh -c #(nop) CMD ["node"] 0B
<missing> 13 days ago /bin/sh -c #(nop) ENTRYPOINT ["docker-entry… 0B
<missing> 13 days ago /bin/sh -c #(nop) COPY file:238737301d473041… 116B
<missing> 13 days ago /bin/sh -c apk add --no-cache --virtual .bui… 5.35MB
<missing> 13 days ago /bin/sh -c #(nop) ENV YARN_VERSION=1.21.1 0B
<missing> 13 days ago /bin/sh -c addgroup -g 1000 node && addu… 74.3MB
<missing> 13 days ago /bin/sh -c #(nop) ENV NODE_VERSION=12.14.1 0B
<missing> 13 days ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
@@ -61,30 +60,31 @@ Look at the following Dockerfile you created for the getting started app.
```dockerfile
# syntax=docker/dockerfile:1
FROM node:lts-alpine
FROM node:24-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
RUN npm install --omit=dev
CMD ["node", "src/index.js"]
EXPOSE 3000
```
Going back to the image history output, you see that each command in the Dockerfile becomes a new layer in the image.
You might remember that when you made a change to the image, the yarn dependencies had to be reinstalled. It doesn't make much sense to ship around the same dependencies every time you build.
You might remember that when you made a change to the image, the dependencies had to be reinstalled. It doesn't make much sense to ship around the same dependencies every time you build.
To fix it, you need to restructure your Dockerfile to help support the caching
of the dependencies. For Node-based applications, those dependencies are defined
in the `package.json` file. You can copy only that file in first, install the
dependencies, and then copy in everything else. Then, you only recreate the yarn
dependencies, and then copy in everything else. Then, you only recreate the
dependencies if there was a change to the `package.json`.
1. Update the Dockerfile to copy in the `package.json` first, install dependencies, and then copy everything else in.
```dockerfile
# syntax=docker/dockerfile:1
FROM node:lts-alpine
FROM node:24-alpine
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --production
COPY package.json package-lock.json ./
RUN npm install --omit=dev
COPY . .
CMD ["node", "src/index.js"]
```
@@ -103,13 +103,13 @@ dependencies if there was a change to the `package.json`.
=> => transferring dockerfile: 175B
=> [internal] load .dockerignore
=> => transferring context: 2B
=> [internal] load metadata for docker.io/library/node:lts-alpine
=> [internal] load metadata for docker.io/library/node:24-alpine
=> [internal] load build context
=> => transferring context: 53.37MB
=> [1/5] FROM docker.io/library/node:lts-alpine
=> [1/5] FROM docker.io/library/node:24-alpine
=> CACHED [2/5] WORKDIR /app
=> [3/5] COPY package.json yarn.lock ./
=> [4/5] RUN yarn install --production
=> [3/5] COPY package.json package-lock.json ./
=> [4/5] RUN npm install --omit=dev
=> [5/5] COPY . .
=> exporting to image
=> => exporting layers
@@ -127,13 +127,13 @@ dependencies if there was a change to the `package.json`.
=> => transferring dockerfile: 37B
=> [internal] load .dockerignore
=> => transferring context: 2B
=> [internal] load metadata for docker.io/library/node:lts-alpine
=> [internal] load metadata for docker.io/library/node:24-alpine
=> [internal] load build context
=> => transferring context: 450.43kB
=> [1/5] FROM docker.io/library/node:lts-alpine
=> [1/5] FROM docker.io/library/node:24-alpine
=> CACHED [2/5] WORKDIR /app
=> CACHED [3/5] COPY package.json yarn.lock ./
=> CACHED [4/5] RUN yarn install --production
=> CACHED [3/5] COPY package.json package-lock.json ./
=> CACHED [4/5] RUN npm install
=> [5/5] COPY . .
=> exporting to image
=> => exporting layers
@@ -182,21 +182,26 @@ for your production build. You can ship the static resources in a static nginx c
```dockerfile
# syntax=docker/dockerfile:1
FROM node:lts AS build
FROM node:24-alpine AS build
WORKDIR /app
COPY package* yarn.lock ./
RUN yarn install
COPY package* ./
RUN npm install
COPY public ./public
COPY src ./src
RUN yarn run build
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
```
In the previous Dockerfile example, it uses the `node:lts` image to perform the build (maximizing layer caching) and then copies the output
In the previous Dockerfile example, it uses the `node:24-alpine` image to perform the build (maximizing layer caching) and then copies the output
into an nginx container.
> [!Tips]
> This React example is for illustration purposes. The getting-started todo app is a `Node.js` backend application, not a React frontend.
## Summary
In this section, you learned a few image building best practices, including layer caching and multi-stage builds.

View File

@@ -3,51 +3,78 @@ title: What next after the Docker workshop
weight: 100
linkTitle: "Part 9: What next"
keywords: get started, setup, orientation, quickstart, intro, concepts, containers,
docker desktop
description: Making sure you have more ideas of what you could do next with your application
docker desktop, AI, model runner, MCP, agents, hardened images, security
description: Explore what to do next after completing the Docker workshop, including securing your images, AI development, and language-specific guides.
aliases:
- /get-started/11_what_next/
- /guides/workshop/10_what_next/
summary: |
Now that you've completed the Docker workshop, you're ready to explore
securing your images with Docker Hardened Images, building AI-powered
applications, and diving into language-specific guides.
notoc: true
secure-images:
- title: What are Docker Hardened Images?
description: Understand secure, minimal, production-ready base images with near-zero CVEs.
link: /dhi/explore/what/
- title: Get started with DHI
description: Pull and run your first Docker Hardened Image in minutes.
link: /dhi/get-started/
- title: Use hardened images
description: Learn how to use DHI in your Dockerfiles and CI/CD pipelines.
link: /dhi/how-to/use/
- title: Explore the DHI catalog
description: Browse available hardened images, variants, and security attestations.
link: /dhi/how-to/explore/
ai-development:
- title: Docker Model Runner
description: Run and manage AI models locally using familiar Docker commands with OpenAI-compatible APIs.
link: /ai/model-runner/
- title: MCP Toolkit
description: Set up, manage, and run containerized MCP servers to power your AI agents.
link: /ai/mcp-catalog-and-toolkit/toolkit/
- title: Build AI agents with cagent
description: Create teams of specialized AI agents that collaborate to solve complex problems.
link: /ai/cagent/
- title: Use AI models in Compose
description: Define AI model dependencies in your Docker Compose applications.
link: /compose/how-tos/model-runner/
language-guides:
- title: Node.js
description: Learn how to containerize and develop Node.js applications.
link: /guides/language/nodejs/
- title: Python
description: Build and run Python applications in containers.
link: /guides/language/python/
- title: Java
description: Containerize Java applications with best practices.
link: /guides/language/java/
- title: Go
description: Develop and deploy Go applications using Docker.
link: /guides/language/golang/
---
Although you're done with the workshop, there's still a lot more to learn about containers.
Congratulations on completing the Docker workshop. You've learned how to containerize applications, work with multi-container setups, use Docker Compose, and apply image-building best practices.
Here are a few other areas to look at next.
Here's what to explore next.
## Container orchestration
## Secure your images
Running containers in production is tough. You don't want to log into a machine and simply run a
`docker run` or `docker compose up`. Why not? Well, what happens if the containers die? How do you
scale across several machines? Container orchestration solves this problem. Tools like Kubernetes,
Swarm, Nomad, and ECS all help solve this problem, all in slightly different ways.
Take your image-building skills to the next level with Docker Hardened Images—secure, minimal, and production-ready base images that are now free for everyone.
The general idea is that you have managers who receive the expected state. This state might be
"I want to run two instances of my web app and expose port 80." The managers then look at all of the
machines in the cluster and delegate work to worker nodes. The managers watch for changes (such as
a container quitting) and then work to make the actual state reflect the expected state.
{{< grid items="secure-images" >}}
## Cloud Native Computing Foundation projects
## Build with AI
The CNCF is a vendor-neutral home for various open-source projects, including Kubernetes, Prometheus,
Envoy, Linkerd, NATS, and more. You can view the [graduated and incubated projects here](https://www.cncf.io/projects/)
and the entire [CNCF Landscape here](https://landscape.cncf.io/). There are a lot of projects to help
solve problems around monitoring, logging, security, image registries, messaging, and more.
Docker makes it easy to run AI models locally and build agentic AI applications. Explore Docker's AI tools and start building AI-powered apps.
## Getting started video workshop
{{< grid items="ai-development" >}}
Docker recommends watching the video workshop from DockerCon 2022. Watch the entire video or use the following links to open the video at a particular section.
## Language-specific guides
* [Docker overview and installation](https://youtu.be/gAGEar5HQoU)
* [Pull, run, and explore containers](https://youtu.be/gAGEar5HQoU?t=1400)
* [Build a container image](https://youtu.be/gAGEar5HQoU?t=3185)
* [Containerize an app](https://youtu.be/gAGEar5HQoU?t=4683)
* [Connect a DB and set up a bind mount](https://youtu.be/gAGEar5HQoU?t=6305)
* [Deploy a container to the cloud](https://youtu.be/gAGEar5HQoU?t=8280)
Apply what you've learned to your preferred programming language with hands-on tutorials.
<iframe src="https://www.youtube-nocookie.com/embed/gAGEar5HQoU" style="max-width: 100%; aspect-ratio: 16 / 9;" width="560" height="auto" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
## Creating a container from scratch
If you'd like to see how containers are built from scratch, Liz Rice from Aqua Security has a fantastic talk in which she creates a container from scratch in Go. While the talk does not go into networking, using images for the filesystem, and other advanced topics, it gives a deep dive into how things are working.
<iframe src="https://www.youtube-nocookie.com/embed/8fi7uSYlOdc" style="max-width: 100%; aspect-ratio: 16 / 9;" width="560" height="auto" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
{{< grid items="language-guides" >}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB