Docker and Docker Compose
Introduction to Docker and docker compose
Containers
Vocab
- Development Environment (dev)
- The environment where you write your code
- Ex. Your laptop
- Add features; Find and eliminate bugs
- Production environment (prod)
- The environment where your app will eventually live
- The live server with real end users
- Do everything we can to avoid bugs in production
Deployment Headaches
- It works on my laptop!
- Run your code in production and it's broken
- Many causes
- Different version of compiler/interpreter
- Dependencies not linked
- Hard-coded paths
- Different environment variables
- etc.
Virtual Machines
- Simulate an entire machine
- Run the virtual machine (VM) in your development environment for testing
- Run an exact copy of the VM on the production server
- No more surprise deployment issues
- Simulating an entire machine can be inefficient
- If you've run a VM on your laptop you know how slow this can get
Containers
- Effectively, lightweight VMs
- Efficiently shares resources across containers
- As opposed to VMs which require dedicated ram/cpu/disk
Security
- Can't break out of a VM/Container [Without sophisticated attacks]
- If an attacker compromises the server, they can only access what you put in the container
- Can't "rm -rf /" your entire machine
- Patch the exploited vulnerability and rebuild the image
- The attacker can still cause significant damage and steal private data
- They just can't destroy your server box
Security
- Sometimes an app has to allow code injection attacks to function
- Autolab
- AWS
- Digital Ocean
- Run user code in their own VM/Container
- One malicious user cannot take down the entire service
Docker
- Docker is software that's used to create containers
- Install Docker in your development environment to test containers
- Install Docker in your production environment and run the same containers
Dockerfile
- To start working with Docker, write a Dockerfile
- This file contains all the instructions needed to build a Docker image
- Some similarities to a Makefile
Dockerfile
- Let's explore this sample Dockerfile
- This Dockerfile creates an image for a node.js app
- You don't need to know node.js. We're using this example to learn Docker
FROM ubuntu:26.04
WORKDIR /app
RUN apt-get update --fix-missing
RUN apt-get install -y nodejs
RUN apt-get install -y npm
COPY package.json package.json
RUN npm install
COPY server.js server.js
COPY src/ src/
CMD ["node", "server.js"]Dockerfile
- The first line of your Dockerfile will specify the base image
- This image is downloaded and the rest of your Dockerfile adds to this image
- In this example: We start with Ubuntu 26.04
Dockerfile
- Use WORKDIR to change your current working directory
- Same as "cd"
- This Dockerfile sets the working directory to
/app - You'll know where your files are if you ever have to navigate the container
Dockerfile
- Use the
RUNkeyword to run commands in the base image - We are starting with a fresh Ubuntu install
- Need to run any commands needed for your app to run
- This is the same as running these commands in the terminal
- Use flags like
-yto ensure everything is automated - All commands run as root
- No need for
sudo
- No need for
Dockerfile
- Use
COPYto copy local files into your imageCOPY <local_src> <image_dest> - Need to copy all files needed for your app
- Destination is relative to the working directory that we set on line 3
Dockerfile
- Install any dependencies needed for your app
- Copy in any files specifying your dependencies (e.g.
go.modandgo.sum) - Very useful to do this before copying your code
- Docker has a very good caching system and won't repeat steps where nothing changed
- Only reinstall dependencies when your dependencies change, not every time your code changes
Dockerfile
- Get your app ready to run
- Copy all the code for your app into the image
- If needed, compile your code after copying it
- Test compiling and running your code locally first (e.g. If you've only ever ran
go run .you'll need to learn how to compile and run separately)
- Test compiling and running your code locally first (e.g. If you've only ever ran
Dockerfile
- Finally, use CMD to run your app
- Important: Do not use RUN to run your app!
- RUN will execute the command when the image is being built
- CMD will execute when the container is run
Dockerfile
- There are many base images to choose from
- Start with an image with your language installed to simplify your Dockerfile
- Search "docker language" to find images/tutorials for your other language
- This image starts with node and npm installed so we can remove the installation lines
Running Your App
- Now that we have a Dockerfile, build an image using this command
docker build -t my-image .
- But we're interested in containers
- Images can be used to create many containers (Like a class being used to create objects)
- To create a container from an image, run this command
docker run -p 8080:8080 my-mage- The
-p 8080:8080will map local port 8080 to container port 8080 - Need to map the port for your app to be accessible from outside the container (e.g. From your browser)
- Your app is now running in a docker container
Running Your App
- When preparing your app to run in a container
- Do not use
localhost[in your code] - Use
0.0.0.0as the host instead - This allows your app to be accessed from outside the container by listening on all network interfaces
- Do not use
- Go does this by default when using the
netpackage
Docker Compose
Docker Compose
- Docker compose manages building/running docker images/containers
- Build and run with one command
docker compose up --build --force-recreate --renew-anon-volumes - No need to use
docker buildanddocker run - Will be used to manage multiple containers
- Separate container for your database
- Configure using a file named
compose.yaml,compose.yml, ordocker-compose.yml
Docker Compose
- CAUTION: DO NOT store passwords in plain text like this!
- Fine for Homework 1
- We will explore a better setup for homework 2
- The
compose.yamlfile uses the YAML format- White space (indentation) matters
- Format:
<key>:<value> - List values prefixed with
-
services:
mysql:
image: mysql
environment:
MYSQL_ROOT_PASSWORD: '7PtypF^*6&to@PAhwaw5'
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
app:
build: .
environment:
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
ports:
- "8080:8080"Docker Compose
- List of all the services for docker compose to run
- A docker container is created for each service
- This compose file will create 2 containers
- One for a mySQL database named
mysql - One for a web server named
app
- One for a mySQL database named
- The name of each service will become its hostname when using network protocols across containers
services:
mysql:
image: mysql
environment:
MYSQL_ROOT_PASSWORD: '7PtypF^*6&to@PAhwaw5'
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
app:
build: .
environment:
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
ports:
- "8080:8080"Docker Compose
- For each service, specify how to build its image
- Use
imageto specify an image to use- Same as having a 1 line Dockerfile
FROM mysql- Useful when you need very little customization
- Use
buildto specify a local Dockerfile- Specify the directory where the Dockerfile can be found
- Using
.to specify the current directory
services:
mysql:
image: mysql
environment:
MYSQL_ROOT_PASSWORD: '7PtypF^*6&to@PAhwaw5'
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
app:
build: .
environment:
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
ports:
- "8080:8080"Docker Compose
- Map a local port to a container port
- Same as using
-p 8080:8080when running a single container
services:
mysql:
image: mysql
environment:
MYSQL_ROOT_PASSWORD: '7PtypF^*6&to@PAhwaw5'
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
app:
build: .
environment:
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
ports:
- "8080:8080"Docker Compose
- Use
environmentto set any needed environment variables - When using MySQL, the image expects certain environment variables that are used for setup
- This is how you setup your username/password and the name of your database
- Hint: Postgres uses the same concept, but with different variable names
services:
mysql:
image: mysql
environment:
MYSQL_ROOT_PASSWORD: '7PtypF^*6&to@PAhwaw5'
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
app:
build: .
environment:
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
ports:
- "8080:8080"Docker Compose
- You can also set environment variables in your app container
- Makes for a cleaner setup and can avoid storing plain-text passwords in your code
- When we learn about authentication, we will discus a better way to handle passwords
- Brief version: Use a
.envfile and use${VARIABLE_NAME}to read your variables
- Brief version: Use a
services:
mysql:
image: mysql
environment:
MYSQL_ROOT_PASSWORD: '7PtypF^*6&to@PAhwaw5'
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
app:
build: .
environment:
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
ports:
- "8080:8080"Docker Compose
- We have a detrimental race condition
- Docker compose will start both containers concurrently
- Your code will likely connect to your database on startup
- What if the database is still starting up and not accepting connections yet?
services:
mysql:
image: mysql
environment:
MYSQL_ROOT_PASSWORD: '7PtypF^*6&to@PAhwaw5'
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
app:
build: .
environment:
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
ports:
- "8080:8080"Docker Compose
- Many different solutions to the race
- Find and implement your favorite approach:
- Handle rejected connections in your code gracefully and retry
- Use docker compose's
depends_onwithhealthcheck(After reading the documentation) - Use a 3rd party solution like
ufoscout's solution on GitHub
services:
mysql:
image: mysql
environment:
MYSQL_ROOT_PASSWORD: '7PtypF^*6&to@PAhwaw5'
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
app:
build: .
environment:
MYSQL_USER: mysql_username
MYSQL_PASSWORD: 'WTc&$5#jAd6jRe9gJ0nL'
MYSQL_DATABASE: my_db
ports:
- "8080:8080"Docker Compose
When communicating across containers, the service name is the container's hostname
Connection string:
postgres://user:pass@HOST:5432/dbRunning with
go run .HOST =
localhostRunning with
docker compose upHOST =
postgres
Docker Compose
Running Your App
- To run your app
docker compose up
- To rebuild and restart the containers
docker compose up --build --force-recreate --renew-anon-volumes- Will ensure your newest code is running in the containers
- Renewing anonymous volumes will destroy your database
- Remove this if you want your data to persist
- To restart the container without rebuilding
docker compose restart