Docker is a platform for building, running and shipping applications
I'm learning from these tutorials on YouTube:
- Docker tutorial for Beginners by Programming with Mosh
- Learn Docker in 1 Hour by JavaScript Mastery
If your code works on one machine, it can work on any other machine with Docker
You don't have to say "It works on my machine" anymore
With Docker, you can "pack" your application with everything it needs and run it on any machine
For example, you are developing a website with Node 14 and MongoDB 4, you can make a package and run this package on any machine with Docker
Docker will automatically install and run all the dependencies in isolated environment called Container
This is also helpful if you are running multiple applications with different versions on software on one machine, now you can run them side by side
Another benefit is, when you are finished using/developing one application, you can remove it with all it's dependencies in one go
Docker helps consistently build, run and ship applications
You might think that container is similar to a Virtual Machine (VM)
The main difference is that VM needs a full operating system to run
It makes VM slower to load and more intensive on resources
Container uses the operating system of host
This makes Containers more lightweight
yay -S docker or go to their website and download it from there
Check if everything is correct with docker version command
You might get some errors, run these commands
sudo systemctl enable docker
sudo systemctl start docker
sudo usermod -aG docker ${USER} Note: You might need to log out and log back in to apply changes
Now, after running docker version you want to see something like this (both client and server running)
Client:
Version: 27.1.1
API version: 1.46
Go version: go1.22.5
Git commit: 63125853e3
Built: Thu Jul 25 17:06:22 2024
OS/Arch: linux/amd64
Context: default
Server:
Engine:
Version: 27.1.1
API version: 1.46 (minimum version 1.24)
Go version: go1.22.5
Git commit: cc13f95251
Built: Thu Jul 25 17:06:22 2024
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: v1.7.20
GitCommit: 8fc6bcff51318944179630522a095cc9dbf9f353.m
runc:
Version: 1.1.13
GitCommit:
docker-init:
Version: 0.19.0
GitCommit: de40ad0
Imagine you have an application
Now you need to "dockerize" it - add Dockerfile to it
Dockerfile is a plain text file which provides information to Docker
Now, Docker can make an Image from the app
Image is like a lightweight, standalone, executable package
Image contains everything our application need to run:
- Cut-down OS
- Runtime environment (Node for example)
- Application files
- Third-party libraries
- Environment variables
Once we have an Image, we tell Docker to run it inside Container
Container is an environment for Image to run
Container is a special type of process
It has it's own file system, which is provided by the Image
Once you have an Image, you can push it to Docker Registry - Docker Hub (like GitHub to Git) so everyone else can use it
Docker Hub is a registry for Docker images
Once you have starting developing your application, you need to add Dockerfile
Just add a new file called Dockerfile, the name has to be the same as you see there, no extension
Now we will be typing code into the Dockerfile
First, you need to specify what container you are starting on, like a base container, like what container will you inherit from
You can do this with FROM command
FROM linuxNow if you have JavaScript application, you are most likely using Node
In this case, you don't need to start from linux, you can start from node, which is already based on linux
FROM nodeYou can specify what Linux distribution you want to use
For example Alpine Linux is very small (5MB !), so it might be a good consideration for Docker container (small image size, even faster load times)
FROM node:alpineIf you want to explore more containers, go to hub.docker.com
Now we need to copy our application files with the COPY command
With following command, we will copy all the files from our program into Image's /app directory
The Image has a file system, and we will create a directory called app inside this filesytem
FROM node:alpine
COPY . /appFinally, you can use CMD function to run commands on Image
FROM node:alpine
COPY . /app
CMD node /app/index.jsIf you don't want to use the absolute path for files, you can set work directory with WORKDIR command
It's basically like cd on Linux
FROM node:alpine
COPY . /app
WORKDIR /app
CMD node index.jsAnother example of Dockerfile
FROM ubuntu:latest
RUN apt update && apt install -y coffee-maker
COPY . /coffee-recipe
RUN ./prepare-beans.sh
CMD ["./brew-coffee.sh"]Another example of Dockerfile
This one can be used to run React app
FROM node:20-alpine
# make group and user which will only be able to run app. it's for security reasons
RUN addgroup app && adduser -S -G app app
# switch to user "app"
USER app
# change current working directory
WORKDIR /app
# copy package.json and package-lock.json
COPY package*.json ./
# change permissions of app folder
USER root
RUN chown -r app:app .
USER app
# install dependencies
RUN npm install
# copy all other files
COPY . .
# expose port 5173. this container will listen on this specific port
EXPOSE 5173
# run the app
CMD npm run devThis is very similar to .gitignore file
It basically tells Docker, which files not to upload
node_modules/
Now, once you are done, you can go to terminal to Build Docker Image
You can do this with docker build command, which looks like this:
docker build -t <tag> <dockerfile_location>
# for example
docker build -t hello-docker .Tag is like a name for the Docker Image, which will identify it. It is set with -t flag
At the end, you need to add location to the Dockerfile. Since we are in the same directory as the Dockerfile, you can just add .
You might think that you will see a new file/files after building Docker Image, but it's not the case
You project will look exactly the same as before
How Docker stores files is complex and not needed to understand right now
To see all Docker Images on your computer, type one of following command in your terminal
docker images
docker image lsYou can see Repository, Tag, Image ID, Created and Size there
docker run <name>
# for example
docker run hello-dockerAfter entering this command, your application should run
Now imagine we have for example a React app and we want to view it
We specify that we want it to run on port 5173 and Node gave us https://localhost:5173 as the address of the site
But if we entered this address to our browser, it will not show anything
Why? Because it the address of Docker Container, not our machine
How to deal with this? We need to map the port of Docker Container to our machine when running the container
It's as simple as this
docker run -p <container>:<our-machine> <name>
# for example
docker run -p 5173:5173 react-appNote: If you try to run multiple containers on the same port, you will get an error. Check the next chapter to find solution
If you have Vite app, you need to modify package.json file in order to make it work
Just replace ..."scripts": {"dev": "vite"... with ..."scripts": {"dev": "vite --host"...
Now imagine you are developing your app and you make some changes to it
You might see that nothing changes inside our container
You can adjust the run command like this
docker run -p 5173:5173 -v "$(pwd):/app" -v /app/node-modules react-appThe -v flag (volume) will tell Docker that you want to mount your current directory (pwd command) to /app directory in Container
This basically means that our local folder is linked to /app folder in Container, and any changes will be immediately reflected to Container
Now the command gets pretty long, doesn't it?
We can use Docker Compose to deal with this
First of all, you need to install Docker Compose
yay -S docker-compose or visit their website and follow the steps there
To use Docker Compose, just add compose.yaml file next to Dockerfle
services:
web:
build:
context: .
ports:
- 5173:5173
volumes:
- .:/app
- /app/node_modulesNow instead of running the previous long docker run ... command, run docker compose up
Note: You might get
Permissioin Deniederror. Simply runsudo docker compose upinstead
services:
backend:
build:
context: ./backend/
ports:
- "5000:5000"
volumes:
- ./backend:/app
environment:
FLASK_ENV: development
frontend:
build:
context: ./frontend/
ports:
- "3000:3000"
volumes:
- ./frontend:/app
- /app/node_modules
depends_on:
- backend
command: npm run dev -- --hostdocker-compose up --build
docker-compose down --rmi all --volumes --remove-orphans But this still doesn't fully solve the problem
Whenever we change package.json file, we need to rebuild it
Here, we can use Docker Compose Watch
Docker Compose Watch can do these things
- Sync
- Rebuild
- Sync-Restart
Now I have no idea what was happening in the tutorial, so I will just write the whole compose.yaml file
# specify the version of docker-compose
version: "3.8"
# define the services/containers to be run
services:
# define the frontend service
# we can use any name for the service. A standard naming convention is to use "web" for the frontend
web:
# we use depends_on to specify that service depends on another service
# in this case, we specify that the web depends on the api service
# this means that the api service will be started before the web service
depends_on:
- api
# specify the build context for the web service
# this is the directory where the Dockerfile for the web service is located
build: ./frontend
# specify the ports to expose for the web service
# the first number is the port on the host machine
# the second number is the port inside the container
ports:
- 5173:5173
# specify the environment variables for the web service
# these environment variables will be available inside the container
environment:
VITE_API_URL: http://localhost:8000
# this is for docker compose watch mode
# anything mentioned under develop will be watched for changes by docker compose watch and it will perform the action mentioned
develop:
# we specify the files to watch for changes
watch:
# it'll watch for changes in package.json and package-lock.json and rebuild the container if there are any changes
- path: ./frontend/package.json
action: rebuild
- path: ./frontend/package-lock.json
action: rebuild
# it'll watch for changes in the frontend directory and sync the changes with the container real time
- path: ./frontend
target: /app
action: sync
# define the api service/container
api:
# api service depends on the db service so the db service will be started before the api service
depends_on:
- db
# specify the build context for the api service
build: ./backend
# specify the ports to expose for the api service
# the first number is the port on the host machine
# the second number is the port inside the container
ports:
- 8000:8000
# specify environment variables for the api service
# for demo purposes, we're using a local mongodb instance
environment:
DB_URL: mongodb://db/anime
# establish docker compose watch mode for the api service
develop:
# specify the files to watch for changes
watch:
# it'll watch for changes in package.json and package-lock.json and rebuild the container and image if there are any changes
- path: ./backend/package.json
action: rebuild
- path: ./backend/package-lock.json
action: rebuild
# it'll watch for changes in the backend directory and sync the changes with the container real time
- path: ./backend
target: /app
action: sync
# define the db service
db:
# specify the image to use for the db service from docker hub. If we have a custom image, we can specify that in this format
# In the above two services, we're using the build context to build the image for the service from the Dockerfile so we specify the image as "build: ./frontend" or "build: ./backend".
# but for the db service, we're using the image from docker hub so we specify the image as "image: mongo:latest"
# you can find the image name and tag for mongodb from docker hub here: https://hub.docker.com/_/mongo
image: mongo:latest
# specify the ports to expose for the db service
# generally, we do this in api service using mongodb atlas. But for demo purposes, we're using a local mongodb instance
# usually, mongodb runs on port 27017. So we're exposing the port 27017 on the host machine and mapping it to the port 27017 inside the container
ports:
- 27017:27017
# specify the volumes to mount for the db service
# we're mounting the volume named "anime" inside the container at /data/db directory
# this is done so that the data inside the mongodb container is persisted even if the container is stopped
volumes:
- anime:/data/db
# define the volumes to be used by the services
volumes:
anime:Also, there is a Link to GitHub for this project
To run this, you need to run 2 commands in 2 terminals
docker compose updocker compose watchIf you list all running Docker Images with docker ps, you might see a lot of Images, which might cause a problems sometimes
To stop a Docker Image, it's as simple as this
docker stop <name-or-id>
# for example
docker stop website
docker stop c5a3bIf you have a lot of stopped containers, which you want to remove, you can run this command
docker container pruneNote: This will remove all stopped containers
You can also remove just specific container with this command
docker rm <name-or-id>
# for example
docker rm website
docker rm aa7If you try to remove running container, you will get an error
You have to either stop the container first, or remove it by force
docker rm aa7 --forceThe command used to take an Image from Docker Hub to your local machine is pull
You can pull any Image from there, either pre-made images like ubuntu or node, or your own Images
It's as simple as this
docker pull <name>
# for example
docker pull ubuntu
docker pull cyprich/websiteYou can verify and run the container with commands you have learned before
docker images
docker run cyprich/website
```
> Note: You can try it online with sites like [Play with Docker](https://labs.play-with-docker.com/), which allows you to make a virtual machine and run Docker image there
## Publish Docker Image
It you want to put your own Docker Image to Docker Hub, you need to follow these steps
First of all, you need to Login
```shell
docker loginJust enter your username and password from Docker Hub and you should successfully Login
Now run this commands
docker tag <local-image-name> <username>/<remote-image-name>
docker push <username>/<remote-image-name>
# example
docker tag website cyprich/website
docker push cyprich/websiteNow if you went to hub.docker.com/repositories and search for your username, you will see your container
Keep in mind, that now anyone can use your container
You can search for Ubuntu on Docker Hub
You can run it with following command
docker run ubuntuIf you already have ubuntu image on your system, it will run, otherwise it will download it
Note: On the official site, there is
docker pull ubuntucommand, but this is like a shortcut
By typing following command, you can list Docker processes on your system
docker psYou will (probably) not see anything right now, because the container that we ran stopped immediately
To list all Docker processes, type the following command
docker ps -aNow you can see that you have "Exited" Ubuntu container
To run container interactively, you can run this command
docker run -it <name>
# in this case
docker run -it ubuntuNow you are running Ubuntu
You can enter commands and whatever