All infomation in this document comes from the official Docker tutorial and adapted to MacOS 11 and ARM-64 architecture.
docker login -u YOUR-USER-NAME
docker tag getting-started YOUR-USER-NAME/getting-started
docker push YOUR-USER-NAME/getting-started
Do docker run -dp 3000:3000 YOUR-USER-NAME/getting-started
in the sand box.
-
Start a
Ubuntu
container that will create a file named /data.txt with a random number between 1 and 10000.docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"
-
Validate we can see the output by
exec
'ing into the container. To do so, open the Dashboard and click the first action of the container that is running theUbuntu
image.In the bash, run
cat /data.txt
to see the content of the/data.txt
file.Or in the command line, do
docker exec <container-id> cat /data.txt
. -
Now start another
Ubuntu
container:docker run -it ubuntu ls /
No
/data.txt
in the filesystem. -
Remove the first container using
docker rm -f
.
Docker's official doc about volumes
"Volumes provide the ability to connect specific filesystem paths of the container back to the host machine. If a directory in the container is mounted, changes in that directory are also seen on the host machine. If we mount that same directory across container restarts, we'd see the same files."
Named volume: a bucket of data stored on the host
-
Create a volume using the
docker volume create
command:docker volume create todo-db
-
Restart the todo app container:
docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started
-
Add some new items then remove the container
docker rm -f <id>
-
Start a new container
Question: where is Docker actually storing the data when using a named volume?
Use:
docker volume inspect todo-db
The Mountpoint
shown is the actual location on the disk where the data is stored. (Root access is necessary to access this directory from the host.)
Bind mounts: the other way to persist data.
Often used to provide additional data into containers.
When working on an application, we can use a bind mount to mount our source code into the container to let it see code changes, respond, and let us see the changes right away.
Named volumes | Bind mounts | |
---|---|---|
Host location | Docker chooses | You control |
Mount example (using -v ) |
my-volume:/usr/local/data | /path/to/data:/usr/local/data |
Populates new volume with container contents | Yes | No |
Support volume drivers | Yes | No |
- Mount our source code into the container
- Install all dependencies, including the "dev" dependencies
- Start
nodemon
to watch for filesystem changes
-
Shutdown all previous
getting-started
containers. -
Run the command below in
/app
folder:docker run -dp 3000:3000 \ -w /app -v "$(pwd):/app" \ node:12-alpine \ sh -c "apk add python && apk add g++ && apk add make && yarn install && yarn run dev"
-
Make a change to the app:
- {submitting ? 'Adding...' : 'Add Item'} + {submitting ? 'Adding...' : 'Add'}
-
Build:
docker build -t getting-started .
A few reasons to use multi-containers:
- There's a good chance you'd have to scale APIs and front-ends differently than databases
- Separate containers let you version and update versions in isolation
- While you may use a container for the database locally, you may want to use a managed service for the database in production. You don't want to ship your database engine with your app then.
- Running multiple processes will require a process manager (the container only starts one process), which adds complexity to container startup/shutdown.
Rule:
If two containers are on the same network, they can talk to each other. If they aren't, they can't.
No available Docker image of MySQL
under the Apple M1 ARM-64 environment. We use MariaDB
instead.
Two ways to put a container on a network: (1) Assign it at start or (2) connect an existing container.
Create the network first and attach the MySQLMariaDB container at startup:
-
Create the network:
docker network create todo-app
-
Start a
MySQLMariaDB container and attach it to the network.docker run -d \ --network todo-app --network-alias mysql \ -v todo-mysql-data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=secret \ -e MYSQL_DATABASE=todos \ - mysql:5.7 + mariadb
-
To confirm we have the database up and running, connect to the database and verify it connects.
- docker exec -it <mysql-container-id> mysql -p + docker exec -it <mysql-container-id> mariadb -p
Password:
secret
Then in the
MySQLMariaDB shell, listh the databases and verify:- mysql> SHOW DATABASES; + MariaDB [(none)]> SHOW DATABASES;
Make use of the nicolaka/netshoot container, which ships with a lot of tools that are useful for troubleshooting or debugging networking issues.
-
Start a new container using the
nicolaka/netshoot
image.docker run -it --network todo-app nicolaka/netshoot
-
Inside the container, we're going to use the
dig
command, which is a useful DNS tool. We're going to look up the IP address for the hostnamemysql
dig mysql
Output:
; <<>> DiG 9.16.11 <<>> mysql ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 2315 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;mysql. IN A ;; ANSWER SECTION: mysql. 600 IN A 172.18.0.2 ;; Query time: 1 msec ;; SERVER: 127.0.0.11#53(127.0.0.11) ;; WHEN: Sat Apr 24 03:10:14 UTC 2021 ;; MSG SIZE rcvd: 44
In the
ANSWER SECTION
, you will see anA
record formysql
that resolves to172.18.0.2
. Whilemysql
isn't normally a valid hostname, Docker was able to resolve it to the IP address of the container that had that network alias (remember the--network-alias
flag we used earlier?).What this means is... our app only simply needs to connect to a host named
mysql
and it'll talk to the database! It doesn't get much simpler than that!
The todo app supports the setting of a few environment variables to specify MySQLMariaDB connection settings. They are:
MYSQL_HOST
the hostname for the runningMySQLMariaDB serverMYSQL_USER
the username to use for the connectionMYSQL_PASSWORD
the password to use for the connectionMYSQL_DB
the database to use once connected
-
We'll specify each of the environment variables above, as well as connect the container to our app network.
docker run -dp 3000:3000 \ -w /app -v "$(pwd):/app" \ --network todo-app \ -e MYSQL_HOST=mysql \ -e MYSQL_USER=root \ -e MYSQL_PASSWORD=secret \ -e MYSQL_DB=todos \ node:12-alpine \ sh -c "yarn install && yarn run dev"
-
If we look at the logs for the container (
docker logs <container-id>
), we should see a message indicating it's using the mysql database. -
Open the app in your browser and add a few items to your todo list.
-
Connect to the
mysqlMariaDB database and prove that the items are being written to the database. Remember, the password issecret
.- docker exec -it <mysql-container-id> mysql -p + docker exec -it <mysql-container-id> mariadb -p
And in the mysql shell, run the following:
MariaDB [todos]> select * from todo_items;
We'll see:
+--------------------------------------+--------+-----------+ | id | name | completed | +--------------------------------------+--------+-----------+ | e69348d3-636f-4e26-bd33-fb7f5bc34167 | test | 0 | | 7c341428-d6a2-4902-a8a5-7bf08668f77d | hi | 0 | | 020cc3d5-4e5b-41fa-83f7-97ea659a324a | strike | 0 | +--------------------------------------+--------+-----------+ 3 rows in set (0.000 sec)
Obviously, your table will look different because it has your items. But, you should see them stored there!
Docker Compose is a tool that was developed to help define and share multi-container applications. With Compose, we can create a YAML file to define the services and with a single command, can spin everything up or tear it all down.
Advantage of using Compose: you can define your application stack in a file, keep it at the root of your project repo (it's now version controlled), and easily enable someone else to contribute to your project.
Use the command to check Docker Compose's version:
docker-compose version
-
At the root of the app project, create a file named
docker-compose.yml
. -
In the compose file, we'll start off by defining the schema version. In most cases, it's best to use the latest supported version. You can look at the Compose file reference for the current schema versions and the compability matrix.
-
Next, we'll define the list of services (or containers) we want to run as part of our application.
And now, we'll start migrating a service at a time into the compose file.
To remember, this was the command we were using to define our app container.
docker run -dp 3000:3000 \
-w /app -v "$(pwd):/app" \
--network todo-app \
-e MYSQL_HOST=mysql \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=secret \
-e MYSQL_DB=todos \
node:12-alpine \
sh -c "yarn install && yarn run dev"
-
First, let's define the service entry and the image for the container. We can pick any name for the service. The name will automatically become a network alias, which will be useful when defining our MySQL service.
version: "3.7" services: app: image: node:12-alpine
-
Typically, you will see the command close to the
image
definition, although there is no requirement on ordering. So, let's go ahead and move that into our file.version: "3.7" services: app: image: node:12-alpine command: sh -c "yarn install && yarn run dev"
-
Let's migrate the
-p 3000:3000
part of the command by defining theports
for the service. We will use the short syntax here, but there is also a more verbose long syntax available as well.version: "3.7" services: app: image: node:12-alpine command: sh -c "yarn install && yarn run dev" ports: - 3000:3000
-
Next, we'll migrate both the working directory (
-w /app
) and the volume mapping (-v "$(pwd):/app"
) by using theworking_dir
andvolumes
definitions. Volumes also has a short and long syntax.One advantage of Docker Compose volume definitions is we can use relative paths from the current directory.
version: "3.7" services: app: image: node:12-alpine command: sh -c "yarn install && yarn run dev" ports: - 3000:3000 working_dir: /app volumes: - ./:/app
-
Finally, we need to migrate the environment variable definitions using the
environment
key.version: "3.7" services: app: image: node:12-alpine command: sh -c "yarn install && yarn run dev" ports: - 3000:3000 working_dir: /app volumes: - ./:/app environment: MYSQL_HOST: mysql MYSQL_USER: root MYSQL_PASSWORD: secret MYSQL_DB: todos
Now, it's time to define the MySQLMariaDB service. The command that we used for that container was the following:
docker run -d \
--network todo-app --network-alias mysql \
-v todo-mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=todos \
mariadb
-
We will first define the new service and name it
mysql
so it automatically gets the network alias. We'll go ahead and specify the image to use as well.version: "3.7" services: app: # The app service definition mysql: - image: mysql:5.7 + image: mariadb
-
Next, we'll define the volume mapping. When we ran the container with
docker run
, the named volume was created automatically. However, that doesn't happen when running with Compose. We need to define the volume in the top-levelvolumes:
section and then specify the mountpoint in the service config. By simply providing only the volume name, the default options are used. There are many more options available though.version: "3.7" services: app: # The app service definition mysql: image: mysql:5.7 volumes: - todo-mysql-data:/var/lib/mysql volumes: todo-mysql-data:
-
Finally, we only need to specify the environment variables.
version: "3.7" services: app: # The app service definition mysql: image: mysql:5.7 volumes: - todo-mysql-data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: secret MYSQL_DATABASE: todos volumes: todo-mysql-data:
At this point, our complete docker-compose.yml should look like this:
version: "3.7"
services:
app:
image: node:12-alpine
command: sh -c "yarn install && yarn run dev"
ports:
- 3000:3000
working_dir: /app
volumes:
- ./:/app
environment:
MYSQL_HOST: mysql
MYSQL_USER: root
MYSQL_PASSWORD: secret
MYSQL_DB: todos
mysql:
image: mysql:5.7
volumes:
- todo-mysql-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: todos
volumes:
todo-mysql-data:
Now that we have our docker-compose.yml
file, we can start it up!
-
Make sure no other copies of the app/db are running first (
docker ps
anddocker rm -f <ids>
). -
Start up the application stack using the
docker-compose
up command. We'll add the-d
flag to run everything in the background.docker-compose up -d
When we run this, we should see output like this:
Creating network "app_default" with the default driver Creating volume "app_todo-mysql-data" with default driver Creating app_app_1 ... done Creating app_mysql_1 ... done
You'll notice that the volume was created as well as a network! By default, Docker Compose automatically creates a network specifically for the application stack (which is why we didn't define one in the compose file).
-
Let's look at the logs using the
docker-compose logs -f
command. You'll see the logs from each of the services interleaved into a single stream. This is incredibly useful when you want to watch for timing-related issues. The-f
flag "follows" the log, so will give you live output as it's generated.If you don't already, you'll see output that looks like this...
Attaching to app_app_1, app_mysql_1 mysql_1 | 2021-04-25 07:29:13+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 1:10.5.9+maria~focal started. mysql_1 | 2021-04-25 07:29:13+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql' mysql_1 | 2021-04-25 07:29:13+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 1:10.5.9+maria~focal started. mysql_1 | 2021-04-25 07:29:13+00:00 [Note] [Entrypoint]: Initializing database files mysql_1 | mysql_1 | mysql_1 | PLEASE REMEMBER TO SET A PASSWORD FOR THE MariaDB root USER ! mysql_1 | To do so, start the server, then issue the following commands: mysql_1 | mysql_1 | '/usr/bin/mysqladmin' -u root password 'new-password' mysql_1 | '/usr/bin/mysqladmin' -u root -h password 'new-password' mysql_1 | mysql_1 | Alternatively you can run: mysql_1 | '/usr/bin/mysql_secure_installation' mysql_1 | mysql_1 | which will also give you the option of removing the test mysql_1 | databases and anonymous user created by default. This is mysql_1 | strongly recommended for production servers. mysql_1 | mysql_1 | See the MariaDB Knowledgebase at https://mariadb.com/kb or the mysql_1 | MySQL manual for more instructions. mysql_1 | mysql_1 | Please report any problems at https://mariadb.org/jira mysql_1 | mysql_1 | The latest information about MariaDB is available at https://mariadb.org/. mysql_1 | You can find additional information about the MySQL part at: mysql_1 | https://dev.mysql.com mysql_1 | Consider joining MariaDB's strong and vibrant community: mysql_1 | https://mariadb.org/get-involved/ mysql_1 | mysql_1 | 2021-04-25 07:29:13+00:00 [Note] [Entrypoint]: Database files initialized mysql_1 | 2021-04-25 07:29:13+00:00 [Note] [Entrypoint]: Starting temporary server mysql_1 | 2021-04-25 07:29:13+00:00 [Note] [Entrypoint]: Waiting for server startup mysql_1 | 2021-04-25 7:29:13 0 [Note] mysqld (mysqld 10.5.9-MariaDB-1:10.5.9+maria~focal) starting as process 105 ... mysql_1 | 2021-04-25 7:29:13 0 [Note] InnoDB: Uses event mutexes mysql_1 | 2021-04-25 7:29:13 0 [Note] InnoDB: Compressed tables use zlib 1.2.11 mysql_1 | 2021-04-25 7:29:13 0 [Note] InnoDB: Number of pools: 1 mysql_1 | 2021-04-25 7:29:13 0 [Note] InnoDB: Using ARMv8 crc32 + pmull instructions mysql_1 | 2021-04-25 7:29:13 0 [Note] mysqld: O_TMPFILE is not supported on /tmp (disabling future attempts) mysql_1 | 2021-04-25 7:29:13 0 [Note] InnoDB: Using Linux native AIO mysql_1 | 2021-04-25 7:29:13 0 [Note] InnoDB: Initializing buffer pool, total size = 134217728, chunk size = 134217728 mysql_1 | 2021-04-25 7:29:13 0 [Note] InnoDB: Completed initialization of buffer pool mysql_1 | 2021-04-25 7:29:13 0 [Note] InnoDB: If the mysqld execution user is authorized, page cleaner thread priority can be changed. See the man page of setpriority(). mysql_1 | 2021-04-25 7:29:13 0 [Note] InnoDB: 128 rollback segments are active. mysql_1 | 2021-04-25 7:29:13 0 [Note] InnoDB: Creating shared tablespace for temporary tables mysql_1 | 2021-04-25 7:29:13 0 [Note] InnoDB: Setting file './ibtmp1' size to 12 MB. Physically writing the file full; Please wait ... mysql_1 | 2021-04-25 7:29:13 0 [Note] InnoDB: File './ibtmp1' size is now 12 MB. mysql_1 | 2021-04-25 7:29:13 0 [Note] InnoDB: 10.5.9 started; log sequence number 45118; transaction id 20 mysql_1 | 2021-04-25 7:29:13 0 [Note] Plugin 'FEEDBACK' is disabled. mysql_1 | 2021-04-25 7:29:13 0 [Note] InnoDB: Loading buffer pool(s) from /var/lib/mysql/ib_buffer_pool mysql_1 | 2021-04-25 7:29:13 0 [Note] InnoDB: Buffer pool(s) load completed at 210425 7:29:13 mysql_1 | 2021-04-25 7:29:13 0 [Warning] 'user' entry 'root@5c9ddc2a2b02' ignored in --skip-name-resolve mode. mysql_1 | 2021-04-25 7:29:13 0 [Warning] 'proxies_priv' entry '@% root@5c9ddc2a2b02' ignored in --skip-name-resolve mode. mysql_1 | 2021-04-25 7:29:13 0 [Note] Reading of all Master_info entries succeeded mysql_1 | 2021-04-25 7:29:13 0 [Note] Added new Master_info '' to hash table mysql_1 | 2021-04-25 7:29:13 0 [Note] mysqld: ready for connections. mysql_1 | Version: '10.5.9-MariaDB-1:10.5.9+maria~focal' socket: '/run/mysqld/mysqld.sock' port: 0 mariadb.org binary distribution mysql_1 | 2021-04-25 07:29:14+00:00 [Note] [Entrypoint]: Temporary server started. mysql_1 | Warning: Unable to load '/usr/share/zoneinfo/leap-seconds.list' as time zone. Skipping it. mysql_1 | Warning: Unable to load '/usr/share/zoneinfo/leapseconds' as time zone. Skipping it. mysql_1 | Warning: Unable to load '/usr/share/zoneinfo/tzdata.zi' as time zone. Skipping it. mysql_1 | 2021-04-25 7:29:16 5 [Warning] 'proxies_priv' entry '@% root@5c9ddc2a2b02' ignored in --skip-name-resolve mode. mysql_1 | 2021-04-25 07:29:16+00:00 [Note] [Entrypoint]: Creating database todos mysql_1 | mysql_1 | 2021-04-25 07:29:16+00:00 [Note] [Entrypoint]: Stopping temporary server mysql_1 | 2021-04-25 7:29:16 0 [Note] mysqld (initiated by: root[root] @ localhost []): Normal shutdown mysql_1 | 2021-04-25 7:29:16 0 [Note] Event Scheduler: Purging the queue. 0 events mysql_1 | 2021-04-25 7:29:16 0 [Note] InnoDB: FTS optimize thread exiting. mysql_1 | 2021-04-25 7:29:16 0 [Note] InnoDB: Starting shutdown... mysql_1 | 2021-04-25 7:29:16 0 [Note] InnoDB: Dumping buffer pool(s) to /var/lib/mysql/ib_buffer_pool mysql_1 | 2021-04-25 7:29:16 0 [Note] InnoDB: Buffer pool(s) dump completed at 210425 7:29:16 mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: Removed temporary tablespace data file: "ibtmp1" mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: Shutdown completed; log sequence number 45130; transaction id 21 mysql_1 | 2021-04-25 7:29:17 0 [Note] mysqld: Shutdown complete mysql_1 | mysql_1 | 2021-04-25 07:29:17+00:00 [Note] [Entrypoint]: Temporary server stopped mysql_1 | mysql_1 | 2021-04-25 07:29:17+00:00 [Note] [Entrypoint]: MySQL init process done. Ready for start up. mysql_1 | mysql_1 | 2021-04-25 7:29:17 0 [Note] mysqld (mysqld 10.5.9-MariaDB-1:10.5.9+maria~focal) starting as process 1 ... mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: Uses event mutexes mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: Compressed tables use zlib 1.2.11 mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: Number of pools: 1 mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: Using ARMv8 crc32 + pmull instructions mysql_1 | 2021-04-25 7:29:17 0 [Note] mysqld: O_TMPFILE is not supported on /tmp (disabling future attempts) mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: Using Linux native AIO mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: Initializing buffer pool, total size = 134217728, chunk size = 134217728 mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: Completed initialization of buffer pool mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: If the mysqld execution user is authorized, page cleaner thread priority can be changed. See the man page of setpriority(). mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: 128 rollback segments are active. mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: Creating shared tablespace for temporary tables mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: Setting file './ibtmp1' size to 12 MB. Physically writing the file full; Please wait ... mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: File './ibtmp1' size is now 12 MB. mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: 10.5.9 started; log sequence number 45130; transaction id 20 mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: Loading buffer pool(s) from /var/lib/mysql/ib_buffer_pool mysql_1 | 2021-04-25 7:29:17 0 [Note] Plugin 'FEEDBACK' is disabled. mysql_1 | 2021-04-25 7:29:17 0 [Note] InnoDB: Buffer pool(s) load completed at 210425 7:29:17 mysql_1 | 2021-04-25 7:29:17 0 [Note] Server socket created on IP: '::'. mysql_1 | 2021-04-25 7:29:17 0 [Warning] 'proxies_priv' entry '@% root@5c9ddc2a2b02' ignored in --skip-name-resolve mode. mysql_1 | 2021-04-25 7:29:17 0 [Note] Reading of all Master_info entries succeeded mysql_1 | 2021-04-25 7:29:17 0 [Note] Added new Master_info '' to hash table mysql_1 | 2021-04-25 7:29:17 0 [Note] mysqld: ready for connections. mysql_1 | Version: '10.5.9-MariaDB-1:10.5.9+maria~focal' socket: '/run/mysqld/mysqld.sock' port: 3306 mariadb.org binary distribution mysql_1 | 2021-04-25 7:29:18 3 [Warning] Aborted connection 3 to db: 'unconnected' user: 'unauthenticated' host: '172.19.0.2' (This connection closed normally without authentication) app_1 | yarn install v1.22.5 app_1 | [1/4] Resolving packages... app_1 | success Already up-to-date. app_1 | Done in 0.19s. app_1 | yarn run v1.22.5 app_1 | $ nodemon src/index.js app_1 | [nodemon] 1.19.2 app_1 | [nodemon] to restart at any time, enter `rs` app_1 | [nodemon] watching dir(s): *.* app_1 | [nodemon] starting `node src/index.js` app_1 | Waiting for mysql:3306.. app_1 | Connected! app_1 | Connected to mysql db at host mysql app_1 | Listening on port 3000
The service name is displayed at the beginning of the line (often colored) to help distinguish messages. If you want to view the logs for a specific service, you can add the service name to the end of the logs command (for example,
docker-compose logs -f app
). -
At this point, you should be able to open your app and see it running. And hey! We're down to a single command!
When you're ready to tear it all down, simply run docker-compose down
or hit the trash can on the Docker Dashboard for the entire app. The containers will stop and the network will be removed.
When you have built an image, it is good practice to scan it for security vulnerabilities using the docker scan
command. Docker has partnered with Snyk to provide the vulnerability scanning service.
For example, to scan the getting-started
image you created earlier in the tutorial, you can just type:
docker scan getting-started
The scan uses a constantly updated database of vulnerabilities, so the output you see will vary as new vulnerabilities are discovered, but it might look something like this:
Testing getting-started...
✗ Medium severity vulnerability found in binutils/binutils
Description: Improper Input Validation
Info: https://snyk.io/vuln/SNYK-ALPINE311-BINUTILS-1255521
Introduced through: binutils/binutils@2.33.1-r0, gcc/g++@9.3.0-r0, gcc/libgcc@9.3.0-r0, gcc/libgomp@9.3.0-r0
From: binutils/binutils@2.33.1-r0
From: gcc/g++@9.3.0-r0 > binutils/binutils@2.33.1-r0
From: gcc/libgcc@9.3.0-r0 > binutils/binutils@2.33.1-r0
and 2 more...
Fixed in: 2.33.1-r1
Organization: undefined
Package manager: apk
Project name: docker-image|getting-started
Docker image: getting-started
Platform: linux/arm64
Tested 38 dependencies for known vulnerabilities, found 1 vulnerability.
For more free scans that keep your images secure, sign up to Snyk at https://dockr.ly/3ePqVcp
Did you know that you can look at what makes up an image? Using the docker image history
command, you can see the command that was used to create each layer within an image.
-
Use the
docker image history
command to see the layers in the getting-started image you created earlier in the tutorial.docker image history getting-started
-
You'll notice that several of the lines are truncated. If you add the
--no-trunc
flag, you'll get the full output (yes... funny how you use a truncated flag to get untruncated output, huh?)docker image history --no-trunc getting-started
Now that you've seen the layering in action, there's an important lesson to learn to help decrease build times for your container images.
Once a layer changes, all downstream layers have to be recreated as well
Let's look at the Dockerfile we were using one more time...
FROM node:12-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
Going back to the image history output, we see that each command in the Dockerfile becomes a new layer in the image. You might remember that when we made a change to the image, the yarn dependencies had to be reinstalled. Is there a way to fix this? It doesn't make much sense to ship around the same dependencies every time we build, right?
To fix this, we need to restructure our Dockerfile to help support the caching of the dependencies. For Node-based applications, those dependencies are defined in the package.json
file. So, what if we copied only that file in first, install the dependencies, and then copy in everything else? Then, we only recreate the yarn dependencies if there was a change to the package.json
. Make sense?
- Update the Dockerfile to copy in the
package.json
first, install dependencies, and then copy everything else in.
FROM node:12-alpine
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --production
COPY . .
CMD ["node", "src/index.js"]
- Create a file named
.dockerignore
in the same folder as theDockerfile
with the following contents.
node_modules
.dockerignore
files are an easy way to selectively copy only image relevant files. You can read more about this here. In this case, the node_modules
folder should be omitted in the second COPY
step because otherwise, it would possibly overwrite files which were created by the command in the RUN
step. For further details on why this is recommended for Node.js applications and other best practices, have a look at their guide on Dockerizing a Node.js web app.
- Build a new image using
docker build
.
docker build -t getting-started .
-
Now, make a change to the
src/static/index.html
file (like change the<title>
to say "The Awesome Todo App"). -
Build the Docker image now using
docker build -t getting-started .
again. This time, your output should look a little different.
While we're not going to dive into it too much in this tutorial, multi-stage builds are an incredibly powerful tool to help use multiple stages to create an image. There are several advantages for them:
- Separate build-time dependencies from runtime dependencies
- Reduce overall image size by shipping only what your app needs to run
When building Java-based applications, a JDK is needed to compile the source code to Java bytecode. However, that JDK isn't needed in production. Also, you might be using tools like Maven or Gradle to help build the app. Those also aren't needed in our final image. Multi-stage builds help.
FROM maven AS build
WORKDIR /app
COPY . .
RUN mvn package
FROM tomcat
COPY --from=build /app/target/file.war /usr/local/tomcat/webapps
In this example, we use one stage (called build
) to perform the actual Java build using Maven. In the second stage (starting at FROM tomcat
), we copy in files from the build
stage. The final image is only the last stage being created (which can be overridden using the --target
flag).
When building React applications, we need a Node environment to compile the JS code (typically JSX), SASS stylesheets, and more into static HTML, JS, and CSS. If we aren't doing server-side rendering, we don't even need a Node environment for our production build. Why not ship the static resources in a static nginx container?
FROM node:12 AS build
WORKDIR /app
COPY package* yarn.lock ./
RUN yarn install
COPY public ./public
COPY src ./src
RUN yarn run build
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
Here, we are using a node:12
image to perform the build (maximizing layer caching) and then copying the output into an nginx container. Cool, huh?